From 319f4bd6dee913c6d854858f1b6e45f1003351b5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 21 Mar 2026 17:49:41 -0400 Subject: [PATCH] feat: add /gsd:forensics for post-mortem workflow investigation (#1303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New command that investigates failed or stuck GSD workflows by analyzing git history, .planning/ artifacts, and file system state. Detects 6 anomaly types: stuck loops, missing artifacts, abandoned work, crash/ interruption, scope drift, and test regressions. Inspired by gsd-build/gsd-2's forensics feature, adapted for gsd-1's markdown-prompt architecture. Uses heuristic (LLM-judged) analysis against available data sources rather than structured telemetry. - commands/gsd/forensics.md: command with success_criteria + critical_rules - get-shit-done/workflows/forensics.md: 8-step investigation workflow - tests/forensics.test.cjs: 21 tests (command, workflow, report, fixtures) - tests/copilot-install.test.cjs: bump expected skill count 55→56 Generates report to .planning/forensics/, offers interactive investigation, and optional GitHub issue creation from findings. Closes #1303 Co-Authored-By: Claude Opus 4.6 (1M context) --- commands/gsd/forensics.md | 56 ++++++ get-shit-done/workflows/forensics.md | 258 +++++++++++++++++++++++++++ tests/copilot-install.test.cjs | 4 +- tests/forensics.test.cjs | 258 +++++++++++++++++++++++++++ 4 files changed, 574 insertions(+), 2 deletions(-) create mode 100644 commands/gsd/forensics.md create mode 100644 get-shit-done/workflows/forensics.md create mode 100644 tests/forensics.test.cjs diff --git a/commands/gsd/forensics.md b/commands/gsd/forensics.md new file mode 100644 index 000000000..3bf67d2c7 --- /dev/null +++ b/commands/gsd/forensics.md @@ -0,0 +1,56 @@ +--- +type: prompt +name: gsd:forensics +description: Post-mortem investigation for failed GSD workflows — analyzes git history, artifacts, and state to diagnose what went wrong +argument-hint: "[problem description]" +allowed-tools: + - Read + - Write + - Bash + - Grep + - Glob +--- + + +Investigate what went wrong during a GSD workflow execution. Analyzes git history, `.planning/` artifacts, and file system state to detect anomalies and generate a structured diagnostic report. + +Purpose: Diagnose failed or stuck workflows so the user can understand root cause and take corrective action. +Output: Forensic report saved to `.planning/forensics/`, presented inline, with optional issue creation. + + + +@~/.claude/get-shit-done/workflows/forensics.md + + + +**Data sources:** +- `git log` (recent commits, patterns, time gaps) +- `git status` / `git diff` (uncommitted work, conflicts) +- `.planning/STATE.md` (current position, session history) +- `.planning/ROADMAP.md` (phase scope and progress) +- `.planning/phases/*/` (PLAN.md, SUMMARY.md, VERIFICATION.md, CONTEXT.md) +- `.planning/reports/SESSION_REPORT.md` (last session outcomes) + +**User input:** +- Problem description: $ARGUMENTS (optional — will ask if not provided) + + + +Read and execute the forensics workflow from @~/.claude/get-shit-done/workflows/forensics.md end-to-end. + + + +- Evidence gathered from all available data sources +- At least 4 anomaly types checked (stuck loop, missing artifacts, abandoned work, crash/interruption) +- Structured forensic report written to `.planning/forensics/report-{timestamp}.md` +- Report presented inline with findings, anomalies, and recommendations +- Interactive investigation offered for deeper analysis +- GitHub issue creation offered if actionable findings exist + + + +- **Read-only investigation:** Do not modify any project files during forensics. Only write the report. +- **Redact sensitive data:** Strip absolute paths, API keys, tokens from reports and issues. +- **Ground findings in evidence:** Every anomaly must cite specific commits, files, or state data. +- **No speculation without evidence:** If data is insufficient, say so — do not fabricate root causes. + diff --git a/get-shit-done/workflows/forensics.md b/get-shit-done/workflows/forensics.md new file mode 100644 index 000000000..42952cafe --- /dev/null +++ b/get-shit-done/workflows/forensics.md @@ -0,0 +1,258 @@ +# Forensics Workflow + +Post-mortem investigation for failed or stuck GSD workflows. Analyzes git history, +`.planning/` artifacts, and file system state to detect anomalies and generate a +structured diagnostic report. + +**Principle:** This is a read-only investigation. Do not modify project files. +Only write the forensic report. + +--- + +## Step 1: Get Problem Description + +```bash +PROBLEM="$ARGUMENTS" +``` + +If `$ARGUMENTS` is empty, ask the user: +> "What went wrong? Describe the issue — e.g., 'autonomous mode got stuck on phase 3', +> 'execute-phase failed silently', 'costs seem unusually high'." + +Record the problem description for the report. + +## Step 2: Gather Evidence + +Collect data from all available sources. Missing sources are fine — adapt to what exists. + +### 2a. Git History + +```bash +# Recent commits (last 30) +git log --oneline -30 + +# Commits with timestamps for gap analysis +git log --format="%H %ai %s" -30 + +# Files changed in recent commits (detect repeated edits) +git log --name-only --format="" -20 | sort | uniq -c | sort -rn | head -20 + +# Uncommitted work +git status --short +git diff --stat +``` + +Record: +- Commit timeline (dates, messages, frequency) +- Most-edited files (potential stuck-loop indicator) +- Uncommitted changes (potential crash/interruption indicator) + +### 2b. Planning State + +Read these files if they exist: +- `.planning/STATE.md` — current milestone, phase, progress, blockers, last session +- `.planning/ROADMAP.md` — phase list with status +- `.planning/config.json` — workflow configuration + +Extract: +- Current phase and its status +- Last recorded session stop point +- Any blockers or flags + +### 2c. Phase Artifacts + +For each phase directory in `.planning/phases/*/`: + +```bash +ls .planning/phases/*/ +``` + +For each phase, check which artifacts exist: +- `{padded}-PLAN.md` or `{padded}-PLAN-*.md` (execution plans) +- `{padded}-SUMMARY.md` (completion summary) +- `{padded}-VERIFICATION.md` (quality verification) +- `{padded}-CONTEXT.md` (design decisions) +- `{padded}-RESEARCH.md` (pre-planning research) + +Track: which phases have complete artifact sets vs gaps. + +### 2d. Session Reports + +Read `.planning/reports/SESSION_REPORT.md` if it exists — extract last session outcomes, +work completed, token estimates. + +### 2e. Git Worktree State + +```bash +git worktree list +``` + +Check for orphaned worktrees (from crashed agents). + +## Step 3: Detect Anomalies + +Evaluate the gathered evidence against these anomaly patterns: + +### Stuck Loop Detection + +**Signal:** Same file appears in 3+ consecutive commits within a short time window. + +```bash +# Look for files committed repeatedly in sequence +git log --name-only --format="---COMMIT---" -20 +``` + +Parse commit boundaries. If any file appears in 3+ consecutive commits, flag as: +- **Confidence HIGH** if the commit messages are similar (e.g., "fix:", "fix:", "fix:" on same file) +- **Confidence MEDIUM** if the file appears frequently but commit messages vary + +### Missing Artifact Detection + +**Signal:** Phase appears complete (has commits, is past in roadmap) but lacks expected artifacts. + +For each phase that should be complete: +- PLAN.md missing → planning step was skipped +- SUMMARY.md missing → phase was not properly closed +- VERIFICATION.md missing → quality check was skipped + +### Abandoned Work Detection + +**Signal:** Large gap between last commit and current time, with STATE.md showing mid-execution. + +```bash +# Time since last commit +git log -1 --format="%ai" +``` + +If STATE.md shows an active phase but the last commit is >2 hours old and there are +uncommitted changes, flag as potential abandonment or crash. + +### Crash/Interruption Detection + +**Signal:** Uncommitted changes + STATE.md shows mid-execution + orphaned worktrees. + +Combine: +- `git status` shows modified/staged files +- STATE.md has an active execution entry +- `git worktree list` shows worktrees beyond the main one + +### Scope Drift Detection + +**Signal:** Recent commits touch files outside the current phase's expected scope. + +Read the current phase PLAN.md to determine expected file paths. Compare against +files actually modified in recent commits. Flag any files that are clearly outside +the phase's domain. + +### Test Regression Detection + +**Signal:** Commit messages containing "fix test", "revert", or re-commits of test files. + +```bash +git log --oneline -20 | grep -iE "fix test|revert|broken|regression|fail" +``` + +## Step 4: Generate Report + +Create the forensics directory if needed: +```bash +mkdir -p .planning/forensics +``` + +Write to `.planning/forensics/report-$(date +%Y%m%d-%H%M%S).md`: + +```markdown +# Forensic Report + +**Generated:** {ISO timestamp} +**Problem:** {user's description} + +--- + +## Evidence Summary + +### Git Activity +- **Last commit:** {date} — "{message}" +- **Commits (last 30):** {count} +- **Time span:** {earliest} → {latest} +- **Uncommitted changes:** {yes/no — list if yes} +- **Active worktrees:** {count — list if >1} + +### Planning State +- **Current milestone:** {version or "none"} +- **Current phase:** {number — name — status} +- **Last session:** {stopped_at from STATE.md} +- **Blockers:** {any flags from STATE.md} + +### Artifact Completeness +| Phase | PLAN | CONTEXT | RESEARCH | SUMMARY | VERIFICATION | +|-------|------|---------|----------|---------|-------------| +{for each phase: name | ✅/❌ per artifact} + +## Anomalies Detected + +### {Anomaly Type} — {Confidence: HIGH/MEDIUM/LOW} +**Evidence:** {specific commits, files, or state data} +**Interpretation:** {what this likely means} + +{repeat for each anomaly found} + +## Root Cause Hypothesis + +Based on the evidence above, the most likely explanation is: + +{1-3 sentence hypothesis grounded in the anomalies} + +## Recommended Actions + +1. {Specific, actionable remediation step} +2. {Another step if applicable} +3. {Recovery command if applicable — e.g., `/gsd:resume-work`, `/gsd:execute-phase N`} + +--- + +*Report generated by `/gsd:forensics`. All paths redacted for portability.* +``` + +**Redaction rules:** +- Replace absolute paths with relative paths (strip `$HOME` prefix) +- Remove any API keys, tokens, or credentials found in git diff output +- Truncate large diffs to first 50 lines + +## Step 5: Present Report + +Display the full forensic report inline. + +## Step 6: Offer Interactive Investigation + +> "Report saved to `.planning/forensics/report-{timestamp}.md`. +> +> I can dig deeper into any finding. Want me to: +> - Trace a specific anomaly to its root cause? +> - Read specific files referenced in the evidence? +> - Check if a similar issue has been reported before?" + +If the user asks follow-up questions, answer from the evidence already gathered. +Read additional files only if specifically needed. + +## Step 7: Offer Issue Creation + +If actionable anomalies were found (HIGH or MEDIUM confidence): + +> "Want me to create a GitHub issue for this? I'll format the findings and redact paths." + +If confirmed: +```bash +gh issue create \ + --title "bug: {concise description from anomaly}" \ + --label "bug" \ + --body "{formatted findings from report}" +``` + +## Step 8: Update STATE.md + +```bash +gsd-tools.cjs state record-session \ + --stopped-at "Forensic investigation complete" \ + --resume-file ".planning/forensics/report-{timestamp}.md" +``` diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index a08fe43a0..e8d05d63f 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -620,7 +620,7 @@ describe('copyCommandsAsCopilotSkills', () => { // Count gsd-* directories — should be 31 const dirs = fs.readdirSync(tempDir, { withFileTypes: true }) .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.strictEqual(dirs.length, 55, `expected 55 skill folders, got ${dirs.length}`); + assert.strictEqual(dirs.length, 56, `expected 56 skill folders, got ${dirs.length}`); } finally { fs.rmSync(tempDir, { recursive: true }); } @@ -1114,7 +1114,7 @@ const { execFileSync } = require('child_process'); const crypto = require('crypto'); const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); -const EXPECTED_SKILLS = 55; +const EXPECTED_SKILLS = 56; const EXPECTED_AGENTS = 18; function runCopilotInstall(cwd) { diff --git a/tests/forensics.test.cjs b/tests/forensics.test.cjs new file mode 100644 index 000000000..4cbeb22e7 --- /dev/null +++ b/tests/forensics.test.cjs @@ -0,0 +1,258 @@ +/** + * GSD Forensics Tests + * + * Validates the forensics command and workflow files exist, + * follow expected patterns, and cover all anomaly detection types. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const repoRoot = path.resolve(__dirname, '..'); +const commandPath = path.join(repoRoot, 'commands', 'gsd', 'forensics.md'); +const workflowPath = path.join(repoRoot, 'get-shit-done', 'workflows', 'forensics.md'); + +describe('forensics command', () => { + test('command file exists', () => { + assert.ok(fs.existsSync(commandPath), 'commands/gsd/forensics.md should exist'); + }); + + test('command has correct frontmatter', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok(content.includes('name: gsd:forensics'), 'should have correct command name'); + assert.ok(content.includes('type: prompt'), 'should have type: prompt'); + assert.ok(content.includes('argument-hint'), 'should have argument-hint'); + }); + + test('command references workflow in execution_context', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok( + content.includes('workflows/forensics.md'), + 'should reference the forensics workflow' + ); + }); + + test('command has success_criteria section', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok(content.includes(''), 'should have success_criteria'); + }); + + test('command has critical_rules section', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok(content.includes(''), 'should have critical_rules'); + }); + + test('command enforces read-only investigation', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok( + content.toLowerCase().includes('read-only') || content.toLowerCase().includes('do not modify'), + 'should enforce read-only investigation' + ); + }); + + test('command requires evidence-grounded findings', () => { + const content = fs.readFileSync(commandPath, 'utf-8'); + assert.ok( + content.includes('Ground findings') || content.includes('cite specific'), + 'should require evidence-grounded analysis' + ); + }); +}); + +describe('forensics workflow', () => { + test('workflow file exists', () => { + assert.ok(fs.existsSync(workflowPath), 'workflows/forensics.md should exist'); + }); + + test('workflow gathers evidence from all data sources', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + const sources = [ + 'git log', + 'git status', + 'STATE.md', + 'ROADMAP.md', + 'PLAN.md', + 'SUMMARY.md', + 'VERIFICATION.md', + 'SESSION_REPORT', + 'worktree', + ]; + for (const source of sources) { + assert.ok( + content.includes(source), + `workflow should reference data source: ${source}` + ); + } + }); + + test('workflow detects all 6 anomaly types', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + const anomalies = [ + 'Stuck Loop', + 'Missing Artifact', + 'Abandoned Work', + 'Crash', + 'Scope Drift', + 'Test Regression', + ]; + for (const anomaly of anomalies) { + assert.ok( + content.includes(anomaly), + `workflow should detect anomaly: ${anomaly}` + ); + } + }); + + test('workflow writes report to forensics directory', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('.planning/forensics/report-'), + 'should write to .planning/forensics/' + ); + }); + + test('workflow includes redaction rules', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('Redaction') || content.includes('redact'), + 'should include data redaction rules' + ); + }); + + test('workflow offers interactive investigation', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('dig deeper') || content.includes('Interactive'), + 'should offer interactive follow-up' + ); + }); + + test('workflow offers GitHub issue creation', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('gh issue create'), + 'should offer to create GitHub issue from findings' + ); + }); + + test('workflow updates STATE.md', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('state record-session'), + 'should update STATE.md via gsd-tools' + ); + }); + + test('workflow has confidence levels for anomalies', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('HIGH') && content.includes('MEDIUM') && content.includes('LOW'), + 'anomalies should have confidence levels' + ); + }); +}); + +describe('forensics report structure', () => { + test('report template has all required sections', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + const sections = [ + 'Evidence Summary', + 'Git Activity', + 'Planning State', + 'Artifact Completeness', + 'Anomalies Detected', + 'Root Cause Hypothesis', + 'Recommended Actions', + ]; + for (const section of sections) { + assert.ok( + content.includes(section), + `report should include section: "${section}"` + ); + } + }); + + test('report includes artifact completeness table', () => { + const content = fs.readFileSync(workflowPath, 'utf-8'); + assert.ok( + content.includes('PLAN') && content.includes('CONTEXT') && content.includes('RESEARCH') && + content.includes('SUMMARY') && content.includes('VERIFICATION'), + 'artifact table should check all 5 artifact types' + ); + }); +}); + +describe('forensics fixture-based tests', () => { + let tmpDir; + + function setup() { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-forensics-test-')); + } + + function teardown() { + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); + } + + test('detects missing artifacts in phase structure', () => { + setup(); + try { + // Phase 1: complete + const phase1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(phase1, { recursive: true }); + fs.writeFileSync(path.join(phase1, '01-PLAN-A.md'), 'plan'); + fs.writeFileSync(path.join(phase1, '01-SUMMARY.md'), 'summary'); + fs.writeFileSync(path.join(phase1, '01-VERIFICATION.md'), 'verification'); + + // Phase 2: missing SUMMARY and VERIFICATION (anomaly) + const phase2 = path.join(tmpDir, '.planning', 'phases', '02-core'); + fs.mkdirSync(phase2, { recursive: true }); + fs.writeFileSync(path.join(phase2, '02-PLAN-A.md'), 'plan'); + + // Verify detection + const p1Files = fs.readdirSync(phase1); + const p2Files = fs.readdirSync(phase2); + + assert.ok(p1Files.some(f => f.includes('SUMMARY')), 'phase 1 has SUMMARY'); + assert.ok(p1Files.some(f => f.includes('VERIFICATION')), 'phase 1 has VERIFICATION'); + assert.ok(!p2Files.some(f => f.includes('SUMMARY')), 'phase 2 missing SUMMARY (anomaly)'); + assert.ok(!p2Files.some(f => f.includes('VERIFICATION')), 'phase 2 missing VERIFICATION (anomaly)'); + } finally { + teardown(); + } + }); + + test('forensics report directory can be created', () => { + setup(); + try { + const forensicsDir = path.join(tmpDir, '.planning', 'forensics'); + fs.mkdirSync(forensicsDir, { recursive: true }); + const reportPath = path.join(forensicsDir, 'report-20260321-150000.md'); + fs.writeFileSync(reportPath, '# Forensic Report\n'); + + assert.ok(fs.existsSync(reportPath), 'report file should be created'); + const content = fs.readFileSync(reportPath, 'utf-8'); + assert.ok(content.includes('Forensic Report'), 'report should have header'); + } finally { + teardown(); + } + }); + + test('handles project with no .planning directory', () => { + setup(); + try { + // No .planning/ at all + const planningExists = fs.existsSync(path.join(tmpDir, '.planning')); + assert.strictEqual(planningExists, false, 'no .planning/ should exist'); + + // Forensics should still work with git data + const forensicsDir = path.join(tmpDir, '.planning', 'forensics'); + fs.mkdirSync(forensicsDir, { recursive: true }); + assert.ok(fs.existsSync(forensicsDir), 'forensics dir created on demand'); + } finally { + teardown(); + } + }); +});