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();
+ }
+ });
+});