Files
msd-core/docs/how-to/recover-and-troubleshoot.md
Tom Boucher d28ab7c8f7 enhance(#3243): sync installed codex .toml model/effort to the passive posture (#3296)
* feat(#3243): sync installed codex .toml model/effort to the passive posture

Implements ADR-2313 D7, and owns the Codex .toml typed IR that Phase 1's
review assigned to this phase.

The IR exists for a structural reason, not tidiness: this phase has to
PARSE these files, and a parser kept bug-compatible with a separate
renderer is the generative-fix-divergence shape this epic already dealt
with once for the model predicate. So Phase 2's parsing MOVES here
rather than being copied — agent-install-check now imports it, and its
test file passing unchanged is the proof the extraction altered nothing.

The load-bearing property is byte-identical round-trip: render(parse(x))
=== x. Without it a sync silently reformats a user's file — line
endings, key order, BOM, trailing newline — turning a two-line repair
into a whole-file diff in their dotfile repo. The IR keeps original
lines and removes targeted ones rather than reconstructing from parsed
fields, which is what makes that property hold.

It also reconciles a real contradiction between Phase 2 and ADR-2313. An
unterminated developer_instructions block: the reader excludes the rest
of the file, deliberately failing toward a false positive, because
misreading prose as a pin only wastes a user's time. The writer must
refuse, because proceeding on a malformed document rewrites it. A false
positive is the safe direction for a reader and the dangerous one for a
writer. So the parse reports the fact and the two consumers branch on
it — one parse, one truth, two policies, instead of two parsers that
agree today.

The sync leaves a legal real-Codex pin and its coupled effort untouched,
reported skipped rather than synced; strips a stale Anthropic or tier
model and an orphaned effort; keeps dry-run as the default; refuses any
file whose parse fails; and skips symlinks exactly as the Claude path
already did. The Claude path itself is byte-identical.

PARSE_REASON.NO_HEADER from the ADR's illustrative snippet is
deliberately not implemented — a missing header is legal, not an error,
so it would be a dead enum member that the enum-lock test then pins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3243): preserve per-line endings and make the codex write atomic

Two findings from an isolated review, both in the write path.

BLOCKER: mixed line endings broke the byte-identical round-trip. `eol`
was a single whole-file flag and split(/\r?\n/) discarded each line's
own terminator, so render re-joined with ONE style and normalized every
line — even with zero strips performed. A file with one CRLF line and
the rest LF came back fully converted. That falsified the A14 guarantee,
violated the design's "must not silently rewrite every line", and made
the CONTEXT.md glossary claim wrong. It was untested because A12 and B15
only cover PURE CRLF; no mixed-ending fixture existed anywhere.

Fixed by keeping each line's terminator alongside its content, so render
is a plain concatenation and a strip removes only the target line and
its own terminator. `eol` survives as informational metadata that render
never reads. Seven fixtures added for the paths nothing exercised:
mixed endings unmodified and with a strip, a lone \r, a file ending on
the block's closing ''' with no newline, multiple trailing newlines, a
BOM-only file, and an empty file.

MINOR, but it contradicted this phase's own contract: the write was
in-place open-truncate, so a failure between truncate and completion
leaves a truncated .toml — exactly what ADR-2313 says must never happen.
The Codex path now writes a sibling temp file and renames over the
target, which is atomic on one filesystem, with cleanup on failure. It
uses the repo's existing retryRenameSync rather than a hand-rolled
rename, and deliberately NOT platformWriteSync, whose normalizeContent
would mangle the very CRLF and trailing-newline bytes the round-trip
property exists to preserve.

The Claude path keeps its in-place write untouched. It has the same
shape, but changing it is not this phase's business and its tests must
stay byte-identical.

B20 previously mocked writeFileSync to throw BEFORE touching anything,
so it proved nothing about a mid-write failure — its passing comment was
true only because of how the mock was built. It now performs a real
truncated write wherever writeFileSync is called, catching both the
naive direct-to-target path and the new temp path, and asserts the
target is byte-identical afterwards with no stray temp file left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3243): preserve the trailing-newline state when stripping a last line

