Add gsd_progress, gsd_roadmap, gsd_history, gsd_doctor, gsd_captures,
and gsd_knowledge tools that parse .gsd/ on disk — no session needed.
Inline lightweight readers in src/readers/ keep the package standalone
(zero new dependencies). 33 new tests, 64 total passing.
Users with existing lsp.json overrides referencing the old
"kotlin-language-server" key would silently lose their Kotlin
LSP config after the rename to "kotlin-lsp". LEGACY_ALIASES
map remaps old keys during mergeServers() so overrides still
merge correctly.
- Add BeforeModelSelectEvent interface and BeforeModelSelectResult type to types.ts
- Add on('before_model_select') subscription overload to ExtensionAPI interface
- Add emitBeforeModelSelect() method to ExtensionAPI interface and ExtensionRuntimeState
- Implement emitBeforeModelSelect() on ExtensionRunner using invokeHandlers (first-override-wins)
- Bind runner's emitBeforeModelSelect into shared runtime at construction time
- Wire emitBeforeModelSelect delegation through createExtensionAPI in loader.ts
The existing repairToolJson only handles YAML bullet lists (#2660).
Two additional malformation patterns from smaller models now cause
tool call failures and stuck retry loops:
1. XML parameter tags mixed into JSON values (#3403):
LLMs (especially Haiku-class) sometimes emit hybrid XML/JSON
syntax like <parameter name="X">value</parameter> inside
JSON string values. Add stripXmlParameterTags() to remove
the tags while preserving content.
2. Truncated numeric values (#3464):
Smaller models emit incomplete numbers like "exitCode": -,
or "durationMs": , when values are cut off mid-generation.
Add repairTruncatedNumbers() to replace these with 0.
Both repairs run before the existing YAML bullet repair phase.
The AJV validation layer (coerceTypes: true) then handles any
remaining string-to-number coercion.
Adds 13 new tests covering detection and repair for both patterns.
Closes#3464, closes#3403, addresses #3369
Explicit model changes aborted the active backoff sleep but left already-queued retry callbacks alive. That let stale provider retries continue after a user or runtime model switch, which could keep surfacing the old provider's backoff errors in the new session state.
Cancel the full retry generation on explicit model switches by clearing queued continue callbacks in RetryHandler, then cover the behavior with regression tests for both the session switch ordering and the queued-retry cancellation path.
The TUI slash dispatcher started treating any unrecognized /command as handled before session.prompt() could resolve extension commands, prompt templates, or /skill:* inputs. That blocked valid non-builtin slash commands and also let /export swallow unrelated /export* prefixes.
Move unknown-command detection to the interactive entry points, allow only known builtins or session-resolved slash commands through, gate /skill:* on the skill-command setting, and tighten /export matching to exact command tokens.
Move regression tests and override tests from standalone files into
the existing test files introduced by PR #666:
- resolve-config-value.test.ts: add REGRESSION #666 describe block
and setAllowedCommandPrefixes override tests
- url-utils.test.ts: add REGRESSION #666 describe block and
setFetchAllowedUrls override tests
- Delete: regression-666.test.ts, resolve-config-value-override.test.ts,
url-utils-override.test.ts
Same 59 tests, fewer files, tests live next to the code they test.
PR #666 introduced hardcoded SAFE_COMMAND_PREFIXES and SSRF URL
blocklists with no override mechanism. Users with non-standard
credential tools (sops, doppler, age, infisical) or needing to fetch
from internal URLs (self-hosted docs, VPN services) were silently
blocked with no recourse.
Add two global-only settings (ignored in project-level settings.json
to preserve the security property against malicious repos):
- allowedCommandPrefixes: replaces the built-in command allowlist
- fetchAllowedUrls: exempts hostnames from SSRF blocking
Both also support env var overrides (GSD_ALLOWED_COMMAND_PREFIXES,
GSD_FETCH_ALLOWED_URLS) for CI/container environments. Env vars
take precedence over settings.json.
Security model: global-only keys are stripped from project settings
at load time via stripGlobalOnlyKeys(), applied at all three
assignment points for this.projectSettings. The merge function
stays untouched — no future caller can accidentally skip stripping.
15 new tests covering override behavior, cache invalidation,
allowlist exemptions, and global-only enforcement.
Self-contained extension at src/resources/extensions/ollama/ that
auto-detects a running Ollama instance, discovers locally pulled models,
and registers them as a first-class provider with zero configuration.
Features:
- Auto-discovery of local models via /api/tags on session_start
- Capability detection (vision, reasoning, context window) for 40+ model families
- /ollama slash command with status, list, pull, remove, ps subcommands
- ollama_manage LLM-callable tool for agent-driven model operations
- Onboarding flow with auto-detect (no API key required)
- Non-blocking async probe — doesn't delay TUI paint
- Respects OLLAMA_HOST env var for non-default endpoints
Core changes (minimal):
- Add "ollama" to KnownProvider in pi-ai types
- Add "ollama" key resolution in env-api-keys.ts
- Add "ollama" default model in model-resolver.ts
- Add "Ollama (Local)" to onboarding wizard with probe flow
- Add extension-manifest.ts and extension-sort.ts to pi-coding-agent
with manifest reading and Kahn's BFS topological sort algorithm
- Add extensionPathsTransform hook to DefaultResourceLoader that runs
between path merging and loadExtensions() — enables pre-load
filtering and reordering without modifying pi internals
- Wire GSD's buildResourceLoader() to provide a transform that:
1. Filters ALL extensions (including community) through the GSD registry
2. Sorts in topological dependency order via sortExtensionPaths()
- Mark discoverAndLoadExtensions() as @deprecated (dead code path)
- Add 16 tests covering manifest reading, dependency sorting, cycles,
missing deps, and non-array deps
Previously, dependencies.extensions in manifests was decorative (sort
existed but was never called), and gsd extensions disable only worked
for bundled extensions. Community extensions in ~/.gsd/agent/extensions/
bypassed the registry entirely.
When an agent requests read(file, offset: 30) on a 13-line file, the
read tool threw "Offset 30 is beyond end of file" which propagated as
invalid JSON downstream during milestone completion. Now clamps the
offset to the last line and prepends a notice, allowing the agent to
continue with valid content.
Fixes both read.ts and hashline-read.ts variants.
Closes#3007
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a session grows beyond the context window of available models,
generateSummary() now detects the overflow and falls back to chunked
summarization: split messages into context-fitting chunks, summarize
the first chunk, then iteratively merge subsequent chunks using the
existing UPDATE_SUMMARIZATION_PROMPT path.
Closes#2932
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Three spawn call sites were missing `shell: process.platform === "win32"`,
causing ENOENT/EINVAL errors on Windows where npm-installed tools are .cmd
batch scripts that require shell resolution:
- exec.ts: hardcoded `shell: false` -> platform-guarded
- lsp/index.ts: missing shell option on project-type command spawn
- lsp/lspmux.ts: missing shell option on lspmux binary spawn
Adds a structural regression test that scans all spawn sites invoking
user-facing binaries and asserts the Windows shell guard is present.
Closes#2854
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: wrap custom messages with system notification prefix in LLM context
Background job completion notifications (delivered as custom messages via
sendMessage with deliverAs: "followUp") were converted to plain role: "user"
messages in convertToLlm(), making the LLM indistinguishable from actual
human input. This caused the agent to confuse background task output with
user messages, responding to job completions as if the user had typed them.
Wrap all custom messages with a clear system notification prefix that
includes the customType and an explicit instruction that the content is
an automated system event, not user input. This follows the same pattern
used by branchSummary and compactionSummary messages which already use
structured prefixes/suffixes.
Closes#3026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve TS import extension and type errors in messages test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a session accumulates many images (screenshots, file reads), the
Anthropic API enforces a 2000px dimension limit for "many-image requests"
and returns a 400 error. Previously this error was not classified as
retryable, causing the session to get permanently stuck in an error loop
with no recovery path.
This adds automatic recovery: detect the specific "image dimensions exceed
max allowed size for many-image requests" error, strip older images from
the conversation history (keeping the 5 most recent), and auto-retry.
Also handles manual retry (continue/retry) by downsizing before retrying.
Closes#2874
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
* 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>
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>
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>
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>
Addresses 30+ issues found in a full review of the interactive TUI spanning
layout/visual, user flow, message rendering, and state management dimensions.
Critical (state/memory):
- Fix onBranchChange unsubscribe function being discarded; store and call in stop()
- Add onThemeChange cleanup in stop() to prevent stale callback retention
- Resolve getUserInput() Promise on shutdown so run() while-loop exits cleanly
- Serialize concurrent message_update event handlers via Promise chain to prevent
duplicate ToolExecutionComponent creation under rapid streaming
- Add cleanup of customFooter, customHeader, autocompleteProvider, and extension
widgets in stop() to prevent timer/watcher leaks
Major (UX/flow):
- Add two-step confirmation for provider auth removal (r key) — matches session
delete pattern; first press shows confirm hint, second press executes
- Normalize list navigation wrapping: oauth-selector and session-selector now
wrap at boundaries, consistent with all other selectors
- Ctrl+C in scoped-models-selector now always cancels modal immediately instead
of clearing search first
- Config-selector position indicator now counts only selectable items, excluding
non-selectable group headers from both numerator and denominator
- user-message-selector auto-dismiss replaced setTimeout(100) with
Promise.resolve().then() to eliminate 100ms flicker
- Add "Unknown command: /foo. Type /help for available commands." feedback for
unrecognized slash commands instead of silently submitting as chat
- Fix dead-end input path: submitPromptsDirectly=false now dispatches prompt
- Wrap session.prompt in isCompacting path with try/catch (was missing, other
path had it)
- Add Esc-to-close hint to provider-manager footer (was undocumented)
Rendering bugs:
- Remove identical dead-code else branch in assistant-message spacing logic
- Add 20-line truncation to generic/unknown tool JSON rendering (was unbounded)
- bash-execution updateDisplay() now uses stored _borderColorKey so
excludeFromContext dim styling is preserved on re-render
- Fix countdown-timer dispose race: _disposed flag prevents extra tick after
clearInterval
- extension-selector nextSelectable() guard prevents cursor landing on separator
- extension-input now rejects empty/whitespace-only submissions
- Normalize bordered-loader spacing: non-cancellable variant no longer adds
orphaned spacer before bottom border
Visual/theme:
- daxnuts.ts center() replaced naive ANSI regex with visibleWidth() from
@gsd/pi-tui for correct true-color sequence handling
- Remove incorrect mistral.ai URL from daxnuts component
- armin.ts now centers art using same visibleWidth approach as daxnuts
- Dark theme warning color: #ffff00 → #e6b800 (muted amber, less harsh)
- dynamic-border default color function wrapped in try/catch to guard against
undefined theme in jiti-loaded extension contexts
- Footer stats grouped with · separator; cache labels changed from R/W to cr:/cw:
- Replace raw \x1b[1m ANSI codes in custom-message, branch-summary-message,
compaction-summary-message, skill-invocation-message with theme.bold()
- welcome-screen visLen now uses strip-ansi instead of hand-rolled regex
Performance:
- diff.ts parseDiffLine regex: [+-\s] → [+\- ] (space only, not all whitespace)
- tab replacement width: 3 spaces → 4 spaces (standard) in both diff.ts and
tool-execution.ts
- chat-controller message_update: skip already-processed content blocks using
lastProcessedContentIndex to reduce O(n) scan per event
Saves in-progress daemon work from M005-m138xe that was sitting uncommitted.
Includes orchestrator expansion, event bridge/formatter enhancements,
message batcher tweaks, and discord bot additions.
Tests cover: provider registration, base URL + API type, reasoning +
context window specs, and non-collision with generated zai models.
Required by CI lint gate (require-tests.sh).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>