When devRoot pointed at a monorepo, discoverProjects scanned one level
deep and listed each workspace/package as a separate project. Now it
checks for monorepo markers (pnpm-workspace.yaml, lerna.json, turbo.json,
nx.json, rush.json, package.json workspaces) before scanning children.
If the root is a monorepo, it returns it as a single project entry.
- Add detectMonorepo() to bridge-service with support for 6 monorepo formats
- Add isMonorepo signal to ProjectDetectionSignals
- Update discoverProjects to short-circuit when root is a monorepo
- Show 'Monorepo' tag in project list UI
- Add 24 tests covering all monorepo detection scenarios
* feat: Migrated headless orchestrator to use execution_complete events,…
- "src/headless.ts"
- "src/headless-ui.ts"
- "src/tests/headless-v2-migration.test.ts"
GSD-Task: S06/T02
* test: Wired pi-coding-agent to re-export JSONL utils from @gsd/rpc-clie…
- "packages/pi-coding-agent/src/modes/rpc/jsonl.ts"
- "packages/pi-coding-agent/package.json"
- "packages/rpc-client/src/index.ts"
- "packages/rpc-client/src/jsonl.ts"
- "packages/rpc-client/src/rpc-client.ts"
- "packages/rpc-client/src/rpc-types.ts"
- "packages/rpc-client/src/rpc-client.test.ts"
- "packages/rpc-client/package.json"
GSD-Task: S06/T03
* feat: Wire --resume flag to resolve session IDs via prefix matching and…
- "src/headless.ts"
- "dist/headless.js"
GSD-Task: S01/T01
* test: Added 5 e2e integration tests proving headless JSON batch, SIGINT…
- "src/tests/integration/e2e-headless.test.ts"
GSD-Task: S01/T02
* test: Updated @gsd/rpc-client and @gsd/mcp-server to 2.52.0 with publis…
- "packages/rpc-client/package.json"
- "packages/mcp-server/package.json"
- "packages/rpc-client/.npmignore"
- "packages/mcp-server/.npmignore"
GSD-Task: S02/T01
* chore: auto-commit after complete-milestone
GSD-Unit: M002-gzq23a
* fix: revert jsonl.ts to inline implementation — @gsd-build/rpc-client not available at source-level test time in CI
The re-export from @gsd-build/rpc-client fails in CI because tests run against
TypeScript source (--experimental-strip-types) before any build step. The npm
dependency resolves to node_modules/ which requires dist/ to exist. Reverting
to the original inline implementation eliminates the cross-package dependency
for source-level imports.
secure_env_collect previously persisted secrets to their destination but left the running Node process unchanged. Extensions like Context7 read process.env directly, so newly collected keys did not work until restart.
Hydrate process.env as soon as a secret is successfully applied, and cover the regression through collectSecretsFromManifest so the current session can use the key immediately.
Closes#2685
* fix: resolve stash pop conflicts and stop swallowing merge errors
After a squash merge, `git stash pop` can conflict on `.gsd/` state files,
leaving them in UU state that permanently blocks all subsequent milestone
merges. The post-commit stash pop catch block now detects `.gsd/` conflicts,
auto-resolves them by accepting the HEAD version (matching the existing
merge-time policy), and drops the stash when safe.
In phases.ts, three catch blocks only handled MergeConflictError and silently
continued on any other error, allowing auto-mode to advance to the next
milestone with unmerged work. All three now stop auto-mode and return a
"merge-failed" break result for non-conflict errors.
Closes#2766
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression tests for stash pop conflict and error handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use correct LogComponent type in stash pop handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: join file array for logWarning in stash pop handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: treat any extracted verdict as terminal in isValidationTerminal
If the LLM writes a VALIDATION file with an unrecognized verdict like
`fail`, the allowlist in isValidationTerminal() returned false, keeping
the state machine in validating-milestone phase and re-dispatching
validate-milestone indefinitely (14+ times observed).
Any non-null verdict from extractVerdict() means validation completed.
Only return false when no verdict could be parsed.
Closes#2769
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression tests for isValidationTerminal with fail verdict
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: update existing test to match new any-verdict-is-terminal behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use localStorage for auth token to enable multi-tab usage
sessionStorage is tab-scoped, so manually opened second tabs cannot
access the auth token delivered via URL fragment to the first tab.
localStorage is shared across all tabs on the same origin, and since
each GSD instance binds to a unique random port the origin already
scopes the token to that instance.
Also adds a `storage` event listener so already-open tabs pick up
token changes immediately.
Closes#2714
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: update web-auth-token test for localStorage migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard activeMilestone.id access in discuss and headless paths
When upstream state corruption (#2772, #2770) produces an activeMilestone
object with undefined id, the existing `!state.activeMilestone` guard
passes (truthy object), and the undefined id propagates to SQLite where
better-sqlite3 throws "Missing named parameter 'mid'".
Strengthen guards at three call sites to check `!state.activeMilestone?.id`
so corrupted state falls through to the no-milestone recovery path.
Closes#2773
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression tests for activeMilestone.id guard
Covers the #2773 fix where a malformed activeMilestone object with
id: undefined bypassed the old truthiness check and caused a crash
in discuss and headless paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clean up zombie parallel workers stuck in error state
refreshWorkerStatuses() set worker.state = "error" for dead workers but
never removed them or deactivated the orchestrator, leaving zombies in
memory forever. restoreRuntimeState() short-circuited on state?.active
without verifying any workers were actually alive.
Two fixes:
1. refreshWorkerStatuses() now checks if all workers are terminal after
the status sweep — if so, deactivates the orchestrator and removes
the persisted state file.
2. restoreRuntimeState() now verifies at least one worker is in a
non-terminal state before returning true. If all workers are dead,
it clears the stale cached state and falls through to restoreState()
which handles proper cleanup.
Closes#2736
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression tests for zombie worker cleanup
Covers #2736: verifies refreshWorkerStatuses() deactivates orchestrator
when all workers reach terminal states, and restoreRuntimeState() clears
stale cached state instead of returning true with only dead workers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: relax milestone validation gate to accept prose evidence
The completion gate at auto-dispatch.ts required exact "MET"/"N/A"
substrings that renderValidationMarkdown() never emits, causing a
deadlock where no validation output could satisfy the gate. The gate
now accepts either the structured template format (MET/N/A table) or
prose evidence patterns (e.g., "Operational: verified", "Operational
checks confirmed") that the validation agent naturally produces.
Closes#2739
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add tests for relaxed validation gate patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: write milestone reports to project root instead of worktree
During worktree isolation, s.basePath points to the temporary worktree
directory. Reports written there are silently lost when the worktree is
cleaned up. Use s.originalBasePath (falling back to s.basePath when not
in a worktree) so reports persist in the actual project directory.
Closes#2751
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression test for milestone report path resolution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: auto-resolve build artifact conflicts in milestone merge
The binary conflict classification in mergeMilestoneToMain only
auto-resolved .gsd/ prefixed files. Machine-generated build artifacts
like .tsbuildinfo, .pyc, __pycache__/, .DS_Store, and .map files were
treated as real code conflicts, blocking auto-merge unnecessarily.
Extract an isSafeToAutoResolve helper that checks both the .gsd/ prefix
and a SAFE_AUTO_RESOLVE_PATTERNS regex list. Matched files are resolved
with --theirs, same as .gsd/ state files.
Closes#2761
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add tests for build artifact auto-resolve patterns
Extract isSafeToAutoResolve and SAFE_AUTO_RESOLVE_PATTERNS to module-level
exports for testability. Add unit tests covering .gsd/ state files, build
artifacts (.tsbuildinfo, .pyc, __pycache__, .DS_Store, .map), and rejection
of real source files (.ts, .js, .py, .json, .md).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: let rate-limit errors attempt model fallback before pausing
Rate-limit errors were early-returning with pauseTransientWithBackoff()
before reaching model fallback logic. Since rate limits are frequently
per-model (not provider-wide), this caused 20+ minute stuck-loops when
fallback models were available. Now rate-limit errors enter the same
fallback path as other transient errors, only pausing if no fallback
model is available.
Closes#2770
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression test for rate-limit model fallback
Verifies that rate-limit errors enter the model fallback path before
pausing (#2770), and that the old early-return bypass no longer exists.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: harden web runtime auth token and lock retry tests
Teach the packaged web runtime harness to recover the auth token from the launcher stderr when the browser-open stub log is absent. Also widen the transient session-lock retry tests so they stay stable under full-suite CPU contention.
* test: harden suite-level RTK and worktree stability
Stabilize the RTK seam tests under full-suite load by using a faster fake RTK binary on Unix and allowing the tests to raise the rewrite timeout without changing the production default. Also widen the transient session-lock retry budget and give the heavy auto-worktree milestone merge suite an explicit timeout so it can complete under CI-level contention.
* test: harden git-service repo bootstrap under suite load
Switch repo bootstrap steps in git-service.test.ts to runGit(...) where the setup only needs direct git invocations.
This removes avoidable shell wrappers from the highest-churn repo setup paths, which makes the full unit suite less prone to child-process flake under load while keeping the test behavior unchanged.
* fix: prevent gsd next from self-killing via stale crash lock
cleanupAfterLoopExit() did not clear the crash lock or session lock.
On the next `/gsd next`, checkRemoteAutoSession() read the stale lock
with the current PID, reported it as a "remote" session, and
stopAutoRemote() sent SIGTERM to the current process.
Three fixes:
1. cleanupAfterLoopExit() now clears crash lock and releases session lock
2. checkRemoteAutoSession() returns { running: false } for own PID
3. stopAutoRemote() guards against self-kill for own PID
Closes#2730
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression tests for stale lock self-kill prevention
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flip two safety mechanisms from opt-in to opt-out so all users benefit
from rollback protection and merge regression checks without manual
configuration.
- git.snapshots: false → true (creates recovery refs before destructive ops)
- git.pre_merge_check: false → "auto" in solo mode (auto-detects test runner)
Both remain configurable; users can explicitly disable with snapshots: false
or pre_merge_check: false.
Closes#2677
* fix: rebuild stale workspace packages after git pull
ensure-workspace-builds.cjs only triggered a build when dist/index.js
was missing entirely. After `git pull` updates package sources, the old
dist/ stayed in place causing TypeScript type errors (bash_transform,
authMode, malformedArguments missing from compiled .d.ts files).
Now compares newest .ts mtime under src/ against dist/index.js mtime
and rebuilds any package whose sources are newer than its dist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(rtk): trust explicit binaryPath without existsSync check; add options object to shared rewriteCommandWithRtk
resolveRtkBinaryPath was calling existsSync on options.binaryPath, making
it impossible to inject a non-existent test binary — tests expected the
options-object API to bypass filesystem checks.
Also brings src/resources/extensions/shared/rtk.ts rewriteCommandWithRtk
in line with the same options-object signature already in src/rtk.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(gsd): extract duplicated status guards and validation helpers
isClosedStatus(), isNonEmptyString(), and validateStringArray() were
each copy-pasted across 5-10 tool handler files with no shared module.
Extract them into status-guards.ts and validation.ts, replace all 26
inline status checks and 8 duplicated validation functions with imports.
Standardizes "inside a closed" -> "in a closed" in two reopen error
messages as a side effect of the normalization pass.
Closes#2727
* refactor(gsd): migrate state.ts isStatusDone to isClosedStatus; fix blank lines and import order
- state.ts had a private isStatusDone() identical to isClosedStatus() —
replace with import from status-guards.ts
- Remove double blank lines left behind in plan-{milestone,slice,task}.ts
and replan-slice.ts after local function extraction
- Fix import ordering in reassess-roadmap.ts (node built-ins first,
status-guards/validation before gsd-db block)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: Registered 6 MCP tools (gsd_execute, gsd_status, gsd_result, gsd_…
- "packages/mcp-server/src/server.ts"
- "packages/mcp-server/src/cli.ts"
- "packages/mcp-server/src/index.ts"
- "packages/rpc-client/dist/index.d.ts"
GSD-Task: S05/T02
* docs: Added 31 integration tests, build pipeline, and consumer README f…
- "packages/mcp-server/src/mcp-server.test.ts"
- "packages/mcp-server/README.md"
- "packages/mcp-server/dist/"
GSD-Task: S05/T03
* fix: prevent auto-mode hard stop on provider errors and suppress duplicate async_job_result follow-ups (#2762)
Two compounding bugs caused auto-mode to silently die after unit completion:
1. async_job_result follow-ups fired after unit completion because deliverResult
ran synchronously in the job promise .then() chain, racing with await_job's
.then() that sets job.awaited=true. Deferring delivery by one microtask via
queueMicrotask ensures await_job marks the job first.
2. Provider error pause converted to hard stop because pauseAuto resolved the
unit promise with {status:"cancelled"} but no ErrorContext, so runUnitPhase
treated it identically to a session-creation timeout and called stopAuto.
Now pauseAuto accepts and forwards ErrorContext, and runUnitPhase checks for
category:"provider" to break without hard-stopping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: update source-scan assertion for new pauseAuto signature
The structural test checked for `resolveAgentEndCancelled()` with empty
parens. Now that pauseAuto passes _errorContext, match the call prefix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs fixed:
1. transaction() now tracks nesting depth. When deleteTask/deleteSlice
(which wrap in transaction()) are called from within an outer
transaction() in reassess-roadmap.ts or replan-slice.ts, the inner
call skips BEGIN/COMMIT since SQLite doesn't support nested
transactions. This fixes:
- reassess-handler.test.ts: 3 failing tests
- replan-handler.test.ts: 4 failing tests
All errors were: 'cannot start a transaction within a transaction'
2. slice_dependencies table and v13/v14 indexes were only created in
migrateSchema (for upgrades from older versions) but missing from
initSchema (for fresh databases). New databases started at schema
version 14 but never created the table, causing 'no such table:
slice_dependencies' when deleteSlice was called.
preferences.md was in ROOT_STATE_FILES which caused syncWorktreeStateBack()
to overwrite the project root's authoritative copy with the worktree's
stale copy. The forward-sync (main → worktree) is already handled
separately in syncGsdStateToWorktree() as additive-only.
Fixes the failing CI test:
worktree-preferences-sync.test.ts:107
'#2684: syncWorktreeStateBack does NOT overwrite project root preferences.md'
Also updates preferences-worktree-sync.test.ts to assert preferences.md
is NOT in ROOT_STATE_FILES (it must be handled separately).
Fixes#2726. Tool handlers were bypassing the DB port layer by calling
_getAdapter() directly for raw SQL. Replace all such callsites with
proper exported DB functions.
- Add setTaskSummaryMd(), setSliceSummaryMd() to gsd-db.ts
- Extend updateMilestoneStatus() to accept optional completedAt param
- Add deleteVerificationEvidence(), deleteAssessmentByScope() to gsd-db.ts
- complete-task.ts: use updateTaskStatus, setTaskSummaryMd, deleteVerificationEvidence
- complete-slice.ts: use updateSliceStatus, setSliceSummaryMd
- complete-milestone.ts: use updateMilestoneStatus for both complete and rollback
- validate-milestone.ts: use insertAssessment, deleteAssessmentByScope
- plan-slice.ts, plan-milestone.ts: remove dead _getAdapter import
Resolve conflicts: keep unified classifyError (PR intent), remove old
classifyProviderError. Port stream_exhausted pattern from main into
unified CONNECTION_RE and add corresponding test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts in provider-error-pause.ts and provider-errors.test.ts.
Add stream_exhausted(_without_result) pattern to unified CONNECTION_RE
(ported from main's classifyProviderError addition).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
plan-task, plan-slice, plan-milestone, reassess-roadmap, and
replan-slice all ran state machine guards (getSlice, getMilestone,
getTask) outside the transaction() callback, then performed writes
in a separate transaction. This created a TOCTOU race: two agents
could both pass the guard simultaneously and both write successfully.
Fix: move all guard checks into the transaction() callback using
the guardError pattern already used by complete-task, complete-slice,
reopen-task, and reopen-slice. The SQLite write lock now covers both
the guard reads and the subsequent writes atomically.
Closes#2723
* refactor(pi-ai): replace model-ID pattern matching with capability metadata
Add ModelCapabilities to Model<TApi> and a CAPABILITY_PATCHES mechanism
so call sites read model.capabilities fields instead of parsing model IDs
or hardcoding provider names.
- types.ts: add ModelCapabilities interface (supportsXhigh, requiresToolCallId,
supportsServiceTier, charsPerToken) and capabilities?: ModelCapabilities to
Model<TApi>
- models.ts: add CAPABILITY_PATCHES table applied at registry init; patches
declare GPT-5.x and Opus 4.6 capabilities once instead of repeating ID
checks at every call site; supportsXhigh() now reads capabilities only
- service-tier.ts: extract SERVICE_TIER_MODEL_PREFIXES constant so the gating
list has a single named home; add path comment pointing to issue #2546 for
the full capability-driven follow-up
No behaviour change. New models and providers can declare capabilities in
their model definitions without touching function logic.
Closes#2546
* fix(pi-ai): apply capability patches to custom/discovered/extension models
Models constructed outside the static pi-ai registry (custom models
from models.json, extension-registered models, discovered models)
bypassed CAPABILITY_PATCHES — causing supportsXhigh() to silently
return false for GPT-5.x or Opus 4.6 variants registered through
those paths.
Export applyCapabilityPatches() from pi-ai and call it in ModelRegistry
after model assembly in all three construction paths: loadModels(),
applyProviderConfig(), and discoverModels().
Add regression tests covering patching, precedence, idempotency,
and synthetic models that mimic the custom/extension path.
Closes#2546
When the milestones DB table has 0 rows (e.g. failed initial migration
per #2529), deriveState fell through to the filesystem path because
deriveStateFromDb was only called when dbMilestones.length > 0. The
reconciliation code inside deriveStateFromDb was unreachable — the very
condition it was supposed to fix gated its execution.
The fix moves disk→DB sync into deriveState itself: when the DB is
available but empty, scan disk milestone directories and insert them
before the length check. This ensures the DB path activates correctly
even after a failed migration.
Closes#2631
preferences.md was missing from both copyPlanningArtifacts() (initial
worktree seed) and the ongoing forward-sync in syncGsdStateToWorktree().
This meant post_unit_hooks, skill rules, and custom instructions from
preferences.md were silently unavailable in auto-mode worktrees.
Fix:
- Add preferences.md to copyPlanningArtifacts() file list
- Add dedicated preferences.md forward-sync in syncGsdStateToWorktree()
with additive-only semantics (only copies when missing in worktree)
- NOT added to ROOT_STATE_FILES to prevent syncWorktreeStateBack() from
overwriting the project root's authoritative preferences.md
Regression test verifies:
1. Forward-sync copies preferences.md when missing from worktree
2. Forward-sync does NOT overwrite existing worktree preferences.md
3. Back-sync does NOT overwrite project root preferences.md
Closes#2684
Claude Code stores marketplace sources under ~/.claude/plugins/marketplaces/,
where each subdirectory (e.g. marketplaces/my-marketplace/) is a marketplace
repo containing .claude-plugin/marketplace.json. The parent directory itself
does not have a marketplace.json.
categorizePluginRoots was checking only the root path for marketplace.json,
so ~/.claude/plugins/marketplaces/ was always categorized as flat (no
marketplace.json at that level). The flat fallback then looked for
package.json, which Claude plugins don't have — they use
.claude-plugin/plugin.json.
Two fixes:
1. categorizePluginRoots now scans one level deeper: when a root isn't
itself a marketplace, it enumerates immediate subdirectories to find
child marketplace repos. Deduplicates via a seen set when the same
marketplace is reachable through multiple roots.
2. discoverClaudePlugins now recognizes .claude-plugin/plugin.json in
addition to package.json, so cached Claude marketplace plugins are
discoverable in the flat-path fallback.
Closes#2717
Co-authored-by: Eric Muller <ericmuller@confluent.io>
The idle watchdog treated ask_user_questions and secure_env_collect as
stalled tools, killing sessions before users could respond.
Root cause: tool tracking stored only toolCallId → timestamp with no
tool name, so the watchdog couldn't distinguish user-interactive tools
from hung tools.
Changes:
- auto-tool-tracking: store toolName alongside timestamp, add
INTERACTIVE_TOOLS set and hasInteractiveToolInFlight()
- auto.ts: forward optional toolName through markToolStart wrapper
- register-hooks: pass event.toolName to markToolStart
- auto-timers: skip stall detection when interactive tool is in-flight,
record lastProgressKind: 'interactive-tool-waiting'
- New test: 13 cases covering interactive exemption, completion cleanup,
backwards compat, and existing behavior preservation
deriveStateFromDb line 565 used activeMilestoneSlices.every() without a
length > 0 guard. In JavaScript, [].every() === true (vacuous truth),
which would cause a premature phase transition to validating-milestone
if the array were empty at that point.
While the current code has an early-return at line 536 that catches
length === 0, the guard is still necessary for consistency with the
identical checks at lines 368 and 413 (which both have the guard),
and to protect against future control-flow changes that might bypass
the early return.
Closes#2667
When VALIDATION.md has verdict needs-remediation and all slices appear
done in the DB, the state machine enters completing-milestone. The
complete-milestone dispatch rule had no verdict check, so it dispatched
the unit — the agent correctly refused (validation failed), no SUMMARY
was written, and the unit was re-dispatched up to MAX_LIFETIME_DISPATCHES
times before stuck detection fired.
The fix adds a verdict check in the completing-milestone dispatch rule
that returns action: stop with level: warning when the verdict is
needs-remediation. Using warning level ensures the session pauses
(resumable) rather than hard-stopping, matching the pattern from #2474.
Closes#2675
* fix: sync milestone DB status in parkMilestone and unparkMilestone
parkMilestone only wrote the PARKED.md filesystem marker but never
updated the DB milestones.status field. Similarly, unparkMilestone
deleted the marker but left the DB at 'parked'. Because
deriveStateFromDb checks BOTH the filesystem marker AND m.status,
an unparked milestone was still skipped — the user saw 'All milestones
complete' despite the milestone being unparked on disk.
The fix adds updateMilestoneStatus() to gsd-db.ts and calls it from
both parkMilestone (→ 'parked') and unparkMilestone (→ 'active'),
guarded by isDbAvailable() with non-fatal try/catch.
Closes#2694
* review: log DB sync failures instead of silently swallowing
Replace empty catch blocks with process.stderr.write so park/unpark
DB sync failures are visible. Matches the pattern used in gsd-db.ts
for non-fatal DB errors.
Addresses review feedback from igouss on PR #2696.
* fix(remote-questions): empty-key entry in auth.json shadows valid Discord bot token
removeProviderToken() called auth.set(provider, { key: '' }) instead of
auth.remove(provider). Since AuthStorage.set() appends for api_key type
(deduplicating by exact key match), this inserted an empty-key entry at
index 0. Every credential lookup (.get(), .find()) matched the empty
entry first, shadowing valid tokens at later indices.
Fixes:
- remote-command.ts: use auth.remove() instead of auth.set() with empty key
- config.ts: hydrateRemoteTokensFromAuth .find() now requires non-empty key
- wizard.ts: loadStoredEnvKeys uses getCredentialsForProvider + .find()
instead of .get() which returns creds[0]
- onboarding.ts: check existing tokens via .some() over full credentials
array instead of .get() which only returns first entry
- key-manager.ts: filter empty-key entries in getAllKeyStatuses, add/remove/
rotate provider pickers, and doctor env-conflict check
Tests: 3186 pass, 0 fail across full GSD test suite
* fix(config): ignore empty shadowing tool keys
* feat: Created gsd-orchestrator/ skill directory with ClawHub frontmatte…
- "gsd-orchestrator/SKILL.md"
- "gsd-orchestrator/references/commands.md"
- "gsd-orchestrator/references/answer-injection.md"
- "gsd-orchestrator/references/json-result.md"
GSD-Task: S03/T01
* test: Add audit persistence tests for workflow-logger (#2722)
The production fix for #2722 (wiring setLogBasePath + preserving
_auditBasePath across _resetLogs) was already merged but had no
test coverage. Add tests verifying both behaviors.
Closes#2722
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug 1: When a tool stalls longer than idle_timeout, the watchdog notifies
but falls through to detectWorkingTreeActivity(), which resets
lastProgressAt when files were modified earlier in the task. Recovery is
never called — the session burns tokens indefinitely.
Fix: Add stalledToolDetected flag + clearInFlightTools() call. The
filesystem-activity check is guarded by !stalledToolDetected so it
cannot override the stall verdict.
Bug 2: After async recoverTimedOutUnit(), pauseAuto/stopAuto may set
s.currentUnit = null during the await, but the next line accesses
s.currentUnit.startedAt without a null guard — crash.
Fix: Add null guard for s.currentUnit after the recovery call.
Closes#2527
Treat Claude SDK generator exhaustion without a terminal result as a
stream interruption instead of a successful completion.
This prevents phantom-success auto-mode advances, keeps the failure
classifiable as transient provider recovery, and adds regression tests
for the fallback message plus provider classification.
Closes#2575
Split fake multi-stage Dockerfile into independent CI builder and
runtime images. Add proper entrypoint with UID/GID remapping via
PUID/PGID, sentinel-based first-boot bootstrap, pre-creation of
critical file targets, and signal-forwarding privilege drop via gosu.
Standardize on Node 24, split compose into minimal + full reference.
Closes#9
All other .gsd/ state files use uppercase naming (DECISIONS.md,
REQUIREMENTS.md, PROJECT.md, etc). This renames the canonical
preferences file to PREFERENCES.md while keeping a migration
fallback — the loader checks PREFERENCES.md first, then falls
back to lowercase preferences.md for existing installations.
Closes#2700
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(gsd): write DB before disk in validate-milestone to match engine pattern
validate-milestone.ts wrote the VALIDATION.md file to disk before
inserting the assessment row into the DB. Every other handler in the
engine (complete-task, complete-slice) does DB-first, disk-second with
rollback compensation. The inverted order meant a crash between disk
write and DB insert would leave an orphaned file with no DB record —
a state that is harder to detect and recover from than the inverse
(DB row exists, file missing → projection rendering can regenerate).
Fix: reorder to DB-first, disk-second. On disk write failure, delete
the DB row via DELETE FROM assessments so state stays consistent.
Add two handler-level tests verifying:
1. Both DB row and disk file exist after success
2. DB row is rolled back (deleted) when disk write fails
Closes#2725
* fix(test): use file-as-directory to trigger disk failure cross-platform
chmod 0o444 does not prevent writes on Windows. Replace with
replacing the milestone directory with a regular file, so
saveFile's mkdirSync/write fails on all platforms.
Fixes windows-portability CI failure.
`/gsd auto M016` silently discarded the milestone ID and started
whichever milestone deriveState() picked as first incomplete. The
command handler parsed --verbose, --debug, and --yolo flags but never
extracted a milestone target.
Root cause: handleAutoCommand() had no milestone-ID extraction step.
The `rest` string from parseYoloFlag was only checked for flags, and
startAuto() was always called without milestone scoping.
Fix: add parseMilestoneTarget() to extract M-prefixed IDs (M001,
M001-a3b4c5) from the command string. When a milestone is specified:
1. Validate it exists via findMilestoneIds() — notify on missing
2. Set GSD_MILESTONE_LOCK env var (already honored by state.ts at
three derivation points and by auto-post-unit.ts) via a
withMilestoneLock() wrapper that cleans up the env var when
auto-mode exits, preventing leakage into subsequent commands.
Both `/gsd auto <milestone>` and `/gsd next <milestone>` are
supported. Flags (--verbose, --debug) continue to work in any order.
Closes#2521