Caught by B17, one of this phase's own tests — the suite working, not a
test problem.

Content is reconstructed as the concatenation of lines[k] +
terminators[k], so a file with no trailing newline has '' as its last
terminator. removeLine spliced out both arrays at the same index, which
is right for a middle line but wrong for the last one: it dropped the
empty terminator and left the PREVIOUS line's newline in place. A file
ending `...\nmodel = "sonnet"` with no trailing newline came back
as `...\n`, gaining a newline the user never wrote.

The new last line now inherits the removed line's terminator, so a
removal leaves the file exactly as if that line had never been written.
Removing the only line yields an empty file rather than a stray
terminator.

Both stripModel and stripReasoningEffort funnel through the one
removeLine, confirmed rather than assumed, so a single fix covers both —
including the row-B7 shape where a stale model and its orphaned effort
are removed in sequence and the second removal targets the last line.

Two of the four new cases are honestly not red-first and say so in
their comments: removing a last line that HAS a trailing newline only
exposes the bug under mixed EOL, since uniform files coincidentally
have equal terminators on both sides; and removing the only line
already degenerated correctly through Array.slice. They are kept as
guards for the new branch rather than dressed up as catches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#3243): document the codex repair path and close the loop

How-to: the Codex-400 entry added in Phase 2 told users to re-run the
installer, because that was the only repair available then. It now leads
with `effort sync` and keeps the reinstall as the alternative, with the
reason to prefer one — a reinstall regenerates the agent files
wholesale, so anyone who hand-edited theirs loses those edits. Detect,
preview, apply is now one continuous path in one place.

Reference: docs/COMMANDS.md had no `effort sync` entry at all — the same
gap `validate agents` had in Phase 2, found the same way. The entry
documents BOTH runtimes, because the command genuinely forks on runtime
and describing only the new half would misdescribe it.

The write flag is `--apply`. The design doc and test matrix both said
`--no-dry-run` throughout, which does not exist — verified against the
actual arg parser in gsd-tools.cjs before writing. Documenting a flag
that does not exist is worse than documenting nothing, because it fails
at the moment someone needs it.

Both surfaces state that only the targeted lines are removed and every
other byte is preserved. That is a user-visible guarantee rather than an
implementation note: it is the difference between a two-line diff and a
reformatted file in someone's dotfile repo, it is what the IR's
round-trip property exists to deliver, and writing it down makes it a
contract a future change has to break knowingly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3243): inherit the trailing-newline state, not the line ending style

My previous rule was subtly wrong and this phase's own test caught it.

"The new last line inherits the removed line's terminator" copies the
removed line's STYLE as well as its presence. A26 uses mixed endings on
purpose — line one terminated \r\n, the model line terminated \n — so
inheriting silently rewrote line one's ending to \n. That is precisely
the defect class the mixed-EOL blocker fix existed to eliminate,
reintroduced one layer down by the fix for it.

The correct rule inherits the EMPTINESS only. If the removed line had no
terminator, the new last line loses its own, preserving "this file has
no trailing newline". Otherwise the new last line keeps its own
terminator: it is already a newline, and already the right style for
that line.

A26's assertion moved too, and that deserves saying plainly rather than
burying: it previously encoded my wrong rule. Changing a test to match
the implementation is usually the mistake, so it was checked from first
principles instead — a file whose first line ends \r\n and whose last
line ends \n, with that last line removed entirely, must be the first
line with its own \r\n intact. The new expectation is what the user's
file should actually look like; the old one was wrong.

A29 adds the interaction nothing covered: the compounding case (strip a
stale model, then its orphaned effort, the second removal landing on the
last line) with non-uniform endings either side. The two fixes meet
there and nothing exercised the meeting point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3243): drop the phantom trailing line from the IR representation

Root cause, not another patch on the removal rule. Three consecutive
fixes there each surfaced the next issue, which was the signal that the
data model was wrong.

