Commit graph

2382 commits

Author SHA1 Message Date
Tom Boucher
cbb9c2edd9 fix: detect project relocation and recover state without data loss (#3080)
* fix: detect project relocation and recover state without data loss

For repos with a remote URL, compute identity as SHA256(remoteUrl) only,
dropping the git root path from the hash. This makes the identity stable
across directory moves/renames -- the most common cause of silent data loss.

For local-only repos, write a .gsd-id marker file in the project root that
records the identity hash. After a move, ensureGsdSymlink reads the marker,
finds the orphaned state directory, and migrates data to the new identity
path automatically.

Also handles the upgrade migration: when an existing .gsd symlink points
to a valid state dir under the old hash format, data is transparently
migrated to the new remote-only hash path.

Closes #2750

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

* fix: handle existing symlink in project-relocation recovery test

Add defensive unlinkSync calls before symlinkSync in ensureGsdSymlinkCore
to prevent EEXIST race conditions when a dangling or residual symlink
exists at the .gsd path during project relocation recovery.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:39:50 -06:00
Tom Boucher
6ac30ae9d9 fix: add free-text input to ask-user-questions when "None of the above" is selected (#3081)
Fixes #2715

The ask-user-questions UI trapped users in a re-asking loop when the agent
needed a free-text explanation rather than a fixed choice. Two paths were
affected:

- RPC fallback (ctx.ui.select): selecting "None of the above" recorded
  the label but never prompted for a free-text explanation. Now follows up
  with ctx.ui.input() so the user can type their answer.

- Custom interview UI: selecting "None of the above" advanced to the next
  question without opening the notes editor. Now auto-focuses the notes
  field so the user can immediately type a free-text response.

Also updated the "None of the above" description from "Press TAB to add
optional notes." to "Select to type your own answer." for discoverability.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:39:35 -06:00
Tom Boucher
a09ae1de26 fix: block work execution during /gsd queue mode (#2545) (#3082)
When /gsd queue was active, the agent had unrestricted access to all
tools and would execute described work instead of creating milestones.
The queue prompt instructed milestone-only behavior, but the system
prompt's "execute with full commitment" directive dominated.

Add a mechanical tool gate (shouldBlockQueueExecution) that blocks
write/edit to non-.gsd/ paths and mutating bash commands when queue
phase is active. Read-only tools, discussion tools, and .gsd/ artifact
writes remain allowed. This enforces the queue contract at the tool
layer rather than relying solely on prompt compliance.

Closes #2545

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:39:26 -06:00
Tom Boucher
eb40f74cfe fix: detect worktree basePath in gsdRoot() to prevent escaping to project root (#3083)
When gsdRoot() is called with a basePath inside .gsd/worktrees/<name>/,
the git-root probe and walk-up logic can escape to the project root's .gsd
directory. This causes ensurePreconditions() to create slice directories
in the wrong location and deriveState() to read stale project-root state
instead of worktree-local state.

Add isInsideGsdWorktree() guard that detects the .gsd/worktrees/<name>/
pattern in the basePath before the git rev-parse probe runs. When detected,
return the worktree-local .gsd path immediately. Also check the
symlink-resolved path for the pattern (handles macOS /tmp -> /private/tmp).

Closes #2594

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:39:15 -06:00
Tom Boucher
a9d881ad8c fix: invalidate stale quick-task captures across milestone boundaries (#3084)
Closes #2872

Quick-task captures resolved in a prior milestone were re-executed in
subsequent sessions because loadActionableCaptures() used the Executed
flag as its sole staleness gate.  Milestone completion never marked
captures as executed, so captures whose issues were fixed by planned
work remained permanently actionable.

Three changes fix this:

1. Track which milestone a capture was resolved in (new **Milestone:**
   field in CAPTURES.md, written by markCaptureResolved and the triage
   prompt).  loadActionableCaptures() now accepts an optional
   currentMilestoneId and excludes captures from prior milestones.

2. Add a verification step to buildQuickTaskPrompt() instructing the
   agent to confirm the issue still exists before making changes.

3. Add stampCaptureMilestone() as a reconciliation safety net --
   executeTriageResolutions() stamps actionable captures that are
   missing the Milestone field with the current milestone ID.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:39:07 -06:00
Tom Boucher
6e22a20580 fix: defer model validation until after extensions register (#3089)
* fix: defer model validation until after extensions register (#2626)

Extension-provided models (e.g. claude-code/claude-sonnet-4-6) were
silently overwritten on every startup because the model validation ran
before createAgentSession(), which is where extensions register their
models in the ModelRegistry. At validation time, extension models did
not exist in the registry, so the user's valid choice was replaced
with a built-in fallback.

Extract validation into validateConfiguredModel() and call it after
createAgentSession() in both print-mode and interactive-mode paths.

Closes #2626

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

* fix: align MinimalSettingsManager interface with SettingsManager

The MinimalSettingsManager interface used `string` for thinking level
types, but SettingsManager uses a specific union type and returns
`undefined`. This caused TS2345 at cli.ts lines 448 and 587.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:38:10 -06:00
Tom Boucher
2f3ffbfc10 fix: repair YAML bullet lists in malformed tool-call JSON (#3090)
* fix: repair YAML bullet lists in malformed tool-call JSON (#2660)

When LLMs copy YAML template formatting into tool-call arguments, they
produce `"key": - item` instead of `"key": ["item"]`, causing JSON parse
errors that block milestone completion. Add a repairToolJson() utility
that detects and converts YAML-style bullet lists into JSON arrays before
parsing. Integrated into both the PartialMessageBuilder (claude-code-cli)
and the anthropic-shared streaming provider, with fallback in
parseStreamingJson for all other providers.

Closes #2660

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

* fix: use .js import extension in repair-tool-json test

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:37:09 -06:00
Tom Boucher
c924f9f1f8 fix: unify SUMMARY.md render paths for projection fidelity (#3091)
* fix: unify SUMMARY.md render paths for projection fidelity

Closes #2720

renderSummaryMarkdown (complete-task.ts) and renderSummaryContent
(workflow-projections.ts) produced structurally different output for the
same data — different frontmatter format, different sections, different
formatting. Deleting a SUMMARY.md and regenerating it via projection
yielded a different file than the original.

Fix: make renderSummaryContent the single source of truth. complete-task
now builds a TaskRow from params and delegates to renderSummaryContent.
The projection renderer passes verification evidence from the DB so
both paths produce identical output including the Verification Evidence
table, Files Created/Modified section, and YAML-format frontmatter.

Added getVerificationEvidence() to gsd-db for projection-time evidence
retrieval, and a 22-assertion parity test that prevents future drift.

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

* fix: safe type assertion for verification evidence query result

Cast through `unknown` to satisfy TS2352 — better-sqlite3's `.all()`
returns `Record<string, unknown>[]` which doesn't directly overlap with
`VerificationEvidenceRow[]`.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:36:37 -06:00
Tom Boucher
3e78270cad fix: chat mode misrepresents terminal output, looks stuck, omits user messages (#3092)
Three root causes addressed:

1. PtyChatParser: user input echoed after a bare prompt line (e.g. "❯ \n"
   followed by "hello\n") was misclassified as assistant content. Added
   _awaitingInput flag that flips true on prompt boundary and classifies the
   next content line as role=user.

2. Chat mode "looks stuck": when the session is idle (connected, not
   streaming, has timeline content), no visual cue indicated GSD was waiting
   for input. Added a "Ready for your input" indicator with a pulsing dot.

3. Transcript overflow misalignment: chatUserMessages was not trimmed when
   liveTranscript/completedTurnSegments overflowed MAX_TRANSCRIPT_BLOCKS,
   causing index-based interleaving to pair user messages with wrong
   assistant responses.

Also exposed isAwaitingInput() on PtyChatParser so chat UIs can query
whether the session is waiting for user input, and widened the > and $
prompt marker regexes to match bare prompts after trimEnd strips trailing
whitespace.

Closes #2707

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:36:21 -06:00
Tom Boucher
4327b4bb3f fix: resolve 4 state corruption bugs in milestone/slice completion (#2945) (#3093)
* fix: resolve 4 state corruption bugs in milestone/slice completion workflow

Closes #2945

Bug 1 - ROADMAP corrupted by inline UAT content:
renderRoadmapContent and renderPlanContent used slice.full_uat_md as a
fallback when the demo field was empty. This injected multi-line UAT
content (preconditions, steps, expected results) into table cells,
corrupting the markdown table and making subsequent slices invisible to
the parser. Fix: use "TBD" fallback instead of full_uat_md.

Bug 2 - complete-milestone accepts pending slices via event replay:
workflow-reconcile's replayEvents blindly called updateSliceStatus("done")
for complete_slice events without validating that all tasks in the slice
were actually complete. During API overload or partial execution, this
allowed slices with pending tasks to be marked done, which then let
complete-milestone succeed. Fix: extract replaySliceComplete function that
validates task completion before updating slice status.

Bug 3 - Worktree directory not cleaned up after merge:
WorktreeResolver._mergeWorktreeMode delegated worktree cleanup to
mergeMilestoneToMain's internal best-effort removeWorktree call, which
can silently fail. Fix: add secondary teardownAutoWorktree call after
successful merge to ensure cleanup.

Bug 4 - Quality gate records not written by validate-milestone:
handleValidateMilestone wrote to the assessments table and rendered
VALIDATION.md to disk, but never persisted quality_gates records in the
DB. Fix: insert milestone-level quality gates (MV01-MV04) alongside the
assessment record. Extended GateScope and GateId types to support
milestone-level validation.

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

* fix: align test type literals with MilestoneRow, SliceRow, and AutoSession

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

* fix: update tests for removed full_summary_md fallback and FK constraints

- workflow-projections: renderPlanContent test now expects TBD fallback
  instead of full_summary_md (removed in #2945 to prevent corruption)
- validate-milestone: insert slice rows before validation so
  quality_gates FK constraint (milestone_id, slice_id) is satisfied
- worktree-resolver: update teardownAutoWorktree assertion from 0 to 1
  to account for secondary cleanup added in #2945 Bug 3

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:36:07 -06:00
Tom Boucher
a02b140f61 fix: isolate guided-flow session state and key discussion milestone queries (#2985) (#3094)
* fix: resolve 4 correctness bugs in GSD extension core (#2985)

Bug 1 — preferences.ts process.cwd() side-channel:
  loadEffectiveGSDPreferences() and loadProjectGSDPreferences() now accept
  an optional projectRoot parameter.  When provided, preferences are loaded
  from the specified project directory instead of relying on process.cwd().
  All 37+ callers continue to work unchanged (parameter defaults to cwd).

Bug 2 — state.ts DB writes inside read functions (CQS violation):
  Extracted disk-to-DB milestone reconciliation into a new exported function
  reconcileDiskMilestonesToDb().  deriveState() and deriveStateFromDb() no
  longer write to the DB as a side effect of reading state.  Callers that
  need reconciliation (auto-start.ts, guided-flow.ts, register-hooks.ts)
  now call it explicitly before reading state.

Bug 3 — guided-flow.ts module-level session state:
  Converted pendingAutoStart from a module-level singleton to a Map keyed
  by basePath.  Concurrent discuss sessions for different projects are now
  independent — the second session no longer silently overwrites the first.

Bug 4 — getDiscussionMilestoneId() unkeyed query:
  getDiscussionMilestoneId() now accepts an optional basePath parameter for
  keyed lookup.  When multiple sessions exist and no basePath is provided,
  it returns null instead of an arbitrary entry.  Single-session backward
  compatibility is preserved.

Closes #2985

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

* refactor: narrow scope to complement igouss PRs #2986 and #2987

Revert changes to preferences.ts (Bug 1), state.ts, auto-start.ts,
register-hooks.ts (Bug 2), and their test files. Those fixes are
covered by @igouss in PRs #2986 and #2987.

This PR now only contains:
- Bug 3: guided-flow.ts pendingAutoStart singleton → Map (session isolation)
- Bug 4: getDiscussionMilestoneId() keyed by basePath
- Supporting unitType additions in preferences-models.ts

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

* fix: align source code with test expectations after scope narrowing

The refactor commit (6972c97c) reverted source changes to state.ts,
preferences.ts, and auto-start.ts but left their corresponding test
assertions in place, causing 8 CI failures:

- isValidationTerminal: treat any extracted verdict as terminal (#2769)
- parseHeadingListFormat: handle raw YAML blocks under headings (#2794)
- bootstrapAutoSession: snapshot ctx.model before guided-flow (#2829)

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

* fix: open project DB before initial deriveState on cold bootstrap (#2841)

When auto-mode starts cold (no prior DB handle), deriveState silently
falls back to markdown-only data for DB-backed helpers (queue-order,
task status), producing stale or incomplete state.  Add
openProjectDbIfPresent() helper that resolves the project-root DB path
and opens it before the first deriveState call, ensuring full data
visibility from the start.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:33:30 -06:00
Tom Boucher
45bd2572ac fix(guided-flow): route dispatchWorkflow through dynamic routing pipeline (#3153)
Closes #2958

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:33:23 -06:00
Tom Boucher
46dff43e21 fix: skip external state migration inside git worktrees (#2970) (#3227)
Add isInsideWorktree() guard at the top of migrateToExternalState() so
migration never runs when basePath is a git worktree. Worktrees share
the same repoIdentity hash as the main repo, so migration would create
a junction to the wrong target and orphan .gsd.migrating.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:33:16 -06:00
Tom Boucher
fb2ef25250 fix: coerce non-numeric strings in DB columns during manifest serialization (#2962) (#3229)
SQLite can store string placeholders like "-", "N/A", or "" in INTEGER
columns after schema migrations or manual inserts. snapshotState() was
passing these through as-is via type assertions, producing JSON that
fails to parse on round-trip. Add toNumeric() helper and apply it to
all numeric columns (exit_code, duration_ms, sequence, seq).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:32:58 -06:00
Tom Boucher
9cebf19559 fix: route allDiscussed and zero-slices paths to queued milestone discussion (#3150) (#3230)
The allDiscussed early-return and pendingSlices.length===0 guard in
showDiscuss() both hard-returned without checking for queued milestones,
blocking users from discussing pending milestones when the active
milestone's slices were all discussed or complete. Now both paths check
for pending milestones and route to showDiscussQueuedMilestone() first.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:32:49 -06:00
Tom Boucher
8b26ec7803 fix: use loose equality for null checks in secure_env_collect (#2997) (#3231)
When ctx.ui.custom() returns undefined instead of null, strict equality
checks (=== null / !== null) let the undefined value pass through to
writeEnvKey, which crashes on .replace(). Switch to loose equality
(== null / != null) in both the manifest orchestrator and extension
execute paths so both null and undefined are treated as "skipped".
Also add a type guard in writeEnvKey for defense in depth.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:32:40 -06:00
Tom Boucher
872b70bf71 fix: prevent prompt explosion from $' in template replacement values (#2968) (#3232)
Replace `replaceAll` with `split/join` in loadPrompt to avoid
JavaScript's special replacement patterns ($', $`, $&) being
interpreted in variable values. This caused exponential prompt
expansion when values contained patterns like `grep -q '^0$'`.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:32:33 -06:00
Tom Boucher
d41ce60530 fix: resolve OAuth API key in buildMemoryLLMCall via modelRegistry (#2959) (#3233)
buildMemoryLLMCall called completeSimple without passing an API key,
which routed to streamSimpleAnthropic -> getEnvApiKey (env vars only).
OAuth users (Claude Max/Pro) store tokens in auth.json, so getEnvApiKey
returned undefined, the call threw, and memory extraction silently failed.

Now resolves the key eagerly via ctx.modelRegistry.getApiKey() which
checks auth.json through authStorage, matching how streamAnthropic and
the compaction orchestrator resolve credentials.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:32:22 -06:00
Tom Boucher
dae746b905 fix(forensics): read completion status from DB instead of legacy file (#3129) (#3234)
The forensics command showed "Completed Keys: 0" because it read from
completed-units.json, which is never populated during normal auto-mode
completion. Now queries the DB (milestones/slices/tasks tables) for
authoritative completion counts, falling back to the legacy file only
when DB is unavailable. Also fixes STATE.md showing "Active Milestone"
for completed milestones — now shows "Last Completed Milestone" when
phase is complete.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:31:57 -06:00
Tom Boucher
dff73009c8 fix: use camelCase parameter names in execute-task and complete-slice prompts (#2933) (#3236)
The prompts told the LLM to pass snake_case params (milestone_id, slice_id,
task_id) but the TypeBox schemas expect camelCase (milestoneId, sliceId,
taskId), causing "Missing named parameter" validation errors.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:31:38 -06:00
Tom Boucher
4a82bc01dc fix: check bootstrap completeness in init wizard gate, not just .gsd/ existence (#2942) (#3237)
A zombie .gsd/ state (symlink exists but missing PREFERENCES.md and
milestones/) caused the init wizard to be skipped entirely, resulting in
an uninitialized project session.

- guided-flow.ts: Replace bare `!existsSync(gsdRoot(basePath))` with a
  compound check for PREFERENCES.md or milestones/ bootstrap artifacts
- auto-start.ts: Check milestones/ path directly instead of .gsd/ which
  ensureGsdSymlink already created (was dead code)
- Add zombie-gsd-state.test.ts verifying both fixes

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:31:10 -06:00
Tom Boucher
0cedaf5fb9 fix: specify write tool for PROJECT.md in milestone/slice prompts (#3238)
* fix: specify write tool for PROJECT.md in complete-milestone/slice prompts (#2946)

The prompts for complete-milestone step 11 and complete-slice step 13
gave ambiguous instructions to "update PROJECT.md" without naming which
tool to use. This caused LLMs to call `edit` with only `newText`,
missing the required `path` and `oldText` parameters. Now both prompts
explicitly instruct the LLM to use the `write` tool with the correct
parameters, since PROJECT.md updates are full-document refreshes.

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

* fix: preserve 'refresh current state if needed' phrase in complete-slice step 13

The PR's rewrite of step 13 to specify the write tool accidentally
removed the phrase that an existing test asserts on. Re-add it while
keeping the explicit write-tool instruction.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:31:04 -06:00
Tom Boucher
3d0eb32756 fix: widen completing-milestone gate to accept "None required" and similar phrasings (#2931) (#3239)
The verification_operational gate used exact equality against "none",
causing a permanent dispatch-stop loop when the planning agent wrote
variants like "None required", "N/A", or "Not applicable". Extract
an isVerificationNotApplicable() helper with a regex that covers
common not-applicable phrasings and use it in the gate condition.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:30:57 -06:00
Tom Boucher
aa6cde32d9 fix: prevent ask_user_questions from poisoning auto-mode dispatch (#2936) (#3240)
Add autonomous execution guard to plan-slice, execute-task, and
complete-slice prompts prohibiting ask_user_questions and
secure_env_collect in auto-mode. When the LLM called these interactive
tools mid-unit, the queued user response caused the subsequent
gsd_plan_slice/gsd_complete_task call to fail with "Skipped due to
queued user message", leading to artifact verification failure and
stuck-loop re-dispatch 2-4x.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:30:43 -06:00
Tom Boucher
493bf3cd6b fix: guard null s.currentUnit in runUnitPhase closeout after stopAuto race (#2939) (#3241)
When the user stops auto-mode mid-unit, stopAuto() resolves the unit
promise and then resets s.currentUnit to null. The resumed runUnitPhase()
coroutine then hits s.currentUnit.startedAt on the closeout line and
throws a TypeError, producing a spurious "Iteration error" warning.

Fix: wrap the closeoutUnit call in an `if (s.currentUnit)` guard
(matching the existing pattern at lines 136 and 344), and switch
remaining accesses to optional chaining.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:30:29 -06:00
Tom Boucher
3a0cc5e849 fix: replace web_search with search-the-web in prompts and agent frontmatter (#2920) (#3245)
Prompt files referenced `web_search` (an Anthropic API implementation
detail) instead of the registered GSD tool name `search-the-web`, causing
Invalid tool input errors when the model attempted to call the wrong tool.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:30:01 -06:00
Tom Boucher
fc813bc657 fix: preserve milestone title in upsertMilestonePlanning when DB row pre-exists (#2879) (#3247)
State reconciliation inserts milestone rows with empty titles via INSERT
OR IGNORE. When gsd_plan_milestone later calls upsertMilestonePlanning,
the UPDATE statement did not include the title column, so it stayed empty
permanently. Add title as a COALESCE-guarded column in the UPDATE and
pass it from the plan-milestone handler.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:29:50 -06:00
Tom Boucher
c602b244c6 fix: invalidate stale milestone validation on roadmap reassessment (#2957) (#3242)
When roadmap reassessment adds/modifies/removes slices after a
needs-remediation validation verdict, the prior milestone-validation
DB row and VALIDATION.md file are now cleared. This forces
deriveState() to return phase: 'validating-milestone' once the new
slices complete, instead of dead-ending at 'completing-milestone'
with a stale verdict.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:29:39 -06:00
Tom Boucher
6a8f33f49c fix(discuss): add roadmap fallback when DB is open but empty (#2892) (#3244)
When gsd.db is truncated to 0 bytes after a crash, getMilestoneSlices()
returns [] even though isDbAvailable() is true. This caused showDiscuss()
to falsely report "All slices are complete" despite incomplete slices
existing in the ROADMAP file. Add a cross-check: if the DB returns zero
slices but a roadmap exists, fall back to parseRoadmapSlices() to derive
slice state from the roadmap (the ground truth).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:29:31 -06:00
Tom Boucher
c31b03d57c fix: integrate Codex & Gemini CLI into provider routes and rate-limit handling (#2922) (#3246)
Extend PROVIDER_ROUTES so doctor/routing recognizes google-gemini-cli
as an alternative for google and openai-codex as an alternative for
openai. Cap rate-limit backoff at 30s for CLI-style providers to avoid
leaving users stuck in long backoff windows.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:23:26 -06:00
Tom Boucher
06d56e5f03 fix(error-classifier): widen STREAM_RE to cover all 7 V8 JSON parse error variants (#2916) (#3243)
Stream-truncation JSON parse errors like "Expected ',' or '}' after
property value in JSON" were falling through to kind: "unknown", causing
permanent auto-mode pause instead of transient 15s backoff.

- Broaden STREAM_RE: replace narrow "Expected double-quoted property name"
  with "Expected.*in JSON" and add "Unterminated.*in JSON" to catch all 7
  V8 JSON parse error message variants
- Move stream check before server/connection checks to prevent false
  matches (e.g. "position 500" matching SERVER_RE, "Unterminated" matching
  CONNECTION_RE's "terminated" pattern)
- Add 4 test cases for the previously uncovered V8 error variants

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:17:25 -06:00
Tom Boucher
c1c185c3b9 fix: prevent git stash from destroying queued milestone CONTEXT files (#2505) (#3273)
The pre-merge stash in mergeMilestoneToMain used --include-untracked
which swept ALL untracked files into the stash, including queued
milestone CONTEXT files under .gsd/milestones/. If stash pop failed,
these files were permanently trapped in the stash entry.

Two-part fix:
1. Add pathspec exclusion `:(exclude).gsd/milestones` to stash push
   so queued milestone dirs are never swept into the stash
2. Shelter queued milestone dirs before squash merge to prevent
   conflicts with copies in the milestone branch (via copyPlanningArtifacts),
   then restore them on both success and error paths

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:17:16 -06:00
Tom Boucher
873a79f484 fix: skip staleness rebuild in npm tarball installs (#2877) (#3250)
The ensure-workspace-builds.cjs postinstall script falsely detected
workspace packages as stale in npm tarball installs. npm sets all
tarball entries to a canonical timestamp (Oct 26 1985), but extraction
ordering causes src/ files to appear 1-2 seconds newer than dist/
files. This triggered a rebuild attempt that either failed silently
(no tsc available) or — when tsc was globally installed — could
produce broken dist/ output, corrupting the known-good pre-built
files and causing the DefaultResourceLoader export error on startup.

The fix gates the src-vs-dist staleness check behind a .git directory
check: only development clones (with .git/) perform the timestamp
comparison. npm tarball installs (no .git/) only check for missing
dist/index.js, which is the safe and correct behavior.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: TÂCHES <afromanguy@me.com>
2026-03-30 14:17:02 -06:00
Tom Boucher
16d31f2253 fix(parallel): check worktree DB for milestone completion in merge (#2812) (#3256)
determineMergeOrder relied solely on orchestrator WorkerInfo.state
being "stopped" to find mergeable milestones. When the orchestrator
state drifts (worker respawned, status.json deleted, etc.), completed
milestones become invisible to the merge command.

Now scans .gsd/worktrees/<MID>/.gsd/gsd.db for milestones with
status='complete', using the same subprocess-sqlite3 pattern as the
parallel-monitor-overlay. The worktree DB is the ground truth.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:52:21 -06:00
Tom Boucher
348c399c9d fix: make claude-code provider stateful with full context and sidechain events (#2859) (#3254)
The claude-code provider in gsd-pi was effectively stateless: it sent
only the last user message, disabled session persistence, and filtered
out all sidechain/subagent events. This made multi-turn conversations
feel isolated and caused incomplete responses.

- Replace extractLastUserPrompt with buildPromptFromContext that
  serialises the full conversation history (system prompt + all turns)
- Change persistSession from false to true for session continuity
- Remove parent_tool_use_id filtering so delegated/sidechain outputs
  are included in the final response
- Extract buildSdkOptions for testability

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:52:14 -06:00
Tom Boucher
a7388f795b fix(worktree): preserve non-empty gsd.db during sync to prevent truncation (#2815) (#3255)
syncProjectRootToWorktree unconditionally deleted the worktree's gsd.db
to force a rebuild from synced artifacts (#853). On respawned workers,
gsd-migrate had already populated the DB (~1.7MB), so the deletion
caused openDatabase to create a new empty file, leading to "no such
table: slices" failures and a respawn loop.

Now only deletes 0-byte (empty/corrupt) DB files, preserving freshly
migrated databases.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:52:06 -06:00
Tom Boucher
abd9be24e9 fix: align @gsd/native module type with compiled output (#3253)
* fix: align @gsd/native module type with compiled output (#2861)

The package declared "type": "module" and used "import"-only export
conditions, but the addon loader used import.meta.url which is
incompatible when the parent package enforces ESM resolution on
Node.js v24. Switch to "type": "commonjs" with "default" export
conditions and remove the import.meta.url/__dirname shim (CJS
provides both natively).

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

* fix: restore dual CJS/ESM compat for native addon loader

The ESM-to-CJS conversion removed import.meta.url polyfills, but the CI
test loader (dist-redirect.mjs) transpiles this file to ESM via
ts.transpileModule — making __dirname and require unavailable at test time.

Add runtime typeof guards that use the CJS globals when available (compiled
output) and fall back to import.meta.url in ESM (test runner). Use
@ts-expect-error to suppress TS1470 for the import.meta branches that are
unreachable in the compiled CJS output.

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

* fix: use indirect eval for import.meta.url to avoid CJS parse-time error

import.meta is a parse-time syntax error in CJS — typeof guards don't
help because Node.js rejects the syntax before executing any code.
Wrapping in new Function("return import.meta.url") hides the syntax
from the CJS parser while still working when executed as ESM (test runner).

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

* fix: replace new Function(import.meta.url) with loader-injected CJS globals

import.meta is static syntax unavailable in new Function() and eval()
scopes, causing rtk-portability CI failures across all platforms.

Instead of trying to access import.meta.url indirectly, the test loader
(dist-redirect.mjs) now injects __dirname, __filename, and require as a
preamble when transpiling workspace packages to ESM. This lets native.ts
use __dirname/require directly in both CJS (production) and ESM (CI test)
contexts without any import.meta.url fallback.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:51:57 -06:00
Tom Boucher
8917cc75be fix: parse hook/* completed-unit keys correctly in forensics + doctor (#2826) (#3252)
The `key.indexOf("/")` split broke compound hook types like
"hook/telegram-progress/M007/S01", yielding unitType="hook" instead of
"hook/telegram-progress". This bypassed the `startsWith("hook/")` guard
in verifyExpectedArtifact, producing false-positive missing-artifact
errors for every hook unit.

Extract a shared `splitCompletedKey()` helper that handles the two-segment
hook prefix and use it in both `detectMissingArtifacts` (forensics.ts) and
the orphaned-key check (doctor-runtime-checks.ts).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:51:45 -06:00
Tom Boucher
01d7200e7b fix: copy mcp.json into auto-mode worktrees (#2791) (#3251)
Add mcp.json to ROOT_STATE_FILES and copyPlanningArtifacts so MCP
server configurations are available inside worktrees.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:51:37 -06:00
Tom Boucher
01aea09f74 fix: add gsd_requirement_save and upsert path for requirement updates (#3249)
* fix: add gsd_requirement_save tool and upsert path for gsd_requirement_update (#2919)

gsd_requirement_update returned not_found for all requirements because
requirements written to REQUIREMENTS.md were never inserted into the DB,
and no create path existed. This adds:

- saveRequirementToDb() + nextRequirementId() in db-writer.ts (symmetric
  to saveDecisionToDb/nextDecisionId)
- gsd_requirement_save tool in db-tools.ts with auto-assigned IDs
- Upsert behavior in updateRequirementInDb() — creates a skeleton row
  when the requirement ID is not in the DB instead of throwing

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

* fix: add null check before reverting requirement on disk write failure

The `existing` variable from `getRequirementById` can be null when the
requirement was newly created (not previously in DB). Guard the revert
call to avoid passing null to `upsertRequirement`.

Fixes TypeScript error: 'Requirement | null' is not assignable to 'Requirement'

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:51:26 -06:00
Tom Boucher
2ea668ee09 fix: handle pause_turn stop reason to prevent 400 errors with native web search (#2869) (#3248)
Map pause_turn to "pauseTurn" instead of "stop" so the agent loop
continues when Anthropic's server pauses a long-running turn (e.g.
native web search hitting its iteration limit). Previously the
incomplete server_tool_use block was saved to history, causing a
400 invalid_request_error on the next API call.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:51:18 -06:00
Tom Boucher
341a211be2 fix: use authoritative milestone status in web roadmap (#2807) (#3258)
* fix: use authoritative milestone status in web roadmap instead of slice heuristics (#2807)

The roadmap view was deriving milestone status from slice completion
flags, which disagrees with the actual GSD state model when milestones
have lifecycle states (complete/active/pending/parked) or validation
verdicts that differ from what slice progress implies.

Add status and validationVerdict fields to WorkspaceMilestoneTarget,
populate them from the state registry and VALIDATION files, and update
getMilestoneStatus() to prefer the authoritative status with a fallback
to the old heuristic for backward compatibility.

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

* fix: add .js import extension and slice type annotations in workspace-status

Fixes TS2835 (missing .js extension for NodeNext resolution) and TS7006
(implicit any on slice callback parameters) that caused CI build failure.

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

* fix: extract workspace types to .ts file to avoid jsx resolution error

Move WorkspaceTaskTarget, WorkspaceSliceTarget, WorkspaceMilestoneTarget,
and RiskLevel to workspace-types.ts so that workspace-status.ts (a plain
.ts file) can import them without requiring --jsx. The .tsx store file
re-exports the types for backward compatibility.

Fixes TS6142 in CI for PR #3258.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:50:57 -06:00
Tom Boucher
a725fa2d9d fix: classify long-context entitlement 429 as quota_exhausted, not rate_limit (#2803) (#3257)
The "Extra usage is required for long context requests" error from
Anthropic is a billing gate, not a transient rate limit. Classify it as
quota_exhausted so the handler enters the fallback path instead of an
infinite backoff loop. When no cross-provider fallback exists, attempt a
[1m] to base model downgrade before stopping cleanly.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:50:36 -06:00
Tom Boucher
50fbd0a837 fix(docs): use ~/.pi/agent/extensions/ for community extension install path (#3131) (#3259)
Community extensions must be placed in ~/.pi/agent/extensions/, not
~/.gsd/agent/extensions/ which is reserved for bundled extensions synced
from the gsd-pi package. Extensions placed in the wrong path are silently
ignored by the loader.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:50:25 -06:00
Tom Boucher
d25dbb15fd fix: add disk→DB slice reconciliation in deriveStateFromDb (#2533) (#3262)
Slices defined in ROADMAP.md but missing from the SQLite database caused
permanent "No slice eligible — check dependency ordering" blocks. The
dependency resolver only considered DB rows, so disk-only slices were
invisible. This adds a reconciliation step (mirroring the existing
milestone reconciliation) that parses each milestone's ROADMAP.md,
compares against getMilestoneSlices(), and inserts missing slices with
correct status based on SUMMARY file presence.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:50:13 -06:00
Tom Boucher
0964c97931 fix: run forensics duplicate detection before investigation (#2704) (#3260)
Move the dedup check from after the Investigation Protocol to before it,
so already-known bugs are caught before spending tokens on deep source
analysis. The DEDUP_PROMPT_SECTION now acts as a pre-investigation gate
with a decision to skip full investigation when a match is found.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:50:06 -06:00
Tom Boucher
3d896eee8a fix: skip TUI render loop on non-TTY stdout to prevent CPU burn (#3095) (#3263)
When gsd is spawned as an RPC bridge child process, stdout is a pipe
(process.stdout.isTTY === undefined). The TUI render loop would run at
~4,600 renders/sec writing ANSI escape codes to the pipe, consuming
500%+ CPU per process while idle.

Add isTTY guard to Terminal interface, ProcessTerminal.start(), TUI.start(),
and requestRender() so the entire render pipeline is skipped on non-TTY stdout.
RemoteTerminal (browser-backed) correctly reports isTTY=true.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:49:55 -06:00
Tom Boucher
da135e9334 fix: persist forensics report context across follow-up turns (#2941) (#3261)
The forensics prompt was sent as a one-shot message via sendMessage()
with triggerTurn: true, causing context loss on follow-up turns. Now
writes an active-forensics.json marker to .gsd/runtime/ so that
buildBeforeAgentStartResult() can re-inject the forensics prompt on
subsequent turns, mirroring how guided task execution context works.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:49:50 -06:00
Tom Boucher
155df22e9e fix: invalidate workspace state on turn_end so milestones list stays current (#2706) (#3266)
The milestones list only refreshed on agent_end events, causing stale
milestone state during multi-turn agent execution. Add turn_end as a
workspace cache invalidation trigger so the UI reflects milestone
changes after each turn boundary.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:48:40 -06:00
Tom Boucher
603839d7f8 fix: eliminate 3 recurring doctor audit false positives (#3105) (#3264)
Bug 1: orphaned worktree check now skips directories that only contain
doctor artifacts (.gsd/doctor-history.jsonl), preventing the circular
false positive where appendDoctorHistory recreates the dir it reports.

Bug 2: blocker_discovered_no_replan check now skips when all tasks are
done, treating the blocker as implicitly resolved and breaking the
deadlock with stale_replan_file.

Bug 3: parsePlan now scans the full body for task checkboxes after the
Tasks section, finding T02+ entries that appear after interleaved
detail headings (## Steps, ## Must-Haves).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 13:48:26 -06:00