* feat: C-Suite expansion — 8 new executive advisory roles Add COO, CPO, CMO, CFO, CRO, CISO, CHRO advisors and Executive Mentor. Expands C-level advisory from 2 to 10 roles with 74 total files. Each role includes: - SKILL.md (lean, <5KB, ~1200 tokens for context efficiency) - Reference docs (loaded on demand, not at startup) - Python analysis scripts (stdlib only, runnable CLI) Executive Mentor features /em: slash commands (challenge, board-prep, hard-call, stress-test, postmortem) with devil's advocate agent. 21 Python tools, 24 reference frameworks, 28,379 total lines. All SKILL.md files combined: ~17K tokens (8.5% of 200K context window). Badge: 88 → 116 skills * feat: C-Suite orchestration layer + 18 complementary skills ORCHESTRATION (new): - cs-onboard: Founder interview → company-context.md - chief-of-staff: Routing, synthesis, inter-agent orchestration - board-meeting: 6-phase multi-agent deliberation protocol - decision-logger: Two-layer memory (raw transcripts + approved decisions) - agent-protocol: Inter-agent invocation with loop prevention - context-engine: Company context loading + anonymization CROSS-CUTTING CAPABILITIES (new): - board-deck-builder: Board/investor update assembly - scenario-war-room: Cascading multi-variable what-if modeling - competitive-intel: Systematic competitor tracking + battlecards - org-health-diagnostic: Cross-functional health scoring (8 dimensions) - ma-playbook: M&A strategy (acquiring + being acquired) - intl-expansion: International market entry frameworks CULTURE & COLLABORATION (new): - culture-architect: Values → behaviors, culture code, health assessment - company-os: EOS/Scaling Up operating system selection + implementation - founder-coach: Founder development, delegation, blind spots - strategic-alignment: Strategy cascade, silo detection, alignment scoring - change-management: ADKAR-based change rollout framework - internal-narrative: One story across employees/investors/customers UPGRADES TO EXISTING ROLES: - All 10 roles get reasoning technique directives - All 10 roles get company-context.md integration - All 10 roles get board meeting isolation rules - CEO gets stage-adaptive temporal horizons (seed→C) Key design decisions: - Two-layer memory prevents hallucinated consensus from rejected ideas - Phase 2 isolation: agents think independently before cross-examination - Executive Mentor (The Critic) sees all perspectives, others don't - 25 Python tools total (stdlib only, no dependencies) 52 new files, 10 modified, 10,862 new lines. Total C-suite ecosystem: 134 files, 39,131 lines. * fix: connect all dots — Chief of Staff routes to all 28 skills - Added complementary skills registry to routing-matrix.md - Chief of Staff SKILL.md now lists all 28 skills in ecosystem - Added integration tables to scenario-war-room and competitive-intel - Badge: 116 → 134 skills - README: C-Level Advisory count 10 → 28 Quality audit passed: ✅ All 10 roles: company-context, reasoning, isolation, invocation ✅ All 6 phases in board meeting ✅ Two-layer memory with DO_NOT_RESURFACE ✅ Loop prevention (no self-invoke, max depth 2, no circular) ✅ All /em: commands present ✅ All complementary skills cross-reference roles ✅ Chief of Staff routes to every skill in ecosystem * refactor: CEO + CTO advisors upgraded to C-suite parity Both roles now match the structural standard of all new roles: - CEO: 11.7KB → 6.8KB SKILL.md (heavy content stays in references) - CTO: 10KB → 7.2KB SKILL.md (heavy content stays in references) Added to both: - Integration table (who they work with and when) - Key diagnostic questions - Structured metrics dashboard table - Consistent section ordering (Keywords → Quick Start → Responsibilities → Questions → Metrics → Red Flags → Integration → Reasoning → Context) CEO additions: - Stage-adaptive temporal horizons (seed=3m/6m/12m → B+=1y/3y/5y) - Cross-references to culture-architect and board-deck-builder CTO additions: - Key Questions section (7 diagnostic questions) - Structured metrics table (DORA + debt + team + architecture + cost) - Cross-references to all peer roles All 10 roles now pass structural parity: ✅ Keywords ✅ QuickStart ✅ Questions ✅ Metrics ✅ RedFlags ✅ Integration * feat: add proactive triggers + output artifacts to all 10 roles Every C-suite role now specifies: - Proactive Triggers: 'surface these without being asked' — context-driven early warnings that make advisors proactive, not reactive - Output Artifacts: concrete deliverables per request type (what you ask → what you get) CEO: runway alerts, board prep triggers, strategy review nudges CTO: deploy frequency monitoring, tech debt thresholds, bus factor flags COO: blocker detection, scaling threshold warnings, cadence gaps CPO: retention curve monitoring, portfolio dog detection, research gaps CMO: CAC trend monitoring, positioning gaps, budget staleness CFO: runway forecasting, burn multiple alerts, scenario planning gaps CRO: NRR monitoring, pipeline coverage, pricing review triggers CISO: audit overdue alerts, compliance gaps, vendor risk CHRO: retention risk, comp band gaps, org scaling thresholds Executive Mentor: board prep triggers, groupthink detection, hard call surfacing This transforms the C-suite from reactive advisors into proactive partners. * feat: User Communication Standard — structured output for all roles Defines 3 output formats in agent-protocol/SKILL.md: 1. Standard Output: Bottom Line → What → Why → How to Act → Risks → Your Decision 2. Proactive Alert: What I Noticed → Why It Matters → Action → Urgency (🔴🟡⚪) 3. Board Meeting: Decision Required → Perspectives → Agree/Disagree → Critic → Action Items 10 non-negotiable rules: - Bottom line first, always - Results and decisions only (no process narration) - What + Why + How for every finding - Actions have owners and deadlines ('we should consider' is banned) - Decisions framed as options with trade-offs - Founder is the highest authority — roles recommend, founder decides - Risks are concrete (if X → Y, costs $Z) - Max 5 bullets per section - No jargon without explanation - Silence over fabricated updates All 10 roles reference this standard. Chief of Staff enforces it as a quality gate. Board meeting Phase 4 uses the Board Meeting Output format. * feat: Internal Quality Loop — verification before delivery No role presents to the founder without passing verification: Step 1: Self-Verification (every role, every time) - Source attribution: where did each data point come from? - Assumption audit: [VERIFIED] vs [ASSUMED] tags on every finding - Confidence scoring: 🟢 high / 🟡 medium / 🔴 low per finding - Contradiction check against company-context + decision log - 'So what?' test: every finding needs a business consequence Step 2: Peer Verification (cross-functional) - Financial claims → CFO validates math - Revenue projections → CRO validates pipeline backing - Technical feasibility → CTO validates - People/hiring impact → CHRO validates - Skip for single-domain, low-stakes questions Step 3: Critic Pre-Screen (high-stakes only) - Irreversible decisions, >20% runway impact, strategy changes - Executive Mentor finds weakest point before founder sees it - Suspicious consensus triggers mandatory pre-screen Step 4: Course Correction (after founder feedback) - Approve → log + assign actions - Modify → re-verify changed parts - Reject → DO_NOT_RESURFACE + learn why - 30/60/90 day post-decision review Board meeting contributions now require self-verified format with confidence tags and source attribution on every finding. * fix: resolve PR review issues 1, 4, and minor observation Issue 1: c-level-advisor/CLAUDE.md — completely rewritten - Was: 2 skills (CEO, CTO only), dated Nov 2025 - Now: full 28-skill ecosystem map with architecture diagram, all roles/orchestration/cross-cutting/culture skills listed, design decisions, integration with other domains Issue 4: Root CLAUDE.md — updated all stale counts - 87 → 134 skills across all 3 references - C-Level: 2 → 33 (10 roles + 5 mentor commands + 18 complementary) - Tool count: 160+ → 185+ - Reference count: 200+ → 250+ Minor observation: Documented plugin.json convention - Explained in c-level-advisor/CLAUDE.md that only executive-mentor has plugin.json because only it has slash commands (/em: namespace) - Other skills are invoked by name through Chief of Staff or directly Also fixed: README.md 88+ → 134 in two places (first line + skills section) * fix: update all plugin/index registrations for 28-skill C-suite 1. c-level-advisor/.claude-plugin/plugin.json — v2.0.0 - Was: 2 skills, generic description - Now: all 28 skills listed with descriptions, all 25 scripts, namespace 'cs', full ecosystem description 2. .codex/skills-index.json — added 18 complementary skills - Was: 10 roles only - Now: 28 total c-level entries (10 roles + 6 orchestration + 6 cross-cutting + 6 culture) - Each with full description for skill discovery 3. .claude-plugin/marketplace.json — updated c-level-skills entry - Was: generic 2-skill description - Now: v2.0.0, full 28-skill ecosystem description, skills_count: 28, scripts_count: 25 * feat: add root SKILL.md for c-level-advisor ClawHub package --------- Co-authored-by: Leo <leo@openclaw.ai>
1101 lines
43 KiB
Python
1101 lines
43 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
okr_tracker.py — OKR Cascade and Alignment Tracker
|
||
|
||
Tracks OKR progress from company → department → team level.
|
||
Calculates scores, flags at-risk key results, and generates alignment reports.
|
||
|
||
Scoring: Google's 0.0–1.0 scale (target: 0.6–0.7; hitting 1.0 means goal was too easy)
|
||
|
||
Usage:
|
||
python okr_tracker.py # Runs with sample data
|
||
python okr_tracker.py --input okrs.json # Custom OKR data
|
||
python okr_tracker.py --input okrs.json --output report.txt
|
||
python okr_tracker.py --format json # Machine-readable output
|
||
"""
|
||
|
||
import json
|
||
import sys
|
||
import argparse
|
||
from datetime import datetime, date
|
||
from typing import Any
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Scoring Engine
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# OKR health thresholds (Google-style 0.0–1.0 scale)
|
||
SCORE_THRESHOLDS = {
|
||
"on_track": 0.70, # Above this: healthy
|
||
"at_risk": 0.40, # Between at_risk and on_track: needs attention
|
||
# Below at_risk: off track
|
||
}
|
||
|
||
STATUS_LABELS = {
|
||
"on_track": "🟢 On Track",
|
||
"at_risk": "🟡 At Risk",
|
||
"off_track": "🔴 Off Track",
|
||
"complete": "✅ Complete",
|
||
"not_started": "⬜ Not Started",
|
||
}
|
||
|
||
RISK_LABELS = {
|
||
"critical": "🔴 Critical",
|
||
"high": "🟠 High",
|
||
"medium": "🟡 Medium",
|
||
"low": "🟢 Low",
|
||
}
|
||
|
||
|
||
def calculate_kr_score(kr: dict) -> float:
|
||
"""
|
||
Calculate a Key Result's progress score (0.0–1.0).
|
||
|
||
Supports multiple KR types:
|
||
- numeric: current_value / target_value
|
||
- percentage: current_pct / target_pct
|
||
- milestone: milestone_score (0.0–1.0 provided directly)
|
||
- boolean: done (1.0) / not done (0.0)
|
||
"""
|
||
kr_type = kr.get("type", "numeric")
|
||
|
||
if kr_type == "boolean":
|
||
return 1.0 if kr.get("done", False) else 0.0
|
||
|
||
elif kr_type == "milestone":
|
||
# Milestone KRs have explicit score (0.0–1.0) or count of milestones hit
|
||
milestones_total = kr.get("milestones_total", 1)
|
||
milestones_hit = kr.get("milestones_hit", 0)
|
||
explicit_score = kr.get("score")
|
||
if explicit_score is not None:
|
||
return max(0.0, min(1.0, float(explicit_score)))
|
||
return milestones_hit / milestones_total if milestones_total > 0 else 0.0
|
||
|
||
elif kr_type == "percentage":
|
||
target = kr.get("target_pct", 100)
|
||
current = kr.get("current_pct", 0)
|
||
baseline = kr.get("baseline_pct", 0)
|
||
if target == baseline:
|
||
return 0.0
|
||
score = (current - baseline) / (target - baseline)
|
||
return max(0.0, min(1.0, score))
|
||
|
||
else: # numeric (default)
|
||
target = kr.get("target_value", 0)
|
||
current = kr.get("current_value", 0)
|
||
baseline = kr.get("baseline_value", 0)
|
||
if target == baseline:
|
||
return 0.0
|
||
# Handle "lower is better" metrics (e.g., churn, response time)
|
||
if kr.get("lower_is_better", False):
|
||
if current <= target:
|
||
return 1.0
|
||
improvement = baseline - current
|
||
needed = baseline - target
|
||
score = improvement / needed if needed != 0 else 0.0
|
||
else:
|
||
score = (current - baseline) / (target - baseline)
|
||
return max(0.0, min(1.0, score))
|
||
|
||
|
||
def get_kr_status(score: float, quarter_progress: float, kr: dict) -> str:
|
||
"""
|
||
Determine KR status based on score, time elapsed in quarter, and trend.
|
||
|
||
A KR is at-risk if its score is significantly behind the time elapsed.
|
||
E.g., if we're 70% through the quarter but KR is at 30%, it's at risk.
|
||
"""
|
||
if kr.get("done", False):
|
||
return "complete"
|
||
|
||
# Not started
|
||
if score == 0.0 and quarter_progress < 0.1:
|
||
return "not_started"
|
||
|
||
# Check against absolute thresholds
|
||
if score >= SCORE_THRESHOLDS["on_track"]:
|
||
return "on_track"
|
||
|
||
# Adjust for time: if we're early in quarter, lower scores are acceptable
|
||
adjusted_threshold = SCORE_THRESHOLDS["at_risk"] * (quarter_progress or 0.5)
|
||
|
||
if score >= max(adjusted_threshold, SCORE_THRESHOLDS["at_risk"]):
|
||
return "at_risk"
|
||
|
||
return "off_track"
|
||
|
||
|
||
def calculate_objective_score(objective: dict, quarter_progress: float) -> dict:
|
||
"""
|
||
Score an objective based on its key results.
|
||
Returns scored objective with KR scores and status.
|
||
"""
|
||
key_results = objective.get("key_results", [])
|
||
if not key_results:
|
||
return {**objective, "score": 0.0, "status": "not_started", "key_results_scored": []}
|
||
|
||
scored_krs = []
|
||
for kr in key_results:
|
||
score = calculate_kr_score(kr)
|
||
status = get_kr_status(score, quarter_progress, kr)
|
||
|
||
# Calculate time-adjusted gap
|
||
expected_score = quarter_progress * 0.85 # Expect 85% of time-proportional progress
|
||
gap = expected_score - score
|
||
|
||
risk_level = _assess_kr_risk(score, status, gap, quarter_progress, kr)
|
||
|
||
scored_krs.append({
|
||
**kr,
|
||
"score": round(score, 3),
|
||
"score_pct": f"{score * 100:.0f}%",
|
||
"status": status,
|
||
"status_label": STATUS_LABELS.get(status, status),
|
||
"expected_score": round(expected_score, 3),
|
||
"gap_vs_expected": round(gap, 3),
|
||
"risk_level": risk_level,
|
||
"risk_label": RISK_LABELS.get(risk_level, risk_level),
|
||
})
|
||
|
||
# Objective score = weighted average of KR scores
|
||
# Weight is explicit in KR data or defaults to equal weight
|
||
total_weight = sum(kr.get("weight", 1.0) for kr in key_results)
|
||
weighted_score = sum(
|
||
kr_scored["score"] * kr.get("weight", 1.0)
|
||
for kr_scored, kr in zip(scored_krs, key_results)
|
||
)
|
||
obj_score = weighted_score / total_weight if total_weight > 0 else 0.0
|
||
|
||
# Objective status = worst KR status (a chain is only as strong as weakest link)
|
||
status_priority = {"off_track": 0, "at_risk": 1, "not_started": 2, "on_track": 3, "complete": 4}
|
||
obj_status = min(scored_krs, key=lambda x: status_priority.get(x["status"], 2))["status"]
|
||
|
||
return {
|
||
**objective,
|
||
"score": round(obj_score, 3),
|
||
"score_pct": f"{obj_score * 100:.0f}%",
|
||
"status": obj_status,
|
||
"status_label": STATUS_LABELS.get(obj_status, obj_status),
|
||
"key_results_scored": scored_krs,
|
||
}
|
||
|
||
|
||
def _assess_kr_risk(
|
||
score: float,
|
||
status: str,
|
||
gap: float,
|
||
quarter_progress: float,
|
||
kr: dict,
|
||
) -> str:
|
||
"""Assess risk level for a key result."""
|
||
if status == "complete" or status == "on_track":
|
||
return "low"
|
||
|
||
weeks_remaining = kr.get("weeks_remaining", max(1, int((1 - quarter_progress) * 13)))
|
||
|
||
# Critical: off track with <4 weeks left
|
||
if status == "off_track" and weeks_remaining <= 4:
|
||
return "critical"
|
||
|
||
# High: significantly behind with limited time
|
||
if gap > 0.3 and weeks_remaining <= 6:
|
||
return "high"
|
||
|
||
# High: off track regardless of time
|
||
if status == "off_track":
|
||
return "high"
|
||
|
||
# Medium: at risk
|
||
if status == "at_risk":
|
||
return "medium"
|
||
|
||
return "low"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# OKR Cascade and Alignment Analysis
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def build_okr_tree(data: dict, quarter_progress: float) -> dict:
|
||
"""
|
||
Build scored OKR tree: company → departments → teams.
|
||
Returns full hierarchy with scores at every level.
|
||
"""
|
||
company = data.get("company_okrs", {})
|
||
departments = data.get("department_okrs", [])
|
||
teams = data.get("team_okrs", [])
|
||
|
||
# Score company-level OKRs
|
||
company_scored = {
|
||
"name": company.get("name", "Company"),
|
||
"quarter": company.get("quarter", ""),
|
||
"objectives": [
|
||
calculate_objective_score(obj, quarter_progress)
|
||
for obj in company.get("objectives", [])
|
||
],
|
||
}
|
||
|
||
# Score department-level OKRs
|
||
depts_scored = []
|
||
for dept in departments:
|
||
dept_objectives = [
|
||
calculate_objective_score(obj, quarter_progress)
|
||
for obj in dept.get("objectives", [])
|
||
]
|
||
dept_score = (
|
||
sum(o["score"] for o in dept_objectives) / len(dept_objectives)
|
||
if dept_objectives else 0.0
|
||
)
|
||
depts_scored.append({
|
||
**dept,
|
||
"objectives": dept_objectives,
|
||
"overall_score": round(dept_score, 3),
|
||
"overall_score_pct": f"{dept_score * 100:.0f}%",
|
||
})
|
||
|
||
# Score team-level OKRs
|
||
teams_scored = []
|
||
for team in teams:
|
||
team_objectives = [
|
||
calculate_objective_score(obj, quarter_progress)
|
||
for obj in team.get("objectives", [])
|
||
]
|
||
team_score = (
|
||
sum(o["score"] for o in team_objectives) / len(team_objectives)
|
||
if team_objectives else 0.0
|
||
)
|
||
teams_scored.append({
|
||
**team,
|
||
"objectives": team_objectives,
|
||
"overall_score": round(team_score, 3),
|
||
"overall_score_pct": f"{team_score * 100:.0f}%",
|
||
})
|
||
|
||
return {
|
||
"company": company_scored,
|
||
"departments": depts_scored,
|
||
"teams": teams_scored,
|
||
}
|
||
|
||
|
||
def analyze_alignment(okr_tree: dict) -> dict:
|
||
"""
|
||
Analyze how team and department OKRs align to company OKRs.
|
||
Flags: orphaned OKRs (no company parent), missing coverage (company OKR with no team support).
|
||
"""
|
||
company_objective_ids = {
|
||
obj.get("id") for obj in okr_tree["company"].get("objectives", [])
|
||
if obj.get("id")
|
||
}
|
||
|
||
# Collect all alignment references from dept and team OKRs
|
||
alignment_map: dict[str, list[str]] = {oid: [] for oid in company_objective_ids}
|
||
orphaned = []
|
||
all_supporting = []
|
||
|
||
def check_objectives(objectives: list, owner_name: str, level: str):
|
||
for obj in objectives:
|
||
supports = obj.get("supports_company_objective_ids", [])
|
||
if not supports:
|
||
# Check if it's supposed to support something
|
||
if obj.get("supports_company_objective_id"):
|
||
supports = [obj["supports_company_objective_id"]]
|
||
|
||
if not supports:
|
||
orphaned.append({
|
||
"level": level,
|
||
"owner": owner_name,
|
||
"objective": obj.get("title", obj.get("name", "Unknown")),
|
||
"issue": "No link to company objective — may be misaligned or low priority",
|
||
})
|
||
else:
|
||
for cid in supports:
|
||
if cid in alignment_map:
|
||
alignment_map[cid].append(f"{level}:{owner_name}")
|
||
all_supporting.append(cid)
|
||
else:
|
||
orphaned.append({
|
||
"level": level,
|
||
"owner": owner_name,
|
||
"objective": obj.get("title", obj.get("name", "Unknown")),
|
||
"issue": f"References company objective '{cid}' which doesn't exist",
|
||
})
|
||
|
||
for dept in okr_tree["departments"]:
|
||
check_objectives(dept["objectives"], dept.get("name", "Unknown Dept"), "Department")
|
||
|
||
for team in okr_tree["teams"]:
|
||
check_objectives(team["objectives"], team.get("name", "Unknown Team"), "Team")
|
||
|
||
# Find company objectives with no support from below
|
||
unsupported = []
|
||
for obj in okr_tree["company"].get("objectives", []):
|
||
obj_id = obj.get("id")
|
||
if obj_id and obj_id not in all_supporting:
|
||
unsupported.append({
|
||
"objective_id": obj_id,
|
||
"objective": obj.get("title", obj.get("name", "Unknown")),
|
||
"issue": "No department or team OKR explicitly supports this company objective",
|
||
})
|
||
|
||
coverage_score = (
|
||
len(set(all_supporting)) / len(company_objective_ids) * 100
|
||
if company_objective_ids else 100
|
||
)
|
||
|
||
return {
|
||
"alignment_map": alignment_map,
|
||
"orphaned_okrs": orphaned,
|
||
"unsupported_company_objectives": unsupported,
|
||
"coverage_score_pct": round(coverage_score, 1),
|
||
}
|
||
|
||
|
||
def collect_at_risk_krs(okr_tree: dict) -> list[dict]:
|
||
"""Collect all at-risk and off-track key results across the full OKR tree."""
|
||
at_risk = []
|
||
|
||
def scan_objectives(objectives: list, owner: str, level: str):
|
||
for obj in objectives:
|
||
for kr in obj.get("key_results_scored", []):
|
||
if kr["status"] in ("at_risk", "off_track"):
|
||
at_risk.append({
|
||
"level": level,
|
||
"owner": owner,
|
||
"objective": obj.get("title", obj.get("name", "Unknown")),
|
||
"key_result": kr.get("title", kr.get("name", "Unknown")),
|
||
"score": kr["score"],
|
||
"score_pct": kr["score_pct"],
|
||
"status": kr["status"],
|
||
"status_label": kr["status_label"],
|
||
"risk_level": kr["risk_level"],
|
||
"risk_label": kr["risk_label"],
|
||
"gap_vs_expected": kr["gap_vs_expected"],
|
||
"notes": kr.get("notes", ""),
|
||
})
|
||
|
||
scan_objectives(
|
||
okr_tree["company"].get("objectives", []),
|
||
okr_tree["company"].get("name", "Company"),
|
||
"Company",
|
||
)
|
||
for dept in okr_tree["departments"]:
|
||
scan_objectives(dept["objectives"], dept.get("name", ""), "Department")
|
||
for team in okr_tree["teams"]:
|
||
scan_objectives(team["objectives"], team.get("name", ""), "Team")
|
||
|
||
# Sort: off_track before at_risk, then by gap
|
||
status_order = {"off_track": 0, "at_risk": 1}
|
||
at_risk.sort(key=lambda x: (status_order.get(x["status"], 2), -x.get("gap_vs_expected", 0)))
|
||
|
||
return at_risk
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Report Formatter
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _score_bar(score: float, width: int = 20) -> str:
|
||
"""Render a text progress bar for a 0.0–1.0 score."""
|
||
filled = round(score * width)
|
||
bar = "█" * filled + "░" * (width - filled)
|
||
return f"[{bar}] {score * 100:.0f}%"
|
||
|
||
|
||
def format_report(
|
||
okr_tree: dict,
|
||
alignment: dict,
|
||
at_risk_krs: list[dict],
|
||
quarter_progress: float,
|
||
quarter_label: str,
|
||
) -> str:
|
||
"""Format full OKR tracking report as plain text."""
|
||
lines = []
|
||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||
company_name = okr_tree["company"].get("name", "Company")
|
||
|
||
lines.append("=" * 70)
|
||
lines.append(f"OKR TRACKING REPORT — {company_name}")
|
||
lines.append(f"Quarter: {quarter_label} | Quarter progress: {quarter_progress * 100:.0f}%")
|
||
lines.append(f"Generated: {now}")
|
||
lines.append("=" * 70)
|
||
|
||
# --- Executive Summary ---
|
||
lines.append("\n📊 EXECUTIVE SUMMARY")
|
||
lines.append("-" * 40)
|
||
|
||
company_objectives = okr_tree["company"].get("objectives", [])
|
||
if company_objectives:
|
||
company_avg = sum(o["score"] for o in company_objectives) / len(company_objectives)
|
||
on_track = sum(1 for o in company_objectives if o["status"] == "on_track")
|
||
at_risk = sum(1 for o in company_objectives if o["status"] == "at_risk")
|
||
off_track = sum(1 for o in company_objectives if o["status"] == "off_track")
|
||
|
||
lines.append(f"Company OKR Score: {_score_bar(company_avg)}")
|
||
lines.append(f"Objectives: {len(company_objectives)} total — "
|
||
f"🟢 {on_track} on track, 🟡 {at_risk} at risk, 🔴 {off_track} off track")
|
||
lines.append(f"At-risk KRs (all): {len(at_risk_krs)}")
|
||
lines.append(f"Alignment coverage: {alignment['coverage_score_pct']}% of company objectives have team support")
|
||
|
||
# Overall health assessment
|
||
if company_avg >= 0.7:
|
||
health = "🟢 HEALTHY — On track for a strong quarter"
|
||
elif company_avg >= 0.5:
|
||
health = "🟡 CAUTION — Some objectives need attention"
|
||
elif company_avg >= 0.3:
|
||
health = "🔴 AT RISK — Multiple objectives behind; intervention needed"
|
||
else:
|
||
health = "🚨 CRITICAL — Quarter in serious jeopardy; executive review required"
|
||
lines.append(f"\nOverall Health: {health}")
|
||
|
||
# --- Company OKRs ---
|
||
lines.append("\n\n🏢 COMPANY OKRs")
|
||
lines.append("-" * 40)
|
||
|
||
for obj in company_objectives:
|
||
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
|
||
lines.append(f" Owner: {obj.get('owner', 'Unassigned')} | Score: {_score_bar(obj['score'], 15)} {obj['status_label']}")
|
||
|
||
for kr in obj.get("key_results_scored", []):
|
||
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
|
||
lines.append(f"\n KR: {kr.get('title', kr.get('name', 'Unknown'))}")
|
||
lines.append(f" Score: {_score_bar(kr['score'], 12)} {kr['status_label']}{risk_marker}")
|
||
|
||
# Show actual progress
|
||
if kr.get("type") == "numeric":
|
||
current = kr.get("current_value", "?")
|
||
target = kr.get("target_value", "?")
|
||
baseline = kr.get("baseline_value", 0)
|
||
unit = kr.get("unit", "")
|
||
lines.append(f" Progress: {current}{unit} / {target}{unit} (baseline: {baseline}{unit})")
|
||
elif kr.get("type") == "percentage":
|
||
lines.append(f" Progress: {kr.get('current_pct', '?')}% / {kr.get('target_pct', '?')}%")
|
||
elif kr.get("type") == "milestone":
|
||
hit = kr.get("milestones_hit", "?")
|
||
total = kr.get("milestones_total", "?")
|
||
lines.append(f" Milestones: {hit} / {total}")
|
||
|
||
if kr.get("notes"):
|
||
lines.append(f" Note: {kr['notes']}")
|
||
|
||
# --- Department OKRs ---
|
||
lines.append("\n\n🏬 DEPARTMENT OKRs")
|
||
lines.append("-" * 40)
|
||
|
||
for dept in okr_tree["departments"]:
|
||
lines.append(f"\n 📁 {dept.get('name', 'Unknown')} | Score: {_score_bar(dept['overall_score'], 15)}")
|
||
|
||
for obj in dept.get("objectives", []):
|
||
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
|
||
lines.append(f" Owner: {obj.get('owner', 'Unassigned')} | {obj['status_label']}")
|
||
supports = obj.get("supports_company_objective_ids", [])
|
||
if supports:
|
||
lines.append(f" Supports: Company Objective(s) {', '.join(supports)}")
|
||
|
||
for kr in obj.get("key_results_scored", []):
|
||
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
|
||
lines.append(f"\n KR: {kr.get('title', kr.get('name', 'Unknown'))}")
|
||
lines.append(f" {_score_bar(kr['score'], 10)} {kr['status_label']}{risk_marker}")
|
||
|
||
# --- Team OKRs ---
|
||
if okr_tree["teams"]:
|
||
lines.append("\n\n👥 TEAM OKRs")
|
||
lines.append("-" * 40)
|
||
|
||
for team in okr_tree["teams"]:
|
||
lines.append(f"\n 📋 {team.get('name', 'Unknown')} | Score: {_score_bar(team['overall_score'], 15)}")
|
||
|
||
for obj in team.get("objectives", []):
|
||
lines.append(f"\n Objective: {obj.get('title', obj.get('name', 'Unknown'))}")
|
||
supports = obj.get("supports_company_objective_ids", [])
|
||
if supports:
|
||
lines.append(f" Supports: {', '.join(supports)}")
|
||
|
||
for kr in obj.get("key_results_scored", []):
|
||
risk_marker = f" {kr['risk_label']}" if kr["risk_level"] in ("critical", "high") else ""
|
||
lines.append(
|
||
f" • {kr.get('title', kr.get('name', 'Unknown'))}: "
|
||
f"{kr['score_pct']} {kr['status_label']}{risk_marker}"
|
||
)
|
||
|
||
# --- At-Risk KRs ---
|
||
lines.append("\n\n⚠️ AT-RISK KEY RESULTS (Action Required)")
|
||
lines.append("-" * 40)
|
||
|
||
if not at_risk_krs:
|
||
lines.append("✅ No key results currently at risk or off track.")
|
||
else:
|
||
critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
|
||
high = [kr for kr in at_risk_krs if kr["risk_level"] == "high"]
|
||
medium = [kr for kr in at_risk_krs if kr["risk_level"] == "medium"]
|
||
|
||
for group_label, group in [("🔴 CRITICAL", critical), ("🟠 HIGH", high), ("🟡 MEDIUM", medium)]:
|
||
if not group:
|
||
continue
|
||
lines.append(f"\n{group_label} ({len(group)} items):")
|
||
for kr in group:
|
||
lines.append(f"\n [{kr['level']}] {kr['owner']}")
|
||
lines.append(f" Obj: {kr['objective']}")
|
||
lines.append(f" KR: {kr['key_result']}")
|
||
lines.append(f" Score: {kr['score_pct']} {kr['status_label']} (gap vs expected: {kr['gap_vs_expected'] * 100:.0f}pp)")
|
||
if kr["notes"]:
|
||
lines.append(f" Note: {kr['notes']}")
|
||
|
||
# --- Alignment Report ---
|
||
lines.append("\n\n🔗 ALIGNMENT REPORT")
|
||
lines.append("-" * 40)
|
||
lines.append(f"Alignment coverage: {alignment['coverage_score_pct']}% of company objectives have explicit support\n")
|
||
|
||
# Show alignment map
|
||
lines.append("Company Objective Coverage:")
|
||
for obj in company_objectives:
|
||
obj_id = obj.get("id", "")
|
||
supporters = alignment["alignment_map"].get(obj_id, [])
|
||
obj_name = obj.get("title", obj.get("name", obj_id))
|
||
count = len(supporters)
|
||
marker = "✅" if count > 0 else "⚠️ "
|
||
lines.append(f" {marker} [{obj_id}] {obj_name}")
|
||
if supporters:
|
||
for s in supporters:
|
||
lines.append(f" ↑ {s}")
|
||
else:
|
||
lines.append(f" ↑ (no department or team OKR supports this)")
|
||
|
||
if alignment["unsupported_company_objectives"]:
|
||
lines.append(f"\n⚠️ Unsupported Company Objectives ({len(alignment['unsupported_company_objectives'])}):")
|
||
for u in alignment["unsupported_company_objectives"]:
|
||
lines.append(f" • [{u['objective_id']}] {u['objective']}")
|
||
lines.append(f" → {u['issue']}")
|
||
|
||
if alignment["orphaned_okrs"]:
|
||
lines.append(f"\n⚠️ Orphaned OKRs (not linked to company objectives):")
|
||
for o in alignment["orphaned_okrs"]:
|
||
lines.append(f" • [{o['level']}] {o['owner']}: {o['objective']}")
|
||
lines.append(f" → {o['issue']}")
|
||
|
||
# --- Recommendations ---
|
||
lines.append("\n\n📋 RECOMMENDED ACTIONS")
|
||
lines.append("-" * 40)
|
||
|
||
recs = _generate_recommendations(okr_tree, at_risk_krs, alignment, quarter_progress)
|
||
for i, rec in enumerate(recs, 1):
|
||
lines.append(f"\n{i}. {rec['title']}")
|
||
lines.append(f" {rec['detail']}")
|
||
lines.append(f" Owner: {rec['owner']} | When: {rec['when']}")
|
||
|
||
lines.append("\n" + "=" * 70)
|
||
lines.append("END OF REPORT")
|
||
lines.append("=" * 70)
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _generate_recommendations(
|
||
okr_tree: dict,
|
||
at_risk_krs: list[dict],
|
||
alignment: dict,
|
||
quarter_progress: float,
|
||
) -> list[dict]:
|
||
"""Generate actionable recommendations based on OKR analysis."""
|
||
recs = []
|
||
|
||
# Critical KRs
|
||
critical = [kr for kr in at_risk_krs if kr["risk_level"] == "critical"]
|
||
if critical:
|
||
recs.append({
|
||
"title": f"Emergency review: {len(critical)} critical key result(s) need immediate intervention",
|
||
"detail": f"Critical KRs: {', '.join(kr['key_result'] for kr in critical[:3])}. "
|
||
f"With limited time remaining, these need escalation today.",
|
||
"owner": "COO + KR owners",
|
||
"when": "This week",
|
||
})
|
||
|
||
# Off-track objectives
|
||
off_track_objs = [
|
||
o for o in okr_tree["company"].get("objectives", [])
|
||
if o["status"] == "off_track"
|
||
]
|
||
if off_track_objs:
|
||
recs.append({
|
||
"title": f"Scope reset for {len(off_track_objs)} off-track company objective(s)",
|
||
"detail": "When a company objective is off track by mid-quarter, "
|
||
"the options are: (1) resource surge, (2) scope reduction, or (3) accept the miss. "
|
||
"Choose explicitly — don't let it drift.",
|
||
"owner": "CEO + COO",
|
||
"when": "Within 1 week",
|
||
})
|
||
|
||
# Alignment gaps
|
||
if alignment["coverage_score_pct"] < 80:
|
||
recs.append({
|
||
"title": "OKR alignment gap — not all company objectives have team support",
|
||
"detail": f"Only {alignment['coverage_score_pct']}% of company objectives have explicit team/dept OKRs supporting them. "
|
||
"Either add supporting OKRs or acknowledge these objectives are founder-owned.",
|
||
"owner": "COO + VPs",
|
||
"when": "Next OKR planning cycle",
|
||
})
|
||
|
||
if alignment["orphaned_okrs"]:
|
||
recs.append({
|
||
"title": f"{len(alignment['orphaned_okrs'])} orphaned OKR(s) with no company objective linkage",
|
||
"detail": "Team OKRs that don't connect to company objectives waste capacity. "
|
||
"Either link them explicitly or discontinue them.",
|
||
"owner": "Team leads + COO",
|
||
"when": "OKR review session",
|
||
})
|
||
|
||
# Late quarter: force ranking
|
||
if quarter_progress >= 0.67:
|
||
at_risk_count = sum(
|
||
1 for o in okr_tree["company"].get("objectives", [])
|
||
if o["status"] in ("at_risk", "off_track")
|
||
)
|
||
if at_risk_count > 0:
|
||
recs.append({
|
||
"title": f"Late quarter: force-rank which at-risk OKRs to save vs. accept as miss",
|
||
"detail": f"{at_risk_count} objectives at risk with <{int((1 - quarter_progress) * 13)} weeks left. "
|
||
"You cannot save everything. Pick the 1–2 most important and resource them fully. "
|
||
"Explicitly accept the others as misses and learn from them.",
|
||
"owner": "CEO + COO",
|
||
"when": "Immediately",
|
||
})
|
||
|
||
# Measurement gaps
|
||
unscored_krs = []
|
||
for obj in okr_tree["company"].get("objectives", []):
|
||
for kr in obj.get("key_results_scored", []):
|
||
if kr["score"] == 0.0 and kr["status"] == "not_started" and quarter_progress > 0.25:
|
||
unscored_krs.append(kr.get("title", kr.get("name", "Unknown")))
|
||
|
||
if unscored_krs:
|
||
recs.append({
|
||
"title": f"{len(unscored_krs)} key result(s) show no progress past Q1",
|
||
"detail": "KRs with zero progress after 25% of quarter has elapsed are either not started, "
|
||
"unmeasured, or forgotten. Require owners to update scores this week.",
|
||
"owner": "KR owners",
|
||
"when": "This week — before next leadership sync",
|
||
})
|
||
|
||
return recs
|
||
|
||
|
||
def format_json_output(okr_tree: dict, alignment: dict, at_risk_krs: list[dict]) -> str:
|
||
"""Format analysis as machine-readable JSON."""
|
||
return json.dumps(
|
||
{
|
||
"generated_at": datetime.now().isoformat(),
|
||
"company_score": (
|
||
sum(o["score"] for o in okr_tree["company"].get("objectives", []))
|
||
/ max(1, len(okr_tree["company"].get("objectives", [])))
|
||
),
|
||
"at_risk_count": len(at_risk_krs),
|
||
"alignment_coverage_pct": alignment["coverage_score_pct"],
|
||
"objectives": okr_tree["company"].get("objectives", []),
|
||
"departments": okr_tree["departments"],
|
||
"teams": okr_tree["teams"],
|
||
"at_risk_key_results": at_risk_krs,
|
||
"alignment": alignment,
|
||
},
|
||
indent=2,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Main Entrypoint
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="OKR Cascade and Alignment Tracker — COO Advisor Tool",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog=__doc__,
|
||
)
|
||
parser.add_argument("--input", "-i", help="Path to JSON OKR data file", default=None)
|
||
parser.add_argument("--output", "-o", help="Path to write report (default: stdout)", default=None)
|
||
parser.add_argument(
|
||
"--format", "-f",
|
||
choices=["text", "json"],
|
||
default="text",
|
||
help="Output format: text (default) or json",
|
||
)
|
||
parser.add_argument(
|
||
"--quarter-progress",
|
||
type=float,
|
||
default=None,
|
||
help="Override quarter progress (0.0–1.0). Default: auto-calculated from quarter dates.",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
if args.input:
|
||
try:
|
||
with open(args.input, "r") as f:
|
||
data = json.load(f)
|
||
except FileNotFoundError:
|
||
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
|
||
sys.exit(1)
|
||
except json.JSONDecodeError as e:
|
||
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
|
||
sys.exit(1)
|
||
else:
|
||
print("No input file specified — running with sample data.\n")
|
||
data = SAMPLE_DATA
|
||
|
||
# Determine quarter progress
|
||
if args.quarter_progress is not None:
|
||
quarter_progress = args.quarter_progress
|
||
else:
|
||
quarter_progress = _calculate_quarter_progress(data)
|
||
|
||
quarter_label = data.get("company_okrs", {}).get("quarter", "Unknown Quarter")
|
||
|
||
# Run analysis
|
||
okr_tree = build_okr_tree(data, quarter_progress)
|
||
alignment = analyze_alignment(okr_tree)
|
||
at_risk_krs = collect_at_risk_krs(okr_tree)
|
||
|
||
# Format output
|
||
if args.format == "json":
|
||
output = format_json_output(okr_tree, alignment, at_risk_krs)
|
||
else:
|
||
output = format_report(okr_tree, alignment, at_risk_krs, quarter_progress, quarter_label)
|
||
|
||
if args.output:
|
||
with open(args.output, "w") as f:
|
||
f.write(output)
|
||
print(f"Report written to: {args.output}")
|
||
else:
|
||
print(output)
|
||
|
||
|
||
def _calculate_quarter_progress(data: dict) -> float:
|
||
"""Auto-calculate quarter progress from start/end dates in data, or default to 0.5."""
|
||
q = data.get("company_okrs", {})
|
||
start_str = q.get("quarter_start")
|
||
end_str = q.get("quarter_end")
|
||
|
||
if not start_str or not end_str:
|
||
return 0.5 # Default to mid-quarter if not specified
|
||
|
||
try:
|
||
start = date.fromisoformat(start_str)
|
||
end = date.fromisoformat(end_str)
|
||
today = date.today()
|
||
total_days = (end - start).days
|
||
elapsed_days = (today - start).days
|
||
progress = elapsed_days / total_days if total_days > 0 else 0.5
|
||
return max(0.0, min(1.0, progress))
|
||
except (ValueError, TypeError):
|
||
return 0.5
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Sample Data
|
||
# ---------------------------------------------------------------------------
|
||
|
||
SAMPLE_DATA = {
|
||
"company_okrs": {
|
||
"name": "AcmeSaaS",
|
||
"quarter": "Q1 2025",
|
||
"quarter_start": "2025-01-01",
|
||
"quarter_end": "2025-03-31",
|
||
"objectives": [
|
||
{
|
||
"id": "CO1",
|
||
"title": "Achieve breakout revenue growth",
|
||
"owner": "CEO",
|
||
"key_results": [
|
||
{
|
||
"id": "CO1-KR1",
|
||
"title": "Reach $5M net new ARR",
|
||
"type": "numeric",
|
||
"baseline_value": 0,
|
||
"current_value": 2800000,
|
||
"target_value": 5000000,
|
||
"unit": "",
|
||
"notes": "Strong January, February softer; pipeline looks better for March",
|
||
},
|
||
{
|
||
"id": "CO1-KR2",
|
||
"title": "Achieve 115% NRR",
|
||
"type": "percentage",
|
||
"baseline_pct": 108,
|
||
"current_pct": 110,
|
||
"target_pct": 115,
|
||
"notes": "Expansion motion improved; churn still elevated in SMB segment",
|
||
},
|
||
{
|
||
"id": "CO1-KR3",
|
||
"title": "Close 3 enterprise deals (>$150K ACV)",
|
||
"type": "numeric",
|
||
"baseline_value": 0,
|
||
"current_value": 1,
|
||
"target_value": 3,
|
||
"unit": " deals",
|
||
"notes": "1 closed, 2 in late-stage negotiation",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
"id": "CO2",
|
||
"title": "Build a world-class product that customers love",
|
||
"owner": "CPO",
|
||
"key_results": [
|
||
{
|
||
"id": "CO2-KR1",
|
||
"title": "Increase feature adoption rate to 65% (% of customers using 3+ core features)",
|
||
"type": "percentage",
|
||
"baseline_pct": 48,
|
||
"current_pct": 52,
|
||
"target_pct": 65,
|
||
"notes": "Onboarding improvements shipped; adoption curve is moving",
|
||
},
|
||
{
|
||
"id": "CO2-KR2",
|
||
"title": "Ship the integration platform (milestone)",
|
||
"type": "milestone",
|
||
"milestones_total": 4,
|
||
"milestones_hit": 1,
|
||
"milestones": [
|
||
"API design complete",
|
||
"Internal alpha",
|
||
"Beta with 5 customers",
|
||
"GA launch",
|
||
],
|
||
"notes": "API design shipped. Internal alpha delayed 2 weeks.",
|
||
},
|
||
{
|
||
"id": "CO2-KR3",
|
||
"title": "NPS score reaches 45",
|
||
"type": "numeric",
|
||
"baseline_value": 32,
|
||
"current_value": 38,
|
||
"target_value": 45,
|
||
"unit": "",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
"id": "CO3",
|
||
"title": "Build an operationally excellent company",
|
||
"owner": "COO",
|
||
"key_results": [
|
||
{
|
||
"id": "CO3-KR1",
|
||
"title": "Reduce burn multiple from 1.8x to 1.3x",
|
||
"type": "numeric",
|
||
"baseline_value": 1.8,
|
||
"current_value": 1.65,
|
||
"target_value": 1.3,
|
||
"lower_is_better": True,
|
||
"unit": "x",
|
||
},
|
||
{
|
||
"id": "CO3-KR2",
|
||
"title": "Achieve <30-day customer onboarding (avg)",
|
||
"type": "numeric",
|
||
"baseline_value": 47,
|
||
"current_value": 38,
|
||
"target_value": 30,
|
||
"lower_is_better": True,
|
||
"unit": " days",
|
||
"notes": "Good progress; blocked by technical setup step (avg 12 days)",
|
||
},
|
||
{
|
||
"id": "CO3-KR3",
|
||
"title": "Voluntary attrition <10%",
|
||
"type": "numeric",
|
||
"baseline_value": 15,
|
||
"current_value": 12,
|
||
"target_value": 10,
|
||
"lower_is_better": True,
|
||
"unit": "%",
|
||
"notes": "2 unexpected departures in January; retention initiatives launched",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
"department_okrs": [
|
||
{
|
||
"name": "Sales",
|
||
"owner": "VP Sales",
|
||
"objectives": [
|
||
{
|
||
"title": "Drive net new ARR to hit company growth target",
|
||
"owner": "VP Sales",
|
||
"supports_company_objective_ids": ["CO1"],
|
||
"key_results": [
|
||
{
|
||
"title": "Close $4M in new business ARR",
|
||
"type": "numeric",
|
||
"baseline_value": 0,
|
||
"current_value": 2200000,
|
||
"target_value": 4000000,
|
||
"unit": "",
|
||
},
|
||
{
|
||
"title": "Maintain pipeline coverage ratio ≥3x",
|
||
"type": "numeric",
|
||
"baseline_value": 2.5,
|
||
"current_value": 3.1,
|
||
"target_value": 3.0,
|
||
"unit": "x",
|
||
},
|
||
{
|
||
"title": "Reduce average sales cycle to 42 days",
|
||
"type": "numeric",
|
||
"baseline_value": 58,
|
||
"current_value": 50,
|
||
"target_value": 42,
|
||
"lower_is_better": True,
|
||
"unit": " days",
|
||
},
|
||
],
|
||
}
|
||
],
|
||
},
|
||
{
|
||
"name": "Engineering",
|
||
"owner": "VP Engineering",
|
||
"objectives": [
|
||
{
|
||
"title": "Deliver the integration platform on schedule",
|
||
"owner": "VP Engineering",
|
||
"supports_company_objective_ids": ["CO2"],
|
||
"key_results": [
|
||
{
|
||
"title": "Integration platform beta live with 5 customers",
|
||
"type": "milestone",
|
||
"milestones_total": 3,
|
||
"milestones_hit": 1,
|
||
"notes": "Alpha delayed — dependency on API gateway refactor",
|
||
},
|
||
{
|
||
"title": "Deploy frequency ≥10/week",
|
||
"type": "numeric",
|
||
"baseline_value": 6,
|
||
"current_value": 9,
|
||
"target_value": 10,
|
||
"unit": "/week",
|
||
},
|
||
{
|
||
"title": "P0/P1 incidents <2 per month",
|
||
"type": "numeric",
|
||
"baseline_value": 5,
|
||
"current_value": 2.5,
|
||
"target_value": 2,
|
||
"lower_is_better": True,
|
||
"unit": "/month",
|
||
},
|
||
],
|
||
}
|
||
],
|
||
},
|
||
{
|
||
"name": "Customer Success",
|
||
"owner": "VP CS",
|
||
"objectives": [
|
||
{
|
||
"title": "Drive retention and expansion to fuel NRR growth",
|
||
"owner": "VP CS",
|
||
"supports_company_objective_ids": ["CO1", "CO2"],
|
||
"key_results": [
|
||
{
|
||
"title": "Gross retention ≥92%",
|
||
"type": "percentage",
|
||
"baseline_pct": 88,
|
||
"current_pct": 89,
|
||
"target_pct": 92,
|
||
"notes": "3 at-risk accounts in red status",
|
||
},
|
||
{
|
||
"title": "Average onboarding time ≤30 days",
|
||
"type": "numeric",
|
||
"baseline_value": 47,
|
||
"current_value": 38,
|
||
"target_value": 30,
|
||
"lower_is_better": True,
|
||
"unit": " days",
|
||
},
|
||
{
|
||
"title": "Expansion ARR from existing customers: $800K",
|
||
"type": "numeric",
|
||
"baseline_value": 0,
|
||
"current_value": 580000,
|
||
"target_value": 800000,
|
||
"unit": "",
|
||
},
|
||
],
|
||
}
|
||
],
|
||
},
|
||
],
|
||
"team_okrs": [
|
||
{
|
||
"name": "Platform Engineering",
|
||
"department": "Engineering",
|
||
"objectives": [
|
||
{
|
||
"title": "Build the integration API infrastructure",
|
||
"supports_company_objective_ids": ["CO2"],
|
||
"key_results": [
|
||
{
|
||
"title": "API gateway v2 deployed to production",
|
||
"type": "boolean",
|
||
"done": False,
|
||
"notes": "Targeting end of week 8",
|
||
},
|
||
{
|
||
"title": "Webhook system handles 10K events/sec",
|
||
"type": "boolean",
|
||
"done": False,
|
||
},
|
||
{
|
||
"title": "P99 API latency <200ms",
|
||
"type": "numeric",
|
||
"baseline_value": 380,
|
||
"current_value": 290,
|
||
"target_value": 200,
|
||
"lower_is_better": True,
|
||
"unit": "ms",
|
||
},
|
||
],
|
||
}
|
||
],
|
||
},
|
||
{
|
||
"name": "Enterprise Sales Team",
|
||
"department": "Sales",
|
||
"objectives": [
|
||
{
|
||
"title": "Land 3 enterprise accounts",
|
||
"supports_company_objective_ids": ["CO1"],
|
||
"key_results": [
|
||
{
|
||
"title": "3 enterprise deals closed",
|
||
"type": "numeric",
|
||
"baseline_value": 0,
|
||
"current_value": 1,
|
||
"target_value": 3,
|
||
"unit": " deals",
|
||
},
|
||
{
|
||
"title": "5 enterprise POCs initiated",
|
||
"type": "numeric",
|
||
"baseline_value": 0,
|
||
"current_value": 4,
|
||
"target_value": 5,
|
||
"unit": " POCs",
|
||
},
|
||
],
|
||
}
|
||
],
|
||
},
|
||
],
|
||
}
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|