splitPreservingTerminators left a phantom empty final entry for any file
ending in a newline: "a\nb\n" became lines ['a','b','']. So for the
common case the real last content line was NOT the last array element,
removeLine's isLastLine check never matched it, and every rule I gave
was reasoning about the wrong element.

What hid it: render was already a plain concatenation, so a phantom
empty line with an empty terminator contributes nothing to the output.
A14's byte-identical round-trip could never have caught it — the defect
is byte-neutral until a removal shifts the index arithmetic under it.
That is worth recording, because "the round-trip test is green" was
exactly the reassurance that kept the search pointed elsewhere.

The representation is now 1:1 — terminators[i] follows lines[i] and may
be '' — with no phantom, verified across empty, no-trailing-newline,
trailing-newline, blank-line and mixed-CRLF inputs. render stays a plain
concat and needs no special cases. With the phantom gone the removal
rule is correct as stated and finally applies to the genuinely last
element.

Consumers checked rather than assumed: the block-range detector and
header scanner are agnostic to array shape, and Phase 2's reader uses
its own independent split, so tests/agent-install-check.test.cjs is
untouched and still passes unchanged.

One test expectation was wrong and is corrected rather than quietly
adjusted: A18 asserted a 7-element terminators array whose trailing ''
was the phantom itself. It now asserts the six real terminators, which
is what the invariant lines.length === terminators.length requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#3243): backfill changeset pr number (#3296)

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 01:31:15 -04:00

13 KiB
Raw Blame History

How to recover and troubleshoot

Goal: Identify and fix common problems — from lost context and corrupted state to installation failures and permission errors — using a conditional recipe structure.

Prerequisites: GSD Core is installed. For install problems specifically, see Install on your runtime.


Context and session problems

If you have lost track of where you are

/gsd-progress

Reads all state files and tells you exactly where you are and what to do next.

To automatically advance to the correct next step:

/gsd-progress --next

If you are starting a new session and need to restore context

/gsd-resume-work

Restores your full session context from the last handoff, including current phase, planning decisions, and where work stopped.

If quality is dropping during a long session

Clear your context window between major commands:

/clear

Then restore state:

/gsd-resume-work

GSD is designed around fresh contexts. Every subagent already gets a clean 200k window. The main session degrades over time — clearing it and resuming is the correct remedy, not pushing on.

If you want to save context before stopping

/gsd-pause-work

Creates .planning/HANDOFF.json with your current position. Add --report to also write a post-session summary to .planning/reports/:

/gsd-pause-work --report

Planning integrity problems

If .planning/ integrity is uncertain

/gsd-health

Reports status across errors, warnings, and informational notes:

Status Meaning
HEALTHY All expected artefacts present and well-formed
DEGRADED Warnings that should be addressed but work can continue
BROKEN Critical errors that will block execution

Common auto-repairable issues (errors E004, E005; warnings W003, W008):

/gsd-health --repair

This recreates missing STATE.md, resets a corrupt config.json to defaults, and adds any missing configuration keys. It will not overwrite PROJECT.md or ROADMAP.md.

If STATE.md references a phase that does not exist

This produces warning W002. Use the state CLI to diagnose and repair:

node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" state validate

Preview what a sync would change without writing:

node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" state sync --verify

Apply the sync:

node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" state sync

These commands reconstruct STATE.md from actual project state on disk. They replace manual STATE.md editing.

If you see "Project already initialised"

.planning/PROJECT.md already exists. /gsd-new-project is a safety check. If you genuinely want to start over, delete the .planning/ directory first:

rm -rf .planning/

Then re-run /gsd-new-project.

If context-window utilisation is high

/gsd-health --context

Probes the context-window utilisation guard. Warns at 60 %, critical at 70 %. If you are above the warning threshold, run /clear followed by /gsd-resume-work before starting the next major command.


Execution problems

If an executor gets "Permission denied" on Bash commands

