Commit graph

3404 commits

Author SHA1 Message Date
github-actions[bot]
2e53b3cbad release: v2.74.0 2026-04-14 15:59:28 +00:00
Jeremy McSpadden
7b20ef8c0c Merge pull request #4186 from jeremymcs/fix/thinking-block-visibility
Fix thinking block overflow in tool-bearing turns
2026-04-14 10:29:00 -05:00
Jeremy
bc22ce95bc Cap thinking output for tool-bearing assistant turns 2026-04-14 10:15:43 -05:00
Jeremy McSpadden
2cafd66bed Merge pull request #4184 from jeremymcs/claude/refactor-code-cleanup-078AQ
fix: preserve image blocks in Claude Code SDK prompt path
2026-04-14 09:39:03 -05:00
Jeremy
01857ea180 fix(claude-code-cli): forward image blocks in SDK query prompt (#4183) 2026-04-14 09:30:02 -05:00
Jeremy McSpadden
7f77322fe2 Merge pull request #4182 from jeremymcs/claude/refactor-code-cleanup-078AQ
fix: keep assistant text visible when thinking traces are long
2026-04-14 09:17:49 -05:00
Jeremy
759bed7dae test: add regression coverage for thinking/chat visibility
Add a regression test for #4181 to ensure assistant-message caps thinking block height when text content is present.
2026-04-14 09:05:29 -05:00
Jeremy
e78eacb40e fix: keep assistant text visible when thinking traces are long
Cap thinking trace render height when assistant text is present so interactive questions remain visible.\n\nFixes #4181.
2026-04-14 09:01:20 -05:00
Jeremy McSpadden
bdef500c85 Merge pull request #4178 from jeremymcs/fix/4175-complete-milestone-false-merge
fix(auto-mode): prevent false milestone merge after complete-milestone failure (#4175)
2026-04-14 07:53:41 -05:00
Jeremy
4a2045d290 fix(state): DB-authoritative milestone completeness (#4179)
Read-side twin of #4175. `deriveStateFromDb` had a SUMMARY-file fallback
that could mark a milestone complete even when the DB row said otherwise,
allowing an orphan SUMMARY.md (crashed complete-milestone turn, partial
merge, manual edit) to cascade into a false auto-merge.

- buildCompletenessSet: drop SUMMARY fallback; only DB status decides.
- buildRegistryAndFindActive: remove SUMMARY from the completeness check;
  still consult SUMMARY as a title fallback for DB-certified milestones.
- allSlicesDone branch: drop `!summaryFile` clause so a terminal-validation
  + orphan-SUMMARY path flows through to `completing-milestone` instead of
  short-circuiting, letting complete-milestone re-run idempotently.

Regression tests: orphan SUMMARY with in-flight slice stays active; orphan
SUMMARY with all-slices-done + validation-terminal lands at
completing-milestone (does not report as already complete).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 07:43:29 -05:00
Jeremy
d07b9bf473 test(#4175): add regression guards for complete-milestone false-merge
Guards the three cooperating fixes shipped in #4178 via source inspection
so a future refactor cannot silently reintroduce the false-merge path:

- stopAuto now uses the DB getMilestone() status as the authoritative
  milestone-complete signal (falls back to SUMMARY presence only when
  the project DB is unavailable).
- postUnitPreVerification pauses auto-mode for complete-milestone after
  retries are exhausted instead of writing a stub blocker placeholder.
- recoverTimedOutUnit pauses complete-milestone instead of writing a
  stub blocker placeholder.

Unblocks the CI lint / require-tests.sh gate on PR #4178.
2026-04-14 07:35:00 -05:00
Jeremy
9957152024 fix(auto-mode): prevent false milestone merge after complete-milestone failure (#4175)
When complete-milestone failed verification, auto-mode could end up merging
the worktree to main anyway and emit a metadata-only merge warning, creating
a misleading near-complete signal while the SUMMARY was never actually written.

The blocker-placeholder path for complete-milestone wrote a stub SUMMARY
without updating DB status, and stopAuto's SUMMARY-presence check treated
the stub as a legitimate completion signal.

- auto-post-unit.ts: skip blocker placeholder and pause auto-mode on
  complete-milestone verification retry exhaustion.
- auto-timeout-recovery.ts: same guard for the idle/hard timeout path.
- auto.ts: make stopAuto Step 4 DB-authoritative (getMilestone.status ===
  "complete") with SUMMARY-presence fallback only for DB-unavailable
  legacy projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 07:24:12 -05:00
Jeremy McSpadden
4f76d434b7 Merge pull request #4155 from NilsR0711/fix/completed-at-null-on-reconcile
fix(gsd): set completed_at when reconciling task status to complete
2026-04-14 06:44:07 -05:00
Jeremy McSpadden
cdcdb1459e Merge pull request #4176 from jeremymcs/worktree-fix-4094-validate-milestone-loop
fix(auto): pause validate-milestone loop when needs-remediation has no slices (#4094)
2026-04-14 06:42:51 -05:00
Jeremy McSpadden
a09b69e27d Merge pull request #4173 from jeremymcs/claude/gsd-step-guidance-5FNrM
Add user feedback when completing steps in step mode
2026-04-14 06:42:37 -05:00
Jeremy
6b21ccbd54 fix(auto): pause on validate-milestone needs-remediation without slices (#4094)
When validate-milestone wrote VALIDATION.md with verdict=needs-remediation
but the agent failed to call gsd_reassess_roadmap to add remediation
slices, state.ts re-derived phase: validating-milestone indefinitely
because the existing #3596/#3670 guard treats needs-remediation as
non-terminal regardless of whether new work was queued. The stuck
detector only fired after 3 consecutive dispatches (~$3 + ~12 min wasted
per incident). Reproduced on M022 and M024.

Add a post-unit guard in runPostUnitVerification for validate-milestone
units: if VALIDATION.md verdict is needs-remediation and no incomplete
slices exist for the milestone (DB-authoritative via getMilestoneSlices,
filesystem fallback via parseRoadmap), pause auto-mode immediately with
a clear blocker. The legitimate re-validation flow is preserved — when
remediation slices have been queued (any non-closed status), the guard
returns continue and the existing state machine handles the work.

Tests cover: pause on all-closed scenario, skipped-status handled as
closed, continue when a queued remediation slice exists, continue on
verdict=pass, and continue when no VALIDATION file is present.
2026-04-14 06:32:42 -05:00
Jeremy
2cec5a1014 test(gsd): cover step-mode completion message helper
Extracts the step-complete notification text into buildStepCompleteMessage
and STEP_COMPLETE_FALLBACK_MESSAGE so the copy can be unit-tested
directly (milestone complete, mid-flight with next unit, unknown phase,
and deriveState-failure fallback). Resolves require-tests CI failure
on PR #4173.
2026-04-14 06:11:53 -05:00
Jeremy McSpadden
5958184e2a Merge pull request #4162 from jeremymcs/claude/refactor-code-cleanup-078AQ
Refactor CLI arg parsing and consolidate shared helpers
2026-04-14 06:10:21 -05:00
Claude
8fec87b6f2 fix(gsd): notify users what to do next after /gsd step finishes
In step mode, /gsd would run one unit and then silently exit the auto
loop, leaving users with no hint that they should /clear and /gsd again
to run the next step. Emit an info notify before returning "step-wizard"
from postUnitPostVerification so the TUI surfaces the next unit label
and the /clear + /gsd guidance (or /gsd auto to switch to auto mode).
Falls back to a generic message if deriveState throws, and handles the
milestone-complete case with a dedicated review message.

https://claude.ai/code/session_015yrPQbZTyJPqTsM654Ym3s
2026-04-14 11:03:04 +00:00
Jeremy
1a8ba9a43b fix(cli): restore --help handling when it follows a subcommand or unknown flag
The #4162 refactor removed parseCliArgs' inline --help handler assuming
loader.ts's fast-path covered it, but loader.ts only intercepts --help/-h
as argv[1]. That broke:

- gsd update --help — fell through to runUpdate() (subcommand help
  check sat dead-code below the update handler)
- gsd --unknown --help in non-TTY — tripped the TTY gate and exited 1

Move the subcommand-help check ahead of every subcommand handler and
fall back to general help when no subcommand matches, so --help wins
whenever it appears anywhere in argv.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 05:50:47 -05:00
Jeremy McSpadden
78e8665c59 Merge pull request #4163 from deseltrus/fix/auto-mode-premature-stops
fix(auto): prevent premature auto-mode stops on blocked phase + missing reassessment
2026-04-14 05:28:11 -05:00
Jeremy McSpadden
21c3f54bdb Merge pull request #4164 from deseltrus/fix/tui-render-duplication-perf
fix(tui): eliminate pinned output duplication and reduce render overhead
2026-04-14 05:27:51 -05:00
deseltrus
064389146c test(tui): add regression tests for render debounce and spinner batching
- DynamicBorder: verify lastExternalRender tracking suppresses redundant
  renders during streaming, and standalone renders fire when idle
- TUI clearOnShrink: verify debounce flag lifecycle — deferred shrink
  preserves maxLinesRendered, flag resets when content grows back

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 06:22:22 +02:00
deseltrus
68bf425606 test: update assertions for blocked-phase behavior change
Tests now expect:
- pauseAuto instead of stopAuto for blocked state (resumable)
- phase:"planning" instead of "blocked" when partial-dep fallback
  picks a slice (slice-level only; milestone-level blocked unchanged)
- activeSlice set via fallback instead of null
2026-04-14 06:20:00 +02:00
deseltrus
73f9434d11 fix(tui): eliminate pinned output duplication and reduce render overhead
rebuildChatFromMessages() called populatePinnedFromMessages() which
re-populated the pinned zone with text already present in the chat
history, causing visible duplication during session state changes.
Additionally, the spinner interval at 80ms generated ~12.5 renders/s
for a purely cosmetic animation, and clearOnShrink triggered
unnecessary full redraws during pinned-zone transitions.

- Remove populatePinnedFromMessages() from rebuildChatFromMessages()
  and add pinnedMessageContainer.clear() instead — the streaming
  lifecycle in chat-controller manages pinned content during active work
- Reduce spinner interval 80ms→200ms with render-batching that skips
  redundant renders when streaming already triggers requestRender()
- Debounce clearOnShrink: defer full redraw by one render tick so
  pinned-clear→new-streaming transitions avoid a wasted full redraw
- Increase notification widget safety-net timer 5s→30s since the
  store subscription already handles push-based updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 06:17:18 +02:00
deseltrus
8919a07962 fix(auto): prevent premature auto-mode stops on blocked phase + missing reassessment
- Change phase:"blocked" from stopAuto to pauseAuto — sessions are now
  resumable instead of requiring manual /gsd auto restart
- Default reassess_after_slice to true — reassessment fires after every
  slice completion unless explicitly disabled (was opt-in, causing missed
  reassessments in multi-slice milestones)
- Change dispatch no-match fallthrough from level:"info" (hard stop) to
  level:"warning" (pause) — unhandled phases are now recoverable
- Add dependency-resolution fallback in resolveSliceDependencies — when
  no slice has ALL deps satisfied, picks the one with the most deps met
  instead of immediately returning blocked (both DB and file-based paths)
2026-04-14 06:00:25 +02:00
Jeremy
1c2096d8f4 chore: remove stale src/app-paths.js leftover
Tracked output from 2022 (commit d93956ba4) that's missing the modern
GSD_HOME env support and webPreferencesPath export present in the .ts
source. No runtime path consumes it, but the test compile script's
copyAssets step overlays src/* onto esbuild output in dist-test, so the
stale .js was shadowing the compiled app-paths and breaking any unit
test transitively importing webPreferencesPath.
2026-04-13 22:38:53 -05:00
Jeremy
6302c952fe test(cli): add unit tests for parseCliArgs
Cover the canonical parseCliArgs export in cli-web-branch.ts including
the new mcp mode, worktree flag (boolean and named forms), and existing
short flags, web mode flags, list flags, and positional message handling.

Also remove src/app-paths.js — a stale tracked output (last touched in
2022, missing GSD_HOME and webPreferencesPath exports). The test compile
script copies all of src/ over esbuild's output, so this stale .js was
shadowing the compiled app-paths in dist-test and breaking any test that
transitively imported it. No runtime path uses it (production loads from
dist/app-paths.js; jiti/tsx prefer the .ts source).

Satisfies require-tests.sh on PR #4162.
2026-04-13 22:37:14 -05:00
Jeremy McSpadden
13be3c58fe Merge pull request #4159 from jeremymcs/fix/4158-windows-junction-regression
fix(cli): use junction symlinks in merged node_modules path
2026-04-13 20:56:23 -05:00
Claude
679b3177a8 refactor(cli): slim down top-level src/ — dedup, unused fallbacks, onboarding
Pure deletion/deduplication pass on top-level src/*.ts. External behavior
unchanged; all targeted unit tests still pass.

cli.ts (−170 net lines)
  - Adopt canonical validateConfiguredModel from startup-model-validation.ts;
    delete the drifted local copy with hardcoded model fallbacks.
  - Import CliFlags + parseCliArgs from cli-web-branch.ts instead of keeping
    a second, 90%-identical parser; pass cliFlags directly into
    runWebCliBranch instead of re-parsing process.argv.
  - Extract 3 helpers for verbatim duplicates:
      * printNonTtyErrorAndExit (TTY gate, 2 call sites)
      * printExtensionErrors (extension load errors, 2 call sites)
      * reapplyValidatedModelOnFallback (post-createAgentSession fix, 2 sites)
  - Factor runHeadlessFromAuto helper shared by the `gsd auto` shorthand
    and the auto-piped-stdout redirect.
  - Collapse ensureRtkBootstrap from hand-rolled _done flag to a
    promise-memoized doRtkBootstrap.
  - Drop redundant validateConfiguredModel pre-createAgentSession calls
    (the post-createAgentSession call is the correct one per #2626).
  - Delete dead --version/-v and --help/-h fast paths (loader.ts already
    handles these before cli.ts is imported).

cli-web-branch.ts
  - Unify CliFlags with worktree, 'mcp' mode, and _selectedSessionPath.
  - Drop unused help?/version? flags (loader.ts intercepts them).

onboarding.ts
  - Add runStep<T>() helper with shared cancel/warn handling; collapse 4
    near-identical try/catch blocks around runLlmStep, runWebSearchStep,
    runRemoteQuestionsStep, runToolKeysStep.
  - Delete trivial isCancelError helper (inlined as p.isCancel).
  - Rewrite loadPico() adapter to build PicoModule from chalk so we can
    drop the redundant picocolors dependency.

package.json / package-lock.json
  - Remove picocolors direct dep (chalk remains the single color library).
2026-04-14 01:51:22 +00:00
Jeremy McSpadden
4ab053d9ba Merge pull request #4156 from jeremymcs/fix/4144-claude-code-subturn-regression
fix(tui): reset segment state on claude-code sub-turn shrink
2026-04-13 20:45:36 -05:00
Jeremy
a80d9e0edf fix(cli): use junction symlinks in merged node_modules path 2026-04-13 20:44:08 -05:00
Jeremy
2bf2313395 test(tui): finalize sub-turn regression tests to stop pinned spinner
The two new sub-turn shrink regression tests created a pinned
DynamicBorder (via message_update with pinnable text + tool) but never
emitted message_end, so the spinner's setInterval kept the test process
alive until CI timed out after 15 minutes. Append a message_end to
each test so the module-level pinnedBorder is torn down.
2026-04-13 20:36:52 -05:00
Jeremy
03b7142400 fix(tui): reset segment state on claude-code sub-turn shrink
Commit c8c416802 (#4144) introduced module-level renderedSegments state
to track interleaved text/tool components per assistant turn, but never
reset it when an adapter shrinks streamingMessage.content[] back to 0/1
at a provider sub-turn boundary within one assistant lifecycle (the
claude-code adapter does this). Consequence chain: the segment walker
finds the stale text-run entry at startIndex=0, calls updateContent on
it with the new (shrunk) message, and the in-place edit destroys the
prior sub-turn's visible text. New tool blocks at contentIndex=1 then
collide with stale registrations, causing visual ordering corruption.
hasToolsInTurn stays sticky-true and lastPinnedText never clears, so
the pinned "Working - Latest Output" mirror freezes on the pre-shrink
snapshot.

Track lastContentLength explicitly. On shrink, clear renderedSegments,
reset lastPinnedText, and reset lastProcessedContentIndex so the
walker treats the new sub-turn as fresh segments that append after
prior sub-turn children. Prior history stays rendered as frozen
components; pendingTools and the spinner border are untouched.

Adds two regression tests in chat-controller-ordering.test.ts: one
verifies prior sub-turn components are not overwritten and new tools
append in content[] order after a shrink, the other verifies the
pinned markdown updates from the first sub-turn's text to the second
sub-turn's text across a shrink boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 19:58:11 -05:00
Nils Reeh
365b36d96a fix(gsd): set completed_at when reconciling task status to complete
reconcileSliceTasks called updateTaskStatus without a completedAt
timestamp, leaving tasks.completed_at NULL for all tasks completed
via the file-existence reconcile path.

Closes #4129

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 02:13:38 +02:00
Jeremy McSpadden
fc7d195e09 Merge pull request #4150 from jeremymcs/claude/debug-tui-auto-mode-vCnxA
Split Container.clear() into clear() and detachChildren()
2026-04-13 18:39:33 -05:00
Claude
33d9a26dd7 fix(tui): keep AUTO-mode widgets alive and drop duplicate health panel
InteractiveMode.renderWidgets() called Container.clear() on the
widgetContainerAbove/Below render mounts, which disposed every mounted
extension widget and then re-added the now-dead components. In AUTO mode
updateProgressWidget re-registers gsd-progress on every unit dispatch,
so gsd-notifications and gsd-health had their refresh timers and store
subscriptions killed after the first dispatch. Renders kept returning
the widgets' frozen cachedLines, making them look alive but never update
(/gsd notifications clear appeared to do nothing, belowEditor last-commit
went stale while the top-of-screen dashboard stayed correct).

Split detach from dispose: add Container.detachChildren() and use it from
the two widget-mount call sites. clear() still disposes for every other
caller (chat, editor, status, pinned-message containers). The
extensionWidgets* maps remain the single owner of widget disposal via
removeExisting() and clearExtensionWidgets().

While in AUTO, gsd-progress duplicates gsd-health on last commit, cost/
budget, and the health signal. Make gsd-progress the single source of
truth: hide gsd-health from auto-start and re-register it from every
exit point in auto.ts (lock-lost stop, cleanupAfterLoopExit !paused
guard, stopAuto, pauseAuto). gsd-notifications stays visible — it is
independent state and, with the detach fix, its subscription + 5s
refresh actually work again.

Tests: Container.detachChildren()/clear() contract guards added to
packages/pi-tui/src/__tests__/tui.test.ts. health-widget,
notification-{store,widget,overlay}, notifications-handler, notifications,
and auto-paused-ui-cleanup suites all pass.
2026-04-13 23:30:25 +00:00
Jeremy McSpadden
96f77d8ff8 Merge pull request #4037 from mastertyko/fix/3925-claude-code-overflow-detect
fix(pi-ai): detect claude-code overflow text
2026-04-13 18:21:36 -05:00
Jeremy McSpadden
ef6abf48bc Merge pull request #4147 from NilsR0711/fix/bun-update-command
fix(gsd): use bun for update when installed via Bun
2026-04-13 18:03:03 -05:00
Jeremy McSpadden
cdd257e59a Merge pull request #4059 from mastertyko/fix/4054-compaction-safe-role-markers
fix(pi-coding-agent): use safe compaction role markers
2026-04-13 17:58:04 -05:00
Nils Reeh
e3e72174fa fix(gsd): use bun for update when installed via Bun (#4145)
When GSD is installed with `bun add -g`, running `gsd update` or
`/gsd update` previously shelled out to `npm install -g`, which fails
with EACCES on systems where npm has no write access to the global
node_modules directory.

Adds `resolveInstallCommand(pkg)` to `update-check.ts` that returns
`bun add -g <pkg>` when `process.versions.bun` is defined (i.e. the
current runtime is Bun), and `npm install -g <pkg>` otherwise.  All
three update paths — `update-cmd.ts`, `commands-handlers.ts`, and the
interactive startup prompt in `update-check.ts` — now use this helper,
including the fallback error message shown to the user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:52:08 +02:00
Jeremy McSpadden
6ba83c83c2 Merge pull request #4146 from jeremymcs/fix/4144-inline-tool-calls
fix(tui): render assistant tool calls inline with text instead of grouped at end
2026-04-13 17:41:41 -05:00
Jeremy
c8c416802f fix(tui): render assistant tool calls inline with text instead of grouped at end
Previously the chat-controller created one AssistantMessageComponent per
assistant message and removed/re-appended it to the chat container's tail
on every tool block, forcing all narration after every tool execution
regardless of stream order. Users had to scroll up to read text that was
written before each tool call.

Replace the reorder hack with a stream-order segment walker that walks
content[] left-to-right, collapses contiguous text/thinking blocks into
text-run segments, emits one segment per tool block, and append-only adds
new segments to chatContainer. AssistantMessageComponent gains a
ContentRange API so a single message can spawn multiple text-run
components, plus a separate showMetadata flag so timestamp/error footers
render only on the trailing segment without duplicating earlier text.

Adds a regression test that streams [text, tool, text, tool, text] and
asserts both interleaved order and per-segment rendered text content.

Closes #4144
2026-04-13 17:23:17 -05:00
Jeremy McSpadden
1ec1a8c4c4 Merge pull request #4060 from mastertyko/fix/3917-claude-code-effort
feat(claude-code): pass thinking level as effort
2026-04-13 16:07:59 -05:00
Jeremy McSpadden
e114f458fd Merge pull request #4141 from jeremymcs/claude/gsd-bootstrap-deep-dive-bQo8K
feat(gsd): extend flat-rate provider detection to custom/externalCli providers
2026-04-13 16:07:24 -05:00
Jeremy
8cf8d2bcf2 fix(gsd): restore isAutoMode plumbing and workflow-logger catch in auto-model-selection
CI on #4141 failed because threading an explicit flatRateCtx parameter
through resolvePreferredModelConfig broke two contracts the test suite
locks in:

  1. interactive-routing-bypass (#3962) asserts that
     resolvePreferredModelConfig is invoked with exactly three positional
     arguments and that its `if (!isAutoMode) return undefined` guard
     lives within the first 600 chars of the function body. The new
     flatRateCtx param + JSDoc pushed the guard past that window and
     lengthened the call site.

  2. silent-catch-diagnostics (#3348) requires migrated files to route
     through workflow-logger instead of leaving empty catch blocks. The
     new buildFlatRateContext() swallowed registry lookup errors with a
     comment-only catch.

Fix both without regressing flat-rate detection:

- Hang the flat-rate context off autoModeStartModel itself via an
  optional `flatRateCtx` field. selectAndApplyModel now enriches
  autoModeStartModel up front (preserving the variable name) and
  resolvePreferredModelConfig reads autoModeStartModel.flatRateCtx —
  signature shrinks back to three params, call site returns to the
  3-arg form the test anchors on.
- Replace the empty catch in buildFlatRateContext() with a
  logWarning("dispatch", ...) that surfaces the lookup failure while
  still falling through with authMode undefined, matching the
  fail-closed policy everywhere else in the file.
2026-04-13 16:00:01 -05:00
Claude
9a93563a64 feat(gsd): extend flat-rate provider detection to custom/externalCli providers
The 3-entry hard-coded FLAT_RATE_PROVIDERS set in auto-model-selection.ts
treated only github-copilot/copilot/claude-code as flat-rate, so dynamic
routing would happily downgrade units on user-registered subscription
proxies and any externalCli CLI wrapper — quality loss with no cost
benefit for users whose provider charges a flat rate per request.

Make isFlatRateProvider extensible by composing three signals:

  1. Built-in list (unchanged, wins first for regression safety).
  2. externalCli auto-detection via ctx.modelRegistry.getProviderAuthMode()
     — any CLI wrapper around the user's subscription is inherently
     flat-rate.
  3. User-declared `flat_rate_providers` preference for private
     subscription-backed proxies, enterprise-gated deployments, and custom
     CLI wrappers the built-in list doesn't know about.

Add a buildFlatRateContext() helper so every call site constructs the
context the same way and degrades gracefully when ctx/prefs/registry are
unavailable (never breaks flat-rate detection).

Thread the context through:

- resolvePreferredModelConfig (routing synthesis guard)
- selectAndApplyModel primary-model and fallback provider checks
- auto-start.ts dynamic-routing banner so the startup message matches
  dispatch-time reality

Preferences:
- Add `flat_rate_providers?: string[]` to GSDPreferences and
  KNOWN_PREFERENCE_KEYS in preferences-types.ts.
- Add a string-array validator in preferences-validation.ts that trims
  whitespace and drops empty entries.

Tests:
- Extend flat-rate-routing-guard.test.ts with 13 new cases covering
  externalCli auto-detection, userFlatRate preference matching
  (case-insensitive), combined signals, buildFlatRateContext() behavior
  (including registry-lookup-throws and non-canonical auth-mode
  responses), plus regression cases for the built-in list.
- Add 5 validator cases in preferences.test.ts for the new
  flat_rate_providers field (string-array accepted, whitespace trimmed,
  non-array rejected, non-string elements rejected, known-key warning
  check).
2026-04-13 20:25:26 +00:00
Jeremy McSpadden
24f51fd76b Merge pull request #4138 from jeremymcs/claude/investigate-issue-4122-6Vi1I
fix(gsd): preserve custom-model selection on /gsd auto bootstrap (#4122)
2026-04-13 15:00:58 -05:00
Jeremy
4fad01694c Merge upstream/main into fix/4122 custom-provider bootstrap 2026-04-13 14:05:12 -05:00
Claude
73558e7557 fix(gsd): preserve custom-model selection on /gsd auto bootstrap (#4122)
When a user picks a custom-provider model via /gsd model (Ollama, vLLM,
LM Studio, OpenAI-compatible proxies — anything defined in
~/.gsd/agent/models.json) and then runs /gsd auto, the bootstrap silently
swaps it out for whichever model PREFERENCES.md happens to list. That
model is invariably a built-in provider (claude-code, anthropic) the user
isn't logged into, so auto-mode immediately fails with
"Not logged in · Please run /login", pauses, and resets the session to
claude-code/claude-sonnet-4-6.

Root cause: #3517 made resolveDefaultSessionModel() (PREFERENCES.md) take
priority over ctx.model (settings.json) in auto-start.ts. That fix was
correct for the scenario where settings.json had a stale built-in default
but PREFERENCES.md was freshly configured, but it has no awareness of
custom providers — PREFERENCES.md cannot reference them, so honoring it
when the session provider is custom always discards the user's explicit
choice.

Add isCustomProvider() to preferences-models.ts which checks whether a
provider is declared in ~/.gsd/agent/models.json (with ~/.pi/agent
fallback). Read the file directly with JSON.parse to avoid pulling in
the model-registry at this call site, and treat any read or parse error
as not-custom so a malformed models.json never breaks bootstrap.

In bootstrapAutoSession(), when the session provider is custom, use
ctx.model directly. Otherwise fall through to the existing #3517
behavior (preferredModel ?? ctx.model).

Tests:
- New behavioral regression in model-isolation.test.ts that mirrors
  the auto-start.ts logic and verifies the four interesting cases:
  custom session beats PREFERENCES.md, built-in session still defers
  to PREFERENCES.md (#3517 preserved), custom session with no
  PREFERENCES.md uses ctx.model, and null ctx.model falls through.
- New string-grep guard in auto-start-model-capture.test.ts that the
  isCustomProvider() call is wired into the snapshot path.
- Updated #3517 grep to allow the new branching shape while still
  asserting preferredModel remains a snapshot source for built-ins.

https://claude.ai/code/session_01QLYCeiXWjSFPEXFxjkSLni
2026-04-13 17:53:32 +00:00