
Decision Logger
- 86 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Decision Logger is a Claude skill providing a two-layer memory that separates raw deliberation transcripts from founder-approved decisions to prevent hallucinated consensus.
About
Decision Logger is a two-layer memory system for tracking executive decisions across agent sessions. Layer 1 stores full deliberation transcripts and Layer 2 stores only founder-approved decisions, so future sessions read only what was approved and rejected ideas do not creep back in. It handles conflict detection, supersession tracking, overdue action-item alerts, and decision search. A team uses it to log decisions, review past ones, and prevent hallucinated consensus.
- Two-layer memory: raw deliberation transcripts (Layer 1) vs founder-approved decisions (Layer 2)
- Prevents hallucinated consensus by only auto-loading approved decisions each session
- Detects DO_NOT_RESURFACE violations, topic contradictions, owner conflicts, and overdue action items
Decision Logger by the numbers
- 86 all-time installs (skills.sh)
- Ranked #1,417 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
decision-logger capabilities & compatibility
- Capabilities
- culture architect
- Use cases
- memory · planning · project management
- Pricing
- Free
What decision-logger says it does
Layer 1 stores raw deliberation transcripts, Layer 2 stores founder-approved decisions only.
Future sessions read Layer 2 only -- this prevents hallucinated consensus from past debates bleeding into new deliberations.
Detection: New proposal text similarity > 70% to a rejected proposal
npx skills add https://github.com/borghei/claude-skills --skill decision-loggerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 86 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Track executive decisions in a two-layer memory so agents never resurface rejected proposals as consensus.
Who is it for?
Founder or executive teams that want an agent to remember only approved decisions and flag conflicts and overdue action items.
Skip if: General-purpose note-taking; it is a governance memory tied to approved-decision discipline.
When should I use this skill?
Logging a decision, reviewing past decisions, checking overdue items, or detecting conflicting decisions.
What you get
An append-only record of approved decisions with conflict detection, supersession tracking, and overdue-item alerts.
- append-only approved-decision log
- conflict and supersession reports
- overdue action-item alerts
By the numbers
- 2-layer memory architecture
- 70% similarity threshold for DO_NOT_RESURFACE detection
- 3 conflict types detected (resurface, topic contradiction, owner conflict)
Files
Decision Logger
Two-layer memory system for executive decisions. Layer 1 stores everything discussed. Layer 2 stores only what the founder approved. Future sessions read Layer 2 only -- this prevents hallucinated consensus from past debates bleeding into new deliberations.
Keywords
decision log, memory, approved decisions, action items, board minutes, conflict detection, DO_NOT_RESURFACE, decision history, overdue, supersession, decision search, decision tracking, accountability
---
Two-Layer Architecture
Why Two Layers?
Single-layer decision logs create a dangerous problem: agents read old debates, rejected proposals, and discarded ideas, then treat them as context for new decisions. This causes "hallucinated consensus" where rejected ideas gradually become accepted through repetition.
The two-layer system prevents this by strictly separating raw discussion from approved decisions.
Layer Architecture
Layer 1: Raw Transcripts (NEVER auto-loaded)
Location: memory/board-meetings/YYYY-MM-DD-raw.md
Contains: Full deliberation, all perspectives, rejected arguments
Loaded: Only on explicit founder request
Retention: Active 90 days, then archived
Layer 2: Approved Decisions (AUTO-LOADED every session)
Location: memory/board-meetings/decisions.md
Contains: Only founder-approved decisions and action items
Loaded: Automatically at start of every board meeting (Phase 1)
Mutation: Append-only. Decisions are never deleted, only superseded.Layer Interaction Rules
| Rule | Rationale |
|---|---|
| Layer 2 is append-only | Preserves complete decision history |
| Layer 1 is never auto-loaded | Prevents hallucinated consensus |
| Only Chief of Staff writes to Layer 2 | Single point of control |
| Agents never write directly | All writes go through Chief of Staff after founder approval |
| Superseded decisions stay in Layer 2 | History is the record; nothing is deleted |
---
Decision Entry Format
Standard Decision Record
## [YYYY-MM-DD] -- [DECISION TITLE]
**Decision:** [One clear statement of what was decided]
**Context:** [1-2 sentences on why this decision was needed]
**Owner:** [One person or role accountable for execution]
**Deadline:** [YYYY-MM-DD]
**Review Date:** [YYYY-MM-DD]
**Confidence:** [High / Medium / Low]
**Rationale:** [Why this option over alternatives, 1-2 sentences]
**User Override:** [If founder changed agent recommendation -- what and why]
**Rejected Alternatives:**
- [Proposal] -- [reason for rejection] [DO_NOT_RESURFACE]
- [Proposal] -- [reason for rejection]
**Action Items:**
- [ ] [Action] -- Owner: [name] -- Due: [YYYY-MM-DD]
- [ ] [Action] -- Owner: [name] -- Due: [YYYY-MM-DD]
**Dependencies:** [Other decisions this depends on]
**Supersedes:** [DATE of previous decision on same topic, if any]
**Superseded by:** [Filled retroactively if overridden later]
**Raw transcript:** memory/board-meetings/[DATE]-raw.md
**Tags:** [topic tags for search -- e.g., pricing, hiring, market-entry]Completed Action Item Format
- [x] [Action] -- Owner: [name] -- Completed: [YYYY-MM-DD] -- Result: [one sentence]---
Conflict Detection System
Before logging any new decision, the system checks for three types of conflicts.
Conflict Type 1: DO_NOT_RESURFACE Violation
A new decision matches a previously rejected proposal.
Detection: New proposal text similarity > 70% to a rejected proposal
Response:
BLOCKED: "[Proposal]" was rejected on [DATE].
Reason: [original rejection reason]
To reopen: Founder must explicitly say "reopen [topic] from [DATE]"
This cannot be overridden by agents.Conflict Type 2: Topic Contradiction
Two active decisions on the same topic reach different conclusions.
Detection: Same tags + contradictory conclusions
Response:
DECISION CONFLICT DETECTED
Active decision (older): [DATE] -- [decision text]
New decision: [DATE] -- [decision text]
These decisions contradict each other.
Options:
1. Supersede old decision (new replaces old)
2. Merge decisions (reconcile the conflict)
3. Defer to founder (present both, let founder choose)Conflict Type 3: Owner Conflict
Same action assigned to different people in different decisions.
Detection: Same action description, different owners
Response:
OWNER CONFLICT
Action: "[action text]"
Decision 1 ([DATE]): Owner = [Person A]
Decision 2 ([DATE]): Owner = [Person B]
Resolve: Which owner is correct?Conflict Resolution Decision Tree
START: Conflict detected
|
v
[What type of conflict?]
|
+-- DO_NOT_RESURFACE --> Block automatically. Only founder can reopen.
|
+-- Topic contradiction --> [Is the new decision from a board meeting?]
| |
| +-- YES --> Supersede old by default (board > individual)
| +-- NO --> Present both to founder for resolution
|
+-- Owner conflict --> [Which decision is more recent?]
|
+-- Flag to founder with both dates
+-- Default to more recent unless founder overrides---
Decision Lifecycle
States
PROPOSED --> APPROVED --> ACTIVE --> [COMPLETED | SUPERSEDED | EXPIRED]
PROPOSED: Agent synthesis presented to founder
APPROVED: Founder explicitly approved
ACTIVE: Being executed, action items in progress
COMPLETED: All action items done, review confirmed success
SUPERSEDED: New decision replaced this one
EXPIRED: Review date passed without renewalState Transitions
| From | To | Trigger | Who |
|---|---|---|---|
| Proposed | Approved | Founder says "yes" or "approve" | Founder |
| Proposed | Rejected | Founder says "no" or "reject" | Founder |
| Approved | Active | Action items begin execution | Automatic |
| Active | Completed | All action items marked done | Chief of Staff |
| Active | Superseded | New decision on same topic | Chief of Staff |
| Active | Expired | Review date passed, no renewal | System alert |
---
Logging Workflow
Post-Decision Logging (after Board Meeting Phase 5)
Step 1: Founder approves synthesis
|
Step 2: Write Layer 1 raw transcript
--> memory/board-meetings/YYYY-MM-DD-raw.md
|
Step 3: Run conflict detection against decisions.md
|
+-- Conflicts found --> Surface to founder, wait for resolution
+-- No conflicts --> Continue
|
Step 4: Append approved entries to decisions.md (Layer 2)
|
Step 5: Set review dates and action item deadlines
|
Step 6: Confirm to founder:
"Logged: [N] decisions, [M] action items tracked, [K] flags added"---
Action Item Management
Overdue Detection
At the start of every session, scan for:
1. Action items past their deadline 2. Decisions with review dates that have passed 3. Decisions older than 90 days with no completion status
Alert Format
OVERDUE ITEMS (as of [today's date])
Action Items Past Deadline:
1. [Action] -- Owner: [name] -- Due: [date] -- [X] days overdue
From decision: [decision title] ([date])
2. [Action] -- Owner: [name] -- Due: [date] -- [X] days overdue
Decisions Pending Review:
1. [Decision title] -- Review was due: [date]
Original decision: [summary]
Prompt: "You decided [X] on [date]. Worth a check-in?"
Stale Decisions (> 90 days, no status update):
1. [Decision title] -- Decided: [date] -- Last update: [date]Action Item Priority Matrix
| Urgency | Impact | Priority | Response |
|---|---|---|---|
| Overdue | High | Critical | Escalate to founder immediately |
| Overdue | Low | High | Flag in next session |
| Due this week | High | High | Surface proactively |
| Due this week | Low | Medium | Include in weekly summary |
| Due next month | Any | Low | Monitor only |
---
Search and Retrieval
Search Capabilities
| Query Type | Example | Returns |
|---|---|---|
| By topic | "pricing" | All decisions tagged with pricing |
| By owner | "CTO" | All decisions and actions owned by CTO |
| By date range | "Q4 2025" | All decisions from Oct-Dec 2025 |
| By status | "overdue" | All overdue action items |
| By conflict | "conflicts" | All detected contradictions |
| By tag | "hiring AND engineering" | Intersection of tags |
Decision Summary Views
| View | Contents | When Used |
|---|---|---|
| Last 10 | Most recent 10 approved decisions | Default quick view |
| Full history | All decisions, chronological | Audit or deep review |
| By owner | Grouped by accountable person | Accountability check |
| By topic | Grouped by tag | Strategic review |
| Overdue only | Only overdue items | Action management |
| Active only | Only decisions with open action items | Execution tracking |
---
File Structure
memory/
board-meetings/
decisions.md # Layer 2: append-only, founder-approved
YYYY-MM-DD-raw.md # Layer 1: full transcript per meeting
archive/
YYYY/ # Raw transcripts after 90 days---
Integration with Other Skills
| Skill | Integration Point |
|---|---|
Chief of Staff (chief-of-staff) | Manages the logging workflow, writes to Layer 2 |
Board Meeting (board-meeting) | Triggers logging after Phase 5 approval |
Strategic Alignment (strategic-alignment) | Checks if decisions cascade properly to team goals |
Executive Mentor (executive-mentor) | Reviews stale decisions for re-evaluation |
Org Health (org-health-diagnostic) | Decision velocity as health indicator |
---
Red Flags
- Same topic discussed 3+ times without a logged decision -- decision avoidance
- Action items consistently overdue by the same owner -- capacity or accountability issue
- Decisions made without checking history -- risk of contradiction
- Layer 1 being loaded without explicit request -- hallucinated consensus risk
- No review dates set on decisions -- decisions age without re-evaluation
- Rejected proposals resurfacing in new language -- DO_NOT_RESURFACE not enforced
- Decision log not consulted at start of board meetings -- institutional memory not used
- All decisions owned by one person -- bottleneck or delegation failure
---
Proactive Triggers
- Review date passed on a decision -- prompt: "You decided [X] on [date]. Worth checking in?"
- Action item overdue > 7 days -- escalate to founder with owner context
- Same topic area has 3+ active decisions -- consolidation review needed
- 30+ days without any logged decision -- is the system being used?
- New decision proposed that matches DO_NOT_RESURFACE -- block and explain
- Decision from 6+ months ago with no status update -- mark as stale, prompt review
---
Output Artifacts
| Request | Deliverable |
|---|---|
| "Show recent decisions" | Last 10 approved decisions with status |
| "What's overdue?" | All overdue action items with owner and days past due |
| "Search decisions about [topic]" | Filtered decision history by topic/tag |
| "Log this decision" | Formatted decision entry with all fields |
| "Check for conflicts" | Conflict scan against all active decisions |
| "Decision summary for board" | Decision velocity, completion rate, open items |
---
Tool Reference
1. decision_tracker.py
Tracks executive decisions with full lifecycle management (Proposed > Approved > Active > Completed/Superseded/Expired). Scans for overdue action items, stale decisions, and generates status summaries.
python scripts/decision_tracker.py --input decisions.json --json
python scripts/decision_tracker.py --input decisions.json| Flag | Type | Description |
|---|---|---|
--input | required | Path to JSON file with decision records (title, status, owner, deadline, action items, tags) |
--json | optional | Output in JSON format instead of human-readable text |
2. decision_quality_scorer.py
Scores decision quality across 6 dimensions: framing (problem definition), alternatives (options considered), information (evidence quality), reasoning (logic soundness), commitment (action clarity), and metacognition (awareness of uncertainty). Generates improvement recommendations.
python scripts/decision_quality_scorer.py --input decision_assessments.json --json
python scripts/decision_quality_scorer.py --input decision_assessments.json| Flag | Type | Description |
|---|---|---|
--input | required | Path to JSON file with decision assessments (dimension scores 1-10, optional outcome data) |
--json | optional | Output in JSON format instead of human-readable text |
3. decision_tree_builder.py
Builds decision trees with expected value analysis. Calculates optimal paths through probability-weighted outcomes, identifies highest-value decisions, and generates sensitivity analysis on key assumptions.
python scripts/decision_tree_builder.py --input tree_data.json --json
python scripts/decision_tree_builder.py --input tree_data.json| Flag | Type | Description |
|---|---|---|
--input | required | Path to JSON file with decision nodes (options, probabilities, outcomes, values) |
--json | optional | Output in JSON format instead of human-readable text |
---
Troubleshooting
| Problem | Likely Cause | Resolution |
|---|---|---|
| Same topic discussed 3+ times without a logged decision | Decision avoidance or no clear decision-making authority | Force a decision at next session; use decision tree builder to clarify options; assign explicit decision owner |
| Action items consistently overdue by same owner | Owner over-committed, lacks capacity, or accountability issue | Review owner workload; redistribute if capacity issue; escalate to founder if accountability issue |
| Decisions made without checking history | Decision log not consulted at session start; no integration habit | Automate decision log loading at board meeting Phase 1; surface relevant past decisions proactively |
| Rejected proposals resurfacing in new language | DO_NOT_RESURFACE not enforced; team members unaware of prior rejection | Enforce conflict detection before logging; block proposals matching rejected items; require explicit "reopen" from founder |
| Decision log growing but never consulted for patterns | Log treated as archive, not strategic tool | Run quarterly decision review; analyze decision velocity, completion rate, and quality trends |
| All decisions owned by one person | Bottleneck or delegation failure | Distribute ownership; use decision quality scorer to assess whether centralization improves or hurts quality |
| Decision quality scores low on "alternatives" dimension | Team anchoring on first option instead of exploring | Require minimum 3 alternatives for decisions above a threshold; use decision tree builder to model options |
---
Success Criteria
- All board meeting decisions logged in Layer 2 within 24 hours of approval
- Action item completion rate exceeds 80% within stated deadlines
- Zero DO_NOT_RESURFACE violations (rejected proposals do not re-enter decision flow)
- Decision review dates honored for 90%+ of active decisions
- Decision quality score averages above 7/10 across all 6 dimensions
- Conflict detection catches 100% of topic contradictions before new decisions are logged
- Decision log consulted at the start of every board meeting session
---
Scope & Limitations
In scope: Two-layer decision memory architecture, decision entry and lifecycle management (Proposed > Approved > Active > Completed/Superseded/Expired), conflict detection (DO_NOT_RESURFACE, topic contradiction, owner conflict), action item tracking with overdue alerting, decision search and retrieval by topic/owner/date/status, decision quality scoring, and expected value analysis via decision trees.
Out of scope: CRM or project management tool integration (tools consume JSON exports), meeting transcription or recording, team-level task management (use project-management/ skills), strategic planning or OKR tracking (use strategic-alignment or ceo-advisor), and automated decision-making. This skill tracks and evaluates decisions; it does not make them.
Limitations: Conflict detection uses tag and text matching; semantically similar but differently worded proposals may not be caught. Decision quality scoring is retrospective and depends on honest self-assessment. Decision tree expected value calculations assume probabilities are estimable; highly uncertain environments may make probability assignment misleading. The two-layer architecture requires discipline to maintain; if Layer 2 is not consistently updated, institutional memory degrades.
---
Integration Points
- chief-of-staff -- Manages the logging workflow; single point of control for Layer 2 writes
- board-meeting -- Triggers decision logging after Phase 5 approval; decision log loaded at Phase 1
- strategic-alignment -- Checks if decisions cascade properly to team goals and OKRs
- executive-mentor -- Reviews stale decisions for re-evaluation; coaches on decision quality improvement
- ceo-advisor -- Strategic decisions logged and tracked; decision patterns inform leadership coaching
#!/usr/bin/env python3
"""
Decision Quality Scorer - Score decision quality across 6 dimensions.
Evaluates framing, alternatives, information, reasoning, commitment, and metacognition.
Generates quality profiles, identifies improvement areas, and tracks quality over time.
Usage:
python decision_quality_scorer.py --input decision_assessments.json
python decision_quality_scorer.py --input decision_assessments.json --json
"""
import argparse
import json
import sys
from datetime import datetime
QUALITY_DIMENSIONS = {
"framing": {
"label": "Framing",
"description": "Problem definition clarity and scope",
"weight": 0.20,
"improvement_tips": {
"low": "Spend more time defining the problem before jumping to solutions",
"medium": "Validate the problem framing with stakeholders before proceeding",
"high": "Strong framing -- ensure it is revisited if new information emerges",
},
},
"alternatives": {
"label": "Alternatives",
"description": "Number and quality of options considered",
"weight": 0.20,
"improvement_tips": {
"low": "Require minimum 3 alternatives before deciding -- avoid anchoring on first option",
"medium": "Include creative or contrarian options -- not just obvious choices",
"high": "Excellent option generation -- document why rejected alternatives were dropped",
},
},
"information": {
"label": "Information",
"description": "Quality and completeness of evidence used",
"weight": 0.15,
"improvement_tips": {
"low": "Decision made with insufficient data -- identify what data would change the decision",
"medium": "Adequate data but gaps remain -- document known unknowns",
"high": "Strong evidence base -- ensure data is current and unbiased",
},
},
"reasoning": {
"label": "Reasoning",
"description": "Logic soundness and bias awareness",
"weight": 0.15,
"improvement_tips": {
"low": "Check for common biases: confirmation bias, sunk cost, anchoring, recency",
"medium": "Logic is sound but may benefit from devil's advocate review",
"high": "Rigorous reasoning -- ensure it is documented for future reference",
},
},
"commitment": {
"label": "Commitment",
"description": "Action clarity, ownership, and deadlines",
"weight": 0.15,
"improvement_tips": {
"low": "Decision made but no clear actions, owners, or deadlines -- add them immediately",
"medium": "Actions defined but may lack specific deadlines or success criteria",
"high": "Clear actions with owners and deadlines -- ensure follow-up is scheduled",
},
},
"metacognition": {
"label": "Metacognition",
"description": "Awareness of uncertainty and reversibility",
"weight": 0.15,
"improvement_tips": {
"low": "Overconfident -- explicitly state what could go wrong and how you would know",
"medium": "Some uncertainty acknowledged -- add specific triggers for re-evaluation",
"high": "Excellent self-awareness -- review date set and contingency plan documented",
},
},
}
def load_data(path):
with open(path, "r") as f:
return json.load(f)
def get_quality_level(score):
if score >= 8:
return "high"
elif score >= 5:
return "medium"
else:
return "low"
def get_overall_label(score):
if score >= 8:
return "Excellent"
elif score >= 6:
return "Good"
elif score >= 4:
return "Adequate"
else:
return "Poor"
def score_decision(assessment):
"""Score a single decision across all dimensions."""
title = assessment.get("title", "Unnamed Decision")
scores = assessment.get("scores", {})
outcome = assessment.get("outcome", {})
dimension_results = {}
weighted_total = 0
for dim_key, dim_config in QUALITY_DIMENSIONS.items():
score = scores.get(dim_key, 5) # Default to 5 if not provided
score = max(1, min(10, score)) # Clamp 1-10
level = get_quality_level(score)
weighted = score * dim_config["weight"]
weighted_total += weighted
dimension_results[dim_key] = {
"label": dim_config["label"],
"score": score,
"level": level,
"weighted_score": round(weighted, 2),
"improvement_tip": dim_config["improvement_tips"][level],
}
overall = round(weighted_total, 1)
result = {
"title": title,
"date": assessment.get("date", "Unknown"),
"overall_quality_score": overall,
"overall_label": get_overall_label(overall),
"dimensions": dimension_results,
"weakest_dimension": min(dimension_results, key=lambda k: dimension_results[k]["score"]),
"strongest_dimension": max(dimension_results, key=lambda k: dimension_results[k]["score"]),
}
# Outcome correlation (if available)
if outcome:
result["outcome"] = {
"success": outcome.get("success", None),
"outcome_score": outcome.get("outcome_score", None),
"notes": outcome.get("notes", ""),
}
return result
def analyze_decisions(data):
"""Analyze all decision quality assessments."""
assessments = data.get("assessments", [])
org_name = data.get("organization", "Organization")
results = {
"timestamp": datetime.now().isoformat(),
"organization": org_name,
"decisions_assessed": len(assessments),
"decision_results": [],
"aggregate_scores": {},
"dimension_averages": {},
"patterns": [],
"recommendations": [],
}
all_dimension_scores = {dim: [] for dim in QUALITY_DIMENSIONS}
all_overall_scores = []
for assessment in assessments:
result = score_decision(assessment)
results["decision_results"].append(result)
all_overall_scores.append(result["overall_quality_score"])
for dim_key, dim_data in result["dimensions"].items():
all_dimension_scores[dim_key].append(dim_data["score"])
# Aggregate scores
if all_overall_scores:
results["aggregate_scores"] = {
"avg_quality": round(sum(all_overall_scores) / len(all_overall_scores), 1),
"min_quality": min(all_overall_scores),
"max_quality": max(all_overall_scores),
"quality_label": get_overall_label(sum(all_overall_scores) / len(all_overall_scores)),
}
# Dimension averages
for dim_key, scores in all_dimension_scores.items():
if scores:
avg = round(sum(scores) / len(scores), 1)
results["dimension_averages"][dim_key] = {
"label": QUALITY_DIMENSIONS[dim_key]["label"],
"average": avg,
"level": get_quality_level(avg),
}
# Patterns
if results["dimension_averages"]:
weakest = min(results["dimension_averages"], key=lambda k: results["dimension_averages"][k]["average"])
strongest = max(results["dimension_averages"], key=lambda k: results["dimension_averages"][k]["average"])
results["patterns"].append(f"Consistently weakest: {QUALITY_DIMENSIONS[weakest]['label']} (avg: {results['dimension_averages'][weakest]['average']}/10)")
results["patterns"].append(f"Consistently strongest: {QUALITY_DIMENSIONS[strongest]['label']} (avg: {results['dimension_averages'][strongest]['average']}/10)")
# Check for low-alternatives pattern
alt_avg = results["dimension_averages"].get("alternatives", {}).get("average", 5)
if alt_avg < 5:
results["patterns"].append("Pattern: decisions consistently lack sufficient alternatives -- anchoring risk")
# Recommendations
recs = results["recommendations"]
if results["aggregate_scores"].get("avg_quality", 0) < 5:
recs.append("Overall decision quality is low -- implement structured decision-making process")
for dim_key, dim_info in results["dimension_averages"].items():
if dim_info["level"] == "low":
tip = QUALITY_DIMENSIONS[dim_key]["improvement_tips"]["low"]
recs.append(f"Improve {dim_info['label']}: {tip}")
# Outcome correlation
decisions_with_outcomes = [d for d in results["decision_results"] if d.get("outcome", {}).get("success") is not None]
if len(decisions_with_outcomes) >= 3:
high_q_success = sum(1 for d in decisions_with_outcomes if d["overall_quality_score"] >= 7 and d["outcome"]["success"])
high_q_total = sum(1 for d in decisions_with_outcomes if d["overall_quality_score"] >= 7)
if high_q_total > 0:
success_rate = round((high_q_success / high_q_total) * 100, 0)
recs.append(f"High-quality decisions (score >= 7) have {success_rate}% success rate")
return results
def format_text(results):
lines = [
"=" * 60,
"DECISION QUALITY REPORT",
"=" * 60,
f"Organization: {results['organization']}",
f"Decisions Assessed: {results['decisions_assessed']}",
f"Report Date: {results['timestamp'][:10]}",
]
agg = results["aggregate_scores"]
if agg:
lines.extend([
"",
f"AGGREGATE QUALITY: {agg['avg_quality']}/10 ({agg['quality_label']})",
f" Range: {agg['min_quality']} - {agg['max_quality']}",
])
if results["dimension_averages"]:
lines.append("")
lines.append("DIMENSION AVERAGES")
sorted_dims = sorted(results["dimension_averages"].items(), key=lambda x: x[1]["average"])
for dim_key, info in sorted_dims:
lines.append(f" {info['label']}: {info['average']}/10 ({info['level']})")
lines.append("")
lines.append("INDIVIDUAL DECISIONS")
for d in results["decision_results"]:
lines.append(f"\n {d['title']} ({d['date']})")
lines.append(f" Quality: {d['overall_quality_score']}/10 ({d['overall_label']})")
lines.append(f" Weakest: {QUALITY_DIMENSIONS[d['weakest_dimension']]['label']} | "
f"Strongest: {QUALITY_DIMENSIONS[d['strongest_dimension']]['label']}")
for dim_key, dim_data in d["dimensions"].items():
lines.append(f" {dim_data['label']}: {dim_data['score']}/10")
if d.get("outcome", {}).get("success") is not None:
lines.append(f" Outcome: {'Success' if d['outcome']['success'] else 'Failed'}")
if results["patterns"]:
lines.append("")
lines.append("PATTERNS")
for p in results["patterns"]:
lines.append(f" * {p}")
if results["recommendations"]:
lines.append("")
lines.append("RECOMMENDATIONS")
for rec in results["recommendations"]:
lines.append(f" * {rec}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Score decision quality across 6 dimensions")
parser.add_argument("--input", required=True, help="Path to JSON decision assessments file")
parser.add_argument("--json", action="store_true", help="Output in JSON format")
args = parser.parse_args()
try:
data = load_data(args.input)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading input file: {e}", file=sys.stderr)
sys.exit(1)
results = analyze_decisions(data)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_text(results))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Decision Tracker - Track executive decisions with lifecycle management.
Manages decisions through states (Proposed > Approved > Active > Completed/Superseded/Expired).
Scans for overdue action items, stale decisions, and generates status summaries.
Usage:
python decision_tracker.py --input decisions.json
python decision_tracker.py --input decisions.json --json
"""
import argparse
import json
import sys
from datetime import datetime, timedelta
def load_data(path):
with open(path, "r") as f:
return json.load(f)
def parse_date(date_str):
"""Parse date string to datetime."""
if not date_str:
return None
for fmt in ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"):
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue
return None
def check_overdue_actions(decision, today):
"""Check for overdue action items in a decision."""
overdue = []
for action in decision.get("action_items", []):
if action.get("completed", False):
continue
due_date = parse_date(action.get("due_date"))
if due_date and due_date < today:
days_overdue = (today - due_date).days
overdue.append({
"action": action.get("description", "Unknown"),
"owner": action.get("owner", "Unassigned"),
"due_date": action.get("due_date"),
"days_overdue": days_overdue,
"decision_title": decision.get("title", "Unknown"),
"decision_date": decision.get("date", "Unknown"),
})
return overdue
def check_stale_decisions(decision, today, stale_days=90):
"""Check if a decision is stale (no update in stale_days)."""
status = decision.get("status", "active")
if status in ("completed", "superseded", "expired"):
return None
last_update = parse_date(decision.get("last_updated", decision.get("date")))
if not last_update:
return None
age = (today - last_update).days
if age > stale_days:
return {
"title": decision.get("title", "Unknown"),
"date": decision.get("date", "Unknown"),
"days_since_update": age,
"status": status,
}
return None
def check_review_overdue(decision, today):
"""Check if review date has passed."""
review_date = parse_date(decision.get("review_date"))
if not review_date:
return None
if review_date < today and decision.get("status") not in ("completed", "superseded", "expired"):
return {
"title": decision.get("title", "Unknown"),
"review_date": decision.get("review_date"),
"days_overdue": (today - review_date).days,
}
return None
def detect_conflicts(decisions):
"""Detect potential conflicts between active decisions."""
conflicts = []
active = [d for d in decisions if d.get("status") in ("approved", "active")]
for i, d1 in enumerate(active):
tags1 = set(d1.get("tags", []))
for d2 in active[i + 1:]:
tags2 = set(d2.get("tags", []))
overlap = tags1 & tags2
if overlap and len(overlap) >= 2:
conflicts.append({
"decision_1": {"title": d1.get("title"), "date": d1.get("date")},
"decision_2": {"title": d2.get("title"), "date": d2.get("date")},
"overlapping_tags": list(overlap),
"type": "potential_topic_conflict",
})
return conflicts
def analyze_decisions(data):
"""Run full decision tracking analysis."""
decisions = data.get("decisions", [])
today = datetime.now()
analysis_date = data.get("analysis_date")
if analysis_date:
today = parse_date(analysis_date) or today
results = {
"timestamp": datetime.now().isoformat(),
"analysis_date": today.strftime("%Y-%m-%d"),
"total_decisions": len(decisions),
"status_distribution": {},
"overdue_actions": [],
"stale_decisions": [],
"review_overdue": [],
"conflicts": [],
"owner_distribution": {},
"recent_decisions": [],
"summary": {},
"recommendations": [],
}
# Status distribution
status_counts = {}
owner_counts = {}
total_actions = 0
completed_actions = 0
all_overdue = []
for decision in decisions:
status = decision.get("status", "unknown")
status_counts[status] = status_counts.get(status, 0) + 1
owner = decision.get("owner", "Unassigned")
owner_counts[owner] = owner_counts.get(owner, 0) + 1
# Check overdue actions
overdue = check_overdue_actions(decision, today)
all_overdue.extend(overdue)
# Count actions
for action in decision.get("action_items", []):
total_actions += 1
if action.get("completed", False):
completed_actions += 1
# Check stale
stale = check_stale_decisions(decision, today)
if stale:
results["stale_decisions"].append(stale)
# Check review
review = check_review_overdue(decision, today)
if review:
results["review_overdue"].append(review)
results["status_distribution"] = status_counts
results["owner_distribution"] = owner_counts
results["overdue_actions"] = sorted(all_overdue, key=lambda x: x["days_overdue"], reverse=True)
# Conflicts
results["conflicts"] = detect_conflicts(decisions)
# Recent decisions (last 10)
sorted_decisions = sorted(decisions, key=lambda x: x.get("date", ""), reverse=True)
results["recent_decisions"] = [
{
"title": d.get("title", "Unknown"),
"date": d.get("date", "Unknown"),
"status": d.get("status", "unknown"),
"owner": d.get("owner", "Unassigned"),
"confidence": d.get("confidence", "unknown"),
}
for d in sorted_decisions[:10]
]
# Summary
action_completion_rate = round((completed_actions / max(total_actions, 1)) * 100, 1)
active_decisions = status_counts.get("active", 0) + status_counts.get("approved", 0)
results["summary"] = {
"active_decisions": active_decisions,
"completed_decisions": status_counts.get("completed", 0),
"total_action_items": total_actions,
"completed_action_items": completed_actions,
"action_completion_rate": action_completion_rate,
"overdue_action_count": len(all_overdue),
"stale_decision_count": len(results["stale_decisions"]),
"review_overdue_count": len(results["review_overdue"]),
"conflict_count": len(results["conflicts"]),
}
# Recommendations
recs = results["recommendations"]
if all_overdue:
critical_overdue = [a for a in all_overdue if a["days_overdue"] > 14]
if critical_overdue:
recs.append(f"CRITICAL: {len(critical_overdue)} action items overdue by 14+ days -- escalate to founder")
else:
recs.append(f"{len(all_overdue)} action items overdue -- review in next session")
if results["stale_decisions"]:
recs.append(f"{len(results['stale_decisions'])} decisions stale (>90 days without update) -- schedule review")
if results["review_overdue"]:
recs.append(f"{len(results['review_overdue'])} decisions past review date -- prompt founder for check-in")
if results["conflicts"]:
recs.append(f"{len(results['conflicts'])} potential decision conflicts detected -- resolve before logging new decisions")
# Owner concentration
if owner_counts:
max_owner = max(owner_counts, key=owner_counts.get)
max_count = owner_counts[max_owner]
if max_count > len(decisions) * 0.5 and len(decisions) > 5:
recs.append(f"Owner concentration: {max_owner} owns {max_count}/{len(decisions)} decisions -- consider distributing")
if action_completion_rate < 60:
recs.append(f"Action completion rate at {action_completion_rate}% -- accountability or capacity issue")
return results
def format_text(results):
lines = [
"=" * 60,
"DECISION TRACKING REPORT",
"=" * 60,
f"Analysis Date: {results['analysis_date']}",
f"Total Decisions: {results['total_decisions']}",
"",
"SUMMARY",
f" Active Decisions: {results['summary']['active_decisions']}",
f" Completed: {results['summary']['completed_decisions']}",
f" Action Items: {results['summary']['completed_action_items']}/{results['summary']['total_action_items']} "
f"({results['summary']['action_completion_rate']}% complete)",
f" Overdue Actions: {results['summary']['overdue_action_count']}",
f" Stale Decisions: {results['summary']['stale_decision_count']}",
f" Reviews Overdue: {results['summary']['review_overdue_count']}",
f" Conflicts: {results['summary']['conflict_count']}",
]
if results["overdue_actions"]:
lines.append("")
lines.append("OVERDUE ACTION ITEMS")
for a in results["overdue_actions"][:10]:
lines.append(f" [{a['days_overdue']}d overdue] {a['action']}")
lines.append(f" Owner: {a['owner']} | Due: {a['due_date']} | Decision: {a['decision_title']}")
if results["stale_decisions"]:
lines.append("")
lines.append("STALE DECISIONS (>90 days)")
for s in results["stale_decisions"]:
lines.append(f" {s['title']} -- {s['days_since_update']} days since update (status: {s['status']})")
if results["review_overdue"]:
lines.append("")
lines.append("REVIEWS OVERDUE")
for r in results["review_overdue"]:
lines.append(f" {r['title']} -- review was due {r['review_date']} ({r['days_overdue']} days ago)")
if results["conflicts"]:
lines.append("")
lines.append("POTENTIAL CONFLICTS")
for c in results["conflicts"]:
lines.append(f" {c['decision_1']['title']} vs {c['decision_2']['title']}")
lines.append(f" Overlapping tags: {', '.join(c['overlapping_tags'])}")
lines.append("")
lines.append("RECENT DECISIONS")
for d in results["recent_decisions"]:
lines.append(f" {d['date']}: {d['title']} ({d['status']}) -- Owner: {d['owner']}")
if results["recommendations"]:
lines.append("")
lines.append("RECOMMENDATIONS")
for rec in results["recommendations"]:
lines.append(f" * {rec}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Track executive decisions with lifecycle management")
parser.add_argument("--input", required=True, help="Path to JSON decisions file")
parser.add_argument("--json", action="store_true", help="Output in JSON format")
args = parser.parse_args()
try:
data = load_data(args.input)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading input file: {e}", file=sys.stderr)
sys.exit(1)
results = analyze_decisions(data)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_text(results))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Decision Tree Builder - Build decision trees with expected value analysis.
Calculates optimal paths through probability-weighted outcomes, identifies
highest-value decisions, and generates sensitivity analysis on key assumptions.
Usage:
python decision_tree_builder.py --input tree_data.json
python decision_tree_builder.py --input tree_data.json --json
"""
import argparse
import json
import sys
from datetime import datetime
def load_data(path):
with open(path, "r") as f:
return json.load(f)
def calculate_expected_value(outcomes):
"""Calculate expected value from probability-weighted outcomes."""
ev = 0
for outcome in outcomes:
probability = outcome.get("probability", 0)
value = outcome.get("value", 0)
ev += probability * value
return round(ev, 2)
def validate_probabilities(outcomes):
"""Validate that probabilities sum to approximately 1.0."""
total = sum(o.get("probability", 0) for o in outcomes)
return abs(total - 1.0) < 0.05 # Allow 5% tolerance
def build_tree(node, depth=0, path=""):
"""Recursively build and evaluate a decision tree."""
node_type = node.get("type", "decision")
name = node.get("name", f"Node-{depth}")
current_path = f"{path}/{name}" if path else name
result = {
"name": name,
"type": node_type,
"depth": depth,
"path": current_path,
}
if node_type == "outcome":
# Terminal node
result["value"] = node.get("value", 0)
result["expected_value"] = node.get("value", 0)
return result
if node_type == "chance":
# Chance node: calculate expected value from outcomes
outcomes = node.get("outcomes", [])
prob_valid = validate_probabilities(outcomes)
result["probability_valid"] = prob_valid
children = []
ev = 0
for outcome in outcomes:
child = build_tree(outcome.get("node", {"type": "outcome", "value": outcome.get("value", 0)}),
depth + 1, current_path)
child["probability"] = outcome.get("probability", 0)
child["outcome_label"] = outcome.get("label", "Unknown")
child_ev = outcome.get("probability", 0) * child.get("expected_value", 0)
ev += child_ev
child["weighted_value"] = round(child_ev, 2)
children.append(child)
result["children"] = children
result["expected_value"] = round(ev, 2)
return result
if node_type == "decision":
# Decision node: choose option with highest expected value
options = node.get("options", [])
children = []
best_ev = float("-inf")
best_option = None
for option in options:
child = build_tree(option, depth + 1, current_path)
children.append(child)
child_ev = child.get("expected_value", 0)
cost = option.get("cost", 0)
net_ev = child_ev - cost
child["cost"] = cost
child["net_expected_value"] = round(net_ev, 2)
if net_ev > best_ev:
best_ev = net_ev
best_option = child["name"]
result["children"] = children
result["best_option"] = best_option
result["best_expected_value"] = round(best_ev, 2)
result["expected_value"] = round(best_ev, 2)
return result
return result
def sensitivity_analysis(tree_data, variable_name, range_pct=20, steps=5):
"""Run sensitivity analysis on a key variable."""
results = []
base_tree = build_tree(tree_data)
base_ev = base_tree.get("best_expected_value", base_tree.get("expected_value", 0))
# Simple sensitivity: vary the first probability we find
for step in range(-steps, steps + 1):
factor = 1 + (step * range_pct / 100 / steps)
# Deep copy and modify
modified = json.loads(json.dumps(tree_data))
_apply_sensitivity(modified, variable_name, factor)
modified_tree = build_tree(modified)
modified_ev = modified_tree.get("best_expected_value", modified_tree.get("expected_value", 0))
results.append({
"factor": round(factor, 3),
"adjustment_pct": round((factor - 1) * 100, 1),
"expected_value": modified_ev,
"change_from_base": round(modified_ev - base_ev, 2),
"best_option": modified_tree.get("best_option", "N/A"),
})
return results
def _apply_sensitivity(node, variable_name, factor):
"""Recursively apply sensitivity factor to matching values."""
if isinstance(node, dict):
for key, val in node.items():
if key == "value" and node.get("name", "").lower() == variable_name.lower():
node[key] = round(val * factor, 2)
elif key == "probability" and node.get("label", "").lower() == variable_name.lower():
node[key] = min(1.0, round(val * factor, 4))
elif isinstance(val, (dict, list)):
_apply_sensitivity(val, variable_name, factor)
elif isinstance(node, list):
for item in node:
_apply_sensitivity(item, variable_name, factor)
def flatten_paths(tree, paths=None, current_path=None):
"""Extract all terminal paths with values."""
if paths is None:
paths = []
if current_path is None:
current_path = []
current_path = current_path + [tree.get("name", "?")]
if tree.get("type") == "outcome" or not tree.get("children"):
paths.append({
"path": " > ".join(current_path),
"value": tree.get("expected_value", tree.get("value", 0)),
})
return paths
for child in tree.get("children", []):
flatten_paths(child, paths, current_path)
return paths
def analyze_tree(data):
"""Run full decision tree analysis."""
tree_data = data.get("tree", data)
decision_name = data.get("decision_name", "Decision")
sensitivity_var = data.get("sensitivity_variable", None)
tree = build_tree(tree_data)
all_paths = flatten_paths(tree)
all_paths.sort(key=lambda x: x["value"], reverse=True)
results = {
"timestamp": datetime.now().isoformat(),
"decision_name": decision_name,
"tree": tree,
"optimal_path": all_paths[0] if all_paths else None,
"worst_path": all_paths[-1] if all_paths else None,
"all_paths": all_paths,
"path_count": len(all_paths),
"value_range": {
"min": all_paths[-1]["value"] if all_paths else 0,
"max": all_paths[0]["value"] if all_paths else 0,
"spread": round(all_paths[0]["value"] - all_paths[-1]["value"], 2) if all_paths else 0,
},
"sensitivity": None,
"recommendations": [],
}
# Best option from root
if tree.get("best_option"):
results["recommended_option"] = tree["best_option"]
results["recommended_ev"] = tree["best_expected_value"]
# Sensitivity analysis
if sensitivity_var:
results["sensitivity"] = {
"variable": sensitivity_var,
"results": sensitivity_analysis(tree_data, sensitivity_var),
}
# Recommendations
recs = results["recommendations"]
if tree.get("best_option"):
recs.append(f"Recommended: {tree['best_option']} (EV: {tree['best_expected_value']:,.0f})")
spread = results["value_range"]["spread"]
if spread > 0:
recs.append(f"Outcome range: {results['value_range']['min']:,.0f} to {results['value_range']['max']:,.0f} (spread: {spread:,.0f})")
if results["sensitivity"]:
sens = results["sensitivity"]["results"]
option_changes = set(s["best_option"] for s in sens if s["best_option"] != "N/A")
if len(option_changes) > 1:
recs.append(f"Sensitivity: optimal option changes with {sensitivity_var} variation -- decision is sensitive to this assumption")
else:
recs.append(f"Sensitivity: optimal option stable across {sensitivity_var} variations -- robust decision")
return results
def format_text(results):
lines = [
"=" * 60,
f"DECISION TREE ANALYSIS: {results['decision_name']}",
"=" * 60,
f"Analysis Date: {results['timestamp'][:10]}",
f"Paths Evaluated: {results['path_count']}",
]
if results.get("recommended_option"):
lines.extend([
"",
f"RECOMMENDATION: {results['recommended_option']}",
f" Expected Value: {results['recommended_ev']:,.0f}",
])
lines.extend([
"",
"VALUE RANGE",
f" Best Outcome: {results['value_range']['max']:,.0f}",
f" Worst Outcome: {results['value_range']['min']:,.0f}",
f" Spread: {results['value_range']['spread']:,.0f}",
])
if results["optimal_path"]:
lines.extend([
"",
f"OPTIMAL PATH: {results['optimal_path']['path']}",
f" Value: {results['optimal_path']['value']:,.0f}",
])
lines.append("")
lines.append("ALL PATHS (ranked by value)")
for p in results["all_paths"][:10]:
lines.append(f" {p['value']:>12,.0f} {p['path']}")
if results["sensitivity"]:
lines.append("")
lines.append(f"SENSITIVITY ANALYSIS: {results['sensitivity']['variable']}")
for s in results["sensitivity"]["results"]:
lines.append(
f" {s['adjustment_pct']:>+6.1f}%: EV={s['expected_value']:>12,.0f} "
f"(change: {s['change_from_base']:>+10,.0f}) option: {s['best_option']}"
)
if results["recommendations"]:
lines.append("")
lines.append("RECOMMENDATIONS")
for rec in results["recommendations"]:
lines.append(f" * {rec}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Build decision trees with expected value analysis")
parser.add_argument("--input", required=True, help="Path to JSON decision tree data file")
parser.add_argument("--json", action="store_true", help="Output in JSON format")
args = parser.parse_args()
try:
data = load_data(args.input)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading input file: {e}", file=sys.stderr)
sys.exit(1)
results = analyze_tree(data)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_text(results))
if __name__ == "__main__":
main()
Related skills
FAQ
Why two layers?
Layer 1 keeps full deliberation but is never auto-loaded; Layer 2 keeps only founder-approved decisions and is auto-loaded, preventing rejected ideas from re-entering as consensus.
Can it stop a rejected idea from coming back?
Yes. It blocks proposals that match a rejected one over 70% similarity as a DO_NOT_RESURFACE violation unless the founder explicitly reopens the topic.