GSD's gsd-executor subagents need write-capable Bash access. Add the required patterns to ~/.claude/settings.json under permissions.allow. At minimum:

"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git merge:*)",
"Bash(git checkout:*)"

For stack-specific patterns (Rails, Python, Node, Rust), see the full table in docs/USER-GUIDE.md under "Executor Subagent Gets Permission denied".

Per-project alternative: add the same block to .claude/settings.local.json in your project root.

If execution fails or produces stubs

Check whether the plan is too ambitious. Plans should have two or three tasks at most. If tasks are too large they exceed what a single context window can produce reliably. Re-plan the phase with smaller scope:

/gsd-plan-phase 1

For systematic diagnosis of what went wrong, see Debug a failed execution.

If you see "FATAL: worktree base mismatch" or the exit-42 warning

This happens when your current branch is ahead of the repository's default branch (for example, an unmerged milestone or feature branch). Claude Code forks executor worktrees from origin/HEAD, not your HEAD, so plan files that exist only on your branch are absent inside the worktree.

Since the fix landed, GSD automatically degrades to sequential execution on the main working tree and prints a one-line warning — the phase will complete without any action from you. To restore parallel execution permanently, run:

node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs" worktree set-baseref

For a full explanation and all available options, see Fix the worktree base-mismatch (exit 42) error.

If parallel execution causes build lock errors or pre-commit hook failures

This is caused by multiple agents triggering build tools simultaneously. GSD handles this automatically since v1.26. If you are on an older version, or still seeing contention, disable parallel execution:

/gsd-settings

Set parallelization.enabled to false.

If a subagent appears to fail but commits were made

Check git log before concluding something broke:

git log --oneline -10

A known Claude Code classification bug can report failure while work succeeded. GSD's orchestrators spot-check actual output, but if you see a mismatch, the commits are the ground truth.


Plan and phase problems

If plans seem wrong or misaligned with your intent

Run /gsd-discuss-phase N before planning. Most plan quality issues come from assumptions that CONTEXT.md would have prevented:

/gsd-discuss-phase 1

To see what assumptions GSD is currently making without starting a full session:

/gsd-discuss-phase 3 --assumptions

If you need to change something after execution

Do not re-run /gsd-execute-phase. Use /gsd-quick for targeted fixes:

/gsd-quick "Fix the login button not responding on mobile Safari"

Or use /gsd-verify-work N to systematically identify and fix issues through UAT.

If a command appears frozen at "Spawning…"

Wait. GSD subagents run in a separate context window. Their work is invisible to the parent session while in progress. The liveness note on the spawn line confirms this is expected. Research and planning agents routinely take 1–5 minutes; verification agents can take longer on large phases.

Do not interrupt the session. Killing it discards in-progress subagent work.

If it has been more than 10 minutes, check whether the agent task still shows as active in the Claude Code sidebar.


Workflow state problems

If the workflow seems corrupted or state is inconsistent

/gsd-forensics

Or with a description:

/gsd-forensics "Phase 3 execution stalled after wave 1"

/gsd-forensics runs a post-mortem investigation: git history anomalies, artefact integrity, STATE.md consistency, uncommitted work, and orphaned worktrees. It writes a report to .planning/forensics/ and surfaces recommended remediation steps. It is read-only and never modifies your project files.

If you need to roll back a phase or plan

/gsd-undo --phase 03          # Roll back all commits for phase 3
/gsd-undo --plan 03-02        # Roll back commits for plan 02 of phase 3
/gsd-undo --last 5            # Pick interactively from the 5 most recent GSD commits

/gsd-undo checks dependent phases before reverting and always shows a confirmation gate.


Install and update problems

If GSD is not recognised after install

Restart your runtime. GSD installs slash commands into your runtime's command directory (for example ~/.claude/commands/gsd/). Most runtimes discover new commands only at startup.

If the problem persists, verify the install:

npx @opengsd/gsd-core@latest --claude --local

For runtime-specific install paths and troubleshooting, see Install on your runtime.

If Codex agents fail to spawn with a 400 about an unsupported model

