Three fixes:
1. Strip backtick wrapping in normalizeFilePath — LLM-generated paths
like \`src/foo.ts\` resolve to nonexistent paths, causing false blocks
2. Exclude task.files from existence checks — it includes files the task
will create, so they legitimately don't pre-exist
3. Lower minimum task count from 2 to 1 — single-task slices are valid
per the planning prompt
Closes#3649Closes#3626
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
renderAllProjections called renderPlanProjection which overwrote the
complete PLAN.md (from markdown-renderer.js) with a simplified projection
missing Must-Haves, Verification, Files Likely Touched sections and
corrupting multi-line task descriptions.
Remove the plan projection call from renderAllProjections — the
authoritative renderer in plan-slice/replan-slice tools is the sole
writer. The renderIfMissing recovery path is preserved for when the
file is actually missing.
Closes#3651
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When switching from isolation:branch/worktree to isolation:none, HEAD
could remain on a milestone/<MID> branch from the prior session. All
subsequent auto-mode commits would silently land on the wrong branch.
Now auto-start checks for stale milestone branches when isolation:none
and auto-checks out to the integration branch (main/master).
Closes#3613
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When complete-slice unit fails after writing SUMMARY.md but before
calling updateSliceStatus(), the DB stays out of sync. The post-unit
check previously reported this as a "rogue" artifact, leading to
infinite re-dispatch of the same complete-slice unit.
Now auto-remediates by calling updateSliceStatus() to sync the DB when
SUMMARY exists on disk but status != "complete". Falls back to rogue
detection if the DB update fails.
Closes#3633
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gsd_milestone_status checked isDbAvailable() but never called
ensureDbOpen(), making it always fail outside auto-mode sessions where
the DB is pre-opened during bootstrap.
Replace with ensureDbOpen() which safely opens existing DB files without
side effects when .gsd/ content exists.
Closes#3644
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gsd_milestone_generate_id inserts a DB row with status "queued" as a
side effect. If the milestone is never planned, this phantom row blocks
the state machine — isGhostMilestone returned false for any milestone
with a DB row, regardless of status.
Now isGhostMilestone treats a "queued" DB row with no disk artifacts
(CONTEXT, ROADMAP, SUMMARY) as a ghost, allowing the state machine to
skip phantom milestones.
Closes#3645
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When validation returns needs-remediation, remediation slices are added
and executed. But the state machine treated any terminal verdict as ready
for completing-milestone, while dispatch correctly blocked completion for
needs-remediation — creating a permanent deadlock.
Now all three derivation paths (deriveStateFromDb, _deriveStateImpl
registry loop, and _deriveStateImpl completion check) treat
needs-remediation as requiring re-validation, routing back to
validating-milestone instead of completing-milestone.
Closes#3596
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip slices with status skipped/complete/done when checking for missing
SUMMARY files. Skipped slices never produce SUMMARYs by design, and
legacy-complete slices may lack them after worktree merge failures.
The DB status is authoritative — missing SUMMARY is a cosmetic gap,
not evidence the slice was incomplete.
Fixes#3620
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add pre-flight stale lock cleanup before proper-lockfile acquisition:
if the .lock/ directory exists but no auto.lock metadata is present
(or the owning PID is dead), remove it proactively instead of waiting
for the 30-min stale window. Also improve the error message when
recovery fails to include the rm command for manual cleanup.
Fixes#3218
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevent race where a freshly-set pending entry (before LLM writes
artifacts) could be falsely detected as stale. Only clear entries
older than 30 seconds with no manifest or CONTEXT.md on disk.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the pending auto-start guard fires, check if the discussion is
actually still in progress by verifying the discussion manifest or
milestone context exists on disk. If neither exists, the entry is stale
from an interrupted session — clear it and allow re-entry.
Fixes#3274
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Skip ENOENT warnings in clearProjectRootStateFiles and untracked file
cleanup since missing files are expected. Check if .git is a directory
before attempting readFileSync in resolveGitDir to avoid EISDIR warning
in normal (non-worktree) repos.
Fixes#3597
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When errorMessage is uninformative (e.g. "success", "ok"), fall back
to the assistant message text content for display while keeping
rawErrorMsg for classification to avoid prose false-positives.
Fixes#3588
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When errorMessage is uninformative (e.g. "success", "ok", "error"),
fall back to the assistant message text content to surface the real
provider error like "Invalid API key · Please run /login".
Fixes#3588
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Distinguish between malformed-JSON pauses and queued-user-message
pauses in the notification so operators see the correct root cause.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add isQueuedUserMessageSkip() predicate and extend recordToolInvocationError
to catch "Skipped due to queued user message." so auto-mode pauses instead
of retrying the same unit until the provider aborts with 3 consecutive
validation failures.
Fixes#3595
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip trailing punctuation before matching and add "provided" to the
alternation so the plan-milestone default value no longer deadlocks
completing-milestone dispatch. Also change plan-milestone verification
defaults from "Not provided." to empty string to prevent recurrence.
Update JSDoc comments to reflect new defaults.
Fixes#3634
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dirent.isDirectory() returns false for symbolic links, so extensions
installed as directory symlinks under ~/.gsd/agent/extensions/ were
invisible to all management commands (list, enable, disable, info).
Apply the same guard already used in loader.ts discoverExtensionsInDir:
entry.isDirectory() || entry.isSymbolicLink()
Closesigouss/gsd-2#20
Strip trailing punctuation before matching and add "provided" to the
alternation so the plan-milestone default value no longer deadlocks
completing-milestone dispatch. Also change plan-milestone verification
defaults from "Not provided." to empty string to prevent recurrence.
Fixes#3634
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backdrop was painting empty lines with dark gray background (48;5;233),
making the entire screen go black. Now uses dim + gray foreground only.
Message truncation now measures actual prefix width with visibleWidth()
instead of hardcoded 20-char estimate, and uses truncateToWidth() for
proper Unicode handling.
When the planning agent writes S##-PLAN.md with task entries but never
calls the gsd_plan_slice persistence tool, the DB has zero task rows
even though the plan file on disk contains valid tasks. This causes
deriveState to return phase='planning' forever — the auto-mode
dispatcher re-dispatches plan-slice in an infinite loop.
Add a reconciliation step in deriveStateFromDb: when the DB returns zero
tasks but the plan file exists and contains parsed tasks, import them
into the DB so the state machine can advance past planning into
execution. This mirrors the existing #2514 reconciliation pattern for
stale task status.
Fixes#3600
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces `r.status === "complete"` with `isClosedStatus(r.status)` in
dispatch-guard.ts so slices completed via the reconciliation replay path
(which writes "done") or skipped slices are correctly recognized as
closed. This was causing auto-mode to block on dependencies that were
actually complete.
Fixes#3601
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Differential renderer can't clear old overlay positions when height
changes between filter cycles. Pad to maxVisibleRows so the overlay
stays the same size regardless of filter state.
Use dark gray background + dim foreground for visible backdrop effect
instead of barely-perceptible SGR dim. Size overlay box to content
instead of padding to fill the entire viewport.
- Overlay layout: verify backdrop dims base lines, no dim without flag,
overlay composites on top of dimmed background
- Notification store: verify markAllRead and clearNotifications do not
delete a foreign process's lock file
_withLock() was unconditionally unlinking the lock file in finally,
even when lock acquisition failed. This could delete another process's
lock and allow unlocked concurrent writes. Now tracks ownership and
only cleans up locks we created.
The notification overlay was rendering too small with few entries, allowing
underlying content to bleed through. Added viewport padding to fill the
overlay box and a new `backdrop` option to OverlayOptions that dims the
background behind modal overlays.
Tighten the deriveState fallback per adversarial review:
- Intent-gated: only fire for low-entropy resume prompts via
RESUME_INTENT_PATTERNS (continue, ok, go ahead, resume, etc.)
- Phase-gated: only during state.phase === "executing"
- Non-resume prompts (help, status, abort, diagnostics) are not
hijacked with execution context
Add behavioral tests: 24 positive matches + 17 negative rejections
for the intent pattern, alongside the 5 structural tests.
When a user types "continue" or bare text to resume an in-progress
session, buildGuidedExecuteContextInjection() only matched two
hardcoded regex patterns and returned null for anything else — causing
the agent to rebuild everything from scratch and burn ~86k tokens.
Add a phase-gated deriveState fallback that injects task execution
context when state.phase === "executing" and an active task exists.
The phase guard prevents misrouting during replanning, gate evaluation,
or other non-execution phases.
newSession() only rebuilt the tool registry when cwd changed. When cwd
stayed the same (e.g., discuss → plan-slice in the same worktree), any
tool narrowing from setActiveTools() persisted — stripping gsd_plan_slice
and other DB tools from auto-mode subagent sessions.
Add an else-branch that calls _refreshToolRegistry with
includeAllExtensionTools:true on every session switch, regardless of cwd.
Also call resetExtensionLoaderCache() in DefaultResourceLoader.reload()
so hot-updated extension code on disk is re-compiled instead of served
from the stale jiti module cache.
Closes#3616
The schema overload detector counted ALL isError tool results toward the
consecutive-failure cap, including bash commands that returned non-zero exit
codes (e.g. rg/grep exit 1 = 'no matches'). Three consecutive exploratory
searches with no matches would trigger the cap and abort the session.
Root cause: the allToolsFailed check used toolResults.every(r => r.isError)
which conflates preparation-phase errors (schema validation, tool-not-found,
tool-blocked) with execution-phase errors (the tool ran successfully but
returned a non-zero exit code).
Fix: track preparationErrorCount alongside tool results. Only preparation
errors (schema/validation failures) increment the consecutive failure
counter. Tool execution errors — like bash exit code 1 — are valid usage
and do not count toward the cap.
Also fixes pre-existing StopReason type mismatches in agent-loop tests
(end_turn → stop, tool_use → toolUse).
Verifies that defensive guards (render-skip, chat cap, dispose, signal
handler cleanup, alert cap, orphan kill) are present in source. These
are structural tests because the leaks manifest over hours of real
usage, not in unit test timescales.
Signal handlers (SIGTERM, SIGINT, beforeExit) were registered on every
session_start but never removed. Over multiple sessions within the same
process, handlers accumulated — each adding another cleanupAll() call
and descendant kill sweep on exit.
Fix: session_shutdown now calls process.off() for each handler before
cleanupAll(), preventing accumulation.
Also: signalCleanup now kills ALL descendant processes (not just those
tracked by bg-shell) to catch bash-tool spawned children.
Alert queue: pendingAlerts is capped at 50 entries to prevent unbounded
growth when background processes generate rapid alerts faster than the
agent consumes them.
pushAlert signature updated to accept null bg parameter for system-level
alerts that don't originate from a tracked process.
Chat component cap: After 100 rendered components, oldest are removed
from the container (session transcript persists on disk via
SessionManager). Prevents unbounded memory growth in long sessions
where thousands of tool calls accumulate DOM-like component trees.
Orphan process prevention: On shutdown, listDescendants(process.pid)
finds ALL child processes (including those spawned by the Bash tool
that bg-shell doesn't track) and kills them with SIGTERM + 500ms
grace + SIGKILL. Prevents orphaned dev servers, build processes, etc.
from persisting after session exit.
Container.render() now returns a stable array reference when output is
unchanged — TUI.doRender() skips ALL post-processing (isImageLine scans,
applyLineResets, differential diffs) when the reference matches.
Loader decouples spinner frame rotation from Text content updates.
Previously every 80ms tick called setText() which invalidated Text's
wrapTextWithAnsi/visibleWidth caches. Now the frame is prepended in
render() while Text caches the message separately.
Text.setText() returns early when text is unchanged, avoiding cache
invalidation on redundant updates.
ToolExecutionComponent.dispose() clears heavy references (image maps,
diff previews, result data) so GC can reclaim memory when components
are removed from the chat history.
LLMs sometimes pass simple string-array fields (provides, keyFiles, etc.)
as a plain string instead of a single-element array, causing TypeBox schema
validation to reject the call before the execute function's coercion logic
can run. Fix by accepting Union([Array, String]) in the schema and adding
wrapArray() coercion for all 8 simple array fields in the execute function.
Notifications from ctx.ui.notify() and workflow-logger now persist to
.gsd/notifications.jsonl instead of evaporating as transient toasts.
- notification-store: JSONL persistence with 500-entry rotation, atomic
temp+rename rewrites, ref-counted suppress API, disk-synced counters
- notify-interceptor: WeakSet-guarded monkey-patch on ctx.ui.notify
installed at session_start and session_switch
- notification-widget: always-on belowEditor strip showing unread count
- notification-overlay: scrollable Ctrl+Alt+N panel with severity filter
- /gsd notifications command: clear, tail, filter subcommands
- workflow-logger: warnings now also persist to notification store
- web API: GET/DELETE /api/notifications with ?countOnly support
- 16 unit tests covering store, suppress, project isolation, resync
1. Post-execution retry bypass (auto-verification.ts)
- When postExecBlockingFailure is true, skip retry and pause immediately
- Post-exec failures are cross-task consistency issues that retrying won't fix
- Added test in post-exec-retry-bypass.test.ts
2. File path normalization (pre-execution-checks.ts)
- Added normalizeFilePath() to handle ./path vs path equivalence
- Normalizes backslashes, removes duplicate slashes, strips leading ./
- Applied to checkFilePathConsistency() and checkTaskOrdering()
- Added tests for path normalization in pre-execution-checks.test.ts
3. Pre-exec fail-closed (auto-post-unit.ts)
- Added try/catch around runPreExecutionChecks() inside runSafely block
- If runPreExecutionChecks throws, set preExecPauseNeeded = true
- Used logError from workflow-logger (not raw stderr)
- Added test in pre-execution-fail-closed.test.ts