* feat(#3146): resolve gsd_run so workflows cannot reach a foreign gsd-tools The predecessor package get-shit-done-cc publishes a colliding gsd-tools bin whose phases.clear DELETES where this package's ARCHIVES, and both print success-shaped output against a gitignored .planning/ -- which is how #3129 cost a user 43 phase directories with no error and nothing recoverable from git. The launcher's PATH branch now resolves gsd_run, published only by this package and self-locating via its own symlink chain to the sibling shim, instead of the colliding gsd-tools. A foreign handler becomes unreachable from PATH, and when no gsd_run is reachable the resolver fails closed rather than falling back -- that fallback was the vulnerability. This is smaller than the branch it replaces, which matters: the preamble is inlined into 113 shipped files and agents/gsd-verifier.md sits 2 bytes under a red-line size cap. unset -f gsd_run leads the preamble so a re-source is idempotent. Without it, command -v finds the shell function, returns a bare name, and the resolver falls through to an exit 1 that kills a sourced caller's shell. Adds gsd-tools runtime-identity, a manual diagnostic reporting this runtime's package coordinates over the baked package-identity (#498) and readHostVersion, with a strict total classifier: only a JSON object with an exact packageName verifies, since JSON.parse admits 0/"str"/[]/null/true. An inlined identity assertion was built and reviewed first, then withdrawn -- it breaks five frozen size ceilings and no assertion fits in 2 bytes. Closes #3146 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#3146): stop sync:launcher relocating a deliberate preamble placement Pre-existing defect, surfaced by this PR because sync is a no-op unless the snippet content actually changes. transformFile inserts the preamble into the first block that CALLS gsd_run, but gsd-core/workflows/explore.md deliberately places it in a bootstrap-only block that DEFINES gsd_run without calling it -- its own comment explains why: declining the research offer must not leave Step 5's commit call unbootstrapped. Stripping empties that block of calls, so the preamble migrated forward and broke the define-before-use invariant tests/explore-command.test.cjs pins. Reproduced on a pristine origin/next checkout with the base snippet and base file, so this was not introduced here. The insertion target now honours a block that already carried the preamble, falling back to the first calling block for files that have none yet. Adds a behavioral regression test over a two-block fixture. Also updates three runtime-launcher-parity tests that pinned the removed PATH fallback to gsd-tools. Their intent is preserved -- the PATH stub is renamed gsd_run so it is reachable by the new resolver, and the RUNTIME_DIR-wins test still asserts the stub is never invoked. Fixture shebangs move to an absolute /bin/sh, because the fixture PATH is deliberately restricted and #!/usr/bin/env sh could not resolve. Refs #3146 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#3146): backfill changeset PR number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#3146): document the FEATURES.md section-numbering practice The monotonically increasing section number in docs/FEATURES.md is the most frequent merge-conflict source in this repo, and it has TWO conflict cells, not one: the ### N. heading and the hand-maintained table of contents. Two PRs adding differently numbered features still collide on the TOC, so renumbering alone does not make a branch safe. This branch alone was renumbered 165 -> 166 -> 167 -> 168 across successive rebases. Adds a CONTRIBUTING section stating the practice: allocate the number last, never pre-emptively renumber, take max+1 after a rebase and update the TOC in the same commit, and never renumber someone else's section. Fork contributors are told explicitly they may leave the number to a maintainer at merge rather than chasing the counter. Agents are told to lease the allocation and to include the file in their published touched set. Records the durable fix as planned rather than pretending it exists: FEATURES.md should be generated from per-feature fragments the way CHANGELOG.md is generated from .changeset/, and the way tests/emitted-drift-acks/ works (#2914). Also renumbers this branch's own section to 168, leaving 167 to the PR already in flight. Refs #3146 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
12 KiB
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
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
# 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/*/:
ls .planning/phases/*/
For each phase, check which artifacts exist:
{padded}-PLAN.mdor{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
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.
# 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
Partial-plan Drift Detection
Signal: commits exist but SUMMARY.md is missing for the current or recently active plan.
Run the same comparison as the execute-phase safe-resume verifier: identify the active plan from STATE.md/phase artifacts, search git history for that plan id, then compare against the expected SUMMARY.md path. If production commits exist but SUMMARY.md is missing, flag a high-confidence partial-plan drift anomaly. This usually means an executor was interrupted after implementation commits but before atomic close-out.
Abandoned Work Detection
Signal: Large gap between last commit and current time, with STATE.md showing mid-execution.
# 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 statusshows modified/staged files- STATE.md has an active execution entry
git worktree listshows 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.
git log --oneline -20 | grep -iE "fix test|revert|broken|regression|fail"
Step 4: Generate Report
Create the forensics directory if needed:
mkdir -p .planning/forensics
Write to .planning/forensics/report-$(date +%Y%m%d-%H%M%S).md:
# 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
$HOMEprefix) - 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:
# Check if "bug" label exists before using it
BUG_LABEL=$(gh label list --repo open-gsd/gsd-core --search "bug" --json name -q '.[0].name' 2>/dev/null)
LABEL_FLAG=""
if [ -n "$BUG_LABEL" ]; then
LABEL_FLAG="--label bug"
fi
gh issue create \
--repo open-gsd/gsd-core \
--title "bug: {concise description from anomaly}" \
$LABEL_FLAG \
--body "{formatted findings from report}"
Step 8: Update STATE.md
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd_run is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
gsd_run query state.record-session \
--stopped-at "Forensic investigation complete" \
--resume-file ".planning/forensics/report-{timestamp}.md"