Symptom — a typed agent (gsd-planner, gsd-executor, …) fails to start and the whole plan/execute flow falls back to a generic agent:

400 invalid_request_error: "The 'sonnet' model is not supported when using Codex with a ChatGPT account."

This means an installed ~/.codex/agents/<agent>.toml still pins a model your Codex session cannot serve. Installs made before the passive model posture landed embedded a per-tier model; a ChatGPT-account session exposes only its own model, so the pin fails the request outright (ADR-2313).

Check which agents are affected:

node gsd-tools.cjs validate agents

The codex_posture section reports one violation per offending agent, naming the file and the offending value. Two things it flags:

  • anthropic_flavored_model — the .toml pins a GSD tier alias (opus, sonnet, haiku, fable) or a claude-* id. Codex rejects all of these.
  • orphaned_reasoning_effort — a model_reasoning_effort with no model, which leaves the model following your Codex session while the effort follows GSD.

An empty violations list means every regular .toml in the directory is posture-clean, so the 400 is coming from somewhere else — check that your Codex session itself is healthy.

One thing the check deliberately does not inspect: an agent file that is a symlink is skipped rather than followed, matching how the effort sync treats them. If you symlink your agent configs, verify those targets by hand.

Fix — repair in place, without a reinstall. Preview what would change:

node gsd-tools.cjs effort sync

That is a dry run; it writes nothing. When the reported changes look right, apply them:

node gsd-tools.cjs effort sync --apply

It removes only the offending model / model_reasoning_effort lines. Everything else — your line endings, comments, key order, and any keys you added by hand — is preserved byte-for-byte, so the result is a two-line diff rather than a reformatted file. An explicit real-Codex pin is left alone, and a file it cannot parse is refused and reported rather than partially rewritten.

Or re-run the installer, which rewrites the agent files wholesale. Current versions write no model at all, so agents inherit the session model:

npx @opengsd/gsd-core@latest --codex --global

Prefer the sync if you have hand-edited your .toml files — a reinstall regenerates them.

If you are on an API-key account and genuinely want a pinned model, name a real Codex model id per agent instead — see How to configure model profiles.

The check is read-only. It reports what is wrong and does not edit your files.

If an update overwrote your local changes

Since v1.17, the installer backs up locally modified files to gsd-local-patches/. Reapply your changes:

/gsd-update --reapply

If you cannot update via npm

If npx @opengsd/gsd-core fails due to npm outages or network restrictions, see docs/manual-update.md for a step-by-step manual update procedure that works without npm access.

For routine updates, see Update GSD.


Cost problems

If model costs are too high

Switch to the budget profile:

/gsd-config --profile budget

Disable research and plan-check agents via settings if the domain is familiar:

/gsd-settings

Also audit which MCP servers are enabled. Every enabled MCP server injects its tool schema into every turn. Browser and platform-specific tools can cost 20k+ tokens each. Disable any that the current phase does not need in .claude/settings.json:

{
  "disabledMcpjsonServers": ["playwright", "mac-tools"]
}

Recovery quick reference

Problem Solution
Lost context or new session /gsd-resume-work or /gsd-progress
Don't know what step is next /gsd-progress --next
Phase went wrong /gsd-undo --phase NN, then re-plan
Something broke /gsd-debug "description" (add --diagnose for analysis without fixes)
STATE.md out of sync state validate then state sync
.planning/ integrity uncertain /gsd-health, then /gsd-health --repair
Workflow state seems corrupted /gsd-forensics
Quick targeted fix /gsd-quick
Plan doesn't match your vision /gsd-discuss-phase N then re-plan
Costs running high /gsd-config --profile budget and /gsd-settings to toggle agents off
Update broke local changes /gsd-update --reapply
Want session summary /gsd-pause-work --report
Parallel execution build errors Update GSD or set parallelization.enabled: false
Worktree base mismatch / exit 42 Auto-degraded to sequential (no action needed); run worktree set-baseref to restore parallelism