Adds a new setting 'respectGitignoreInPicker' (default: true) that
controls whether the @ file picker respects .gitignore when listing
files. When set to false, gitignored files appear in fuzzy search
results.
Wired through:
- CombinedAutocompleteProvider: new constructor option + setter
- SettingsManager: getter/setter with persistence
- Settings selector UI: toggle in settings panel
- InteractiveMode: reads setting at init, updates provider on change
* fix: prevent data loss on crash with atomic writes, file locking, and error handling
Wave 1 of failure recovery safeguards:
1. Atomic session file rewrites (tmp+rename) — _rewriteFile() and forkFrom()
now use atomicWriteFileSync to prevent session file corruption on crash
2. Atomic auto.lock writes — crash-recovery.ts writeLock() uses tmp+rename
so the crash detection system itself can't be corrupted
3. unhandledRejection handler — catches silent process death from unhandled
promise rejections in OAuth, extensions, LSP, or MCP connections
4. try/catch in emitToolCall — matches pattern used by emitUserBash,
emitContext, and emitToolResult to prevent extension handler crashes
from killing the entire agent turn
5. File locking on session appends — prevents concurrent pi instances from
interleaving partial JSON lines in session JSONL files using the same
proper-lockfile pattern established in auth-storage.ts and settings-manager.ts
* fix: add OAuth timeouts, RPC exit detection, and command context guards
Wave 2 of failure recovery safeguards:
1. OAuth fetch timeouts — all fetch() calls across all OAuth providers
(Anthropic, OpenAI Codex, Google Antigravity, Google Gemini CLI,
GitHub Copilot) now have 30-second AbortSignal.timeout() to prevent
indefinite hangs when OAuth servers are unresponsive
2. RPC subprocess exit detection — pending requests are now rejected
when the agent subprocess exits unexpectedly, preventing indefinite
hangs in the RPC client
3. Extension command context guards — default handlers for newSession,
fork, navigateTree, switchSession, and reload now throw explicit
errors instead of silently returning success when called before
bindCommandContext()
4. OAuth error detail preservation — token refresh errors now preserve
the original error as `cause` for better diagnostics
* fix: resource cleanup, LSP retry, and crash detection on session resume
Wave 3 of failure recovery safeguards:
1. Atomic completed-units.json cleanup — milestone completion writes
now use tmp+rename pattern for consistency with auto-recovery.ts
2. Bash temp file cleanup — track temp files created for large output
and register a process exit handler to clean them up
3. Settings write queue flush on shutdown — call settingsManager.flush()
during interactive mode shutdown so queued writes aren't lost
4. LSP initialization retry — wrap getOrCreateClient with up to 2 retries
with exponential backoff (1s, 2s) for transient spawn failures
5. Crash detection on session resume — wasInterrupted() checks if last
assistant turn had tool calls without results, shows warning on resume
* fix: blob garbage collection and LSP debug logging
Wave 4 of failure recovery safeguards:
1. Blob garbage collection — BlobStore.gc(referencedHashes) removes
orphaned blobs not referenced by any session file, plus totalSize()
for monitoring blob directory growth
2. LSP JSON parse error logging — malformed LSP messages are now logged
at debug level (when DEBUG env is set) instead of being silently dropped
Two fixes:
1. lsp/config.ts: Use `where.exe` instead of `which` on Windows.
MSYS's `which` returns POSIX paths (/c/Users/...) that Node's
spawn() can't execute. `where.exe` returns native Windows paths.
2. lsp/client.ts: Handle spawn ENOENT error gracefully. When the LSP
server binary doesn't exist, the error event now triggers a clean
exit instead of bubbling up and crashing auto-mode.
On Windows, process.cwd() returns backslash paths (C:\Users\name\...).
When these paths are injected into system prompts, worktree context
blocks, or tool results, the model copies them into bash commands.
Bash interprets backslashes as escape characters, silently stripping
them — producing invalid paths like 'C:Usersnamedevelopmentapp-name'.
This is not a regex hack — it's a proper cross-platform boundary:
- Filesystem operations (fs, path.join, spawn cwd) use native paths
unchanged. Node handles both separators correctly for I/O.
- LLM-visible text (prompts, tool results, extension messages) uses
toPosixPath() to normalize to forward slashes. C:/Users/name/...
is valid in Git Bash, WSL bash, PowerShell, and Node.js.
Changes:
- utils/path-display.ts: New toPosixPath() utility in pi-coding-agent
package (for system prompt) and shared extension module (for
extensions that can't import from the compiled package at dev time)
- system-prompt.ts: Normalize resolvedCwd before injecting into the
'Current working directory' line
- gsd/index.ts: Normalize all process.cwd() and originalBase paths in
worktree context blocks injected into the system prompt
- bg-shell/index.ts: Normalize cwd in tool result text (start, env
actions) that the model reads and may reference in commands
- path-display.test.ts: 9 regression tests covering toPosixPath
behavior and system prompt output verification. Includes a scanner
that fails if any Windows absolute paths with backslashes appear in
buildSystemPrompt() output.
Audit scope: Checked all process.cwd() usage across pi-coding-agent
and all bundled extensions. Filesystem-only paths (join, readFile,
spawn cwd, existsSync) are correct and left unchanged. Only paths
entering LLM text are normalized.
Anthropic rate limit reset windows are typically 60-120s. The previous 60s
default, combined with the +1s buffer in extractRetryAfterMs(), meant that
virtually all rate limit retries were immediately abandoned.
300s (5 min) covers the vast majority of rate limit windows and lets the
built-in retry logic work as intended.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: when the LLM runs `cmd &`, bash forks the process and
exits immediately. The forked process inherits Node's piped stdout/
stderr FDs. Node.js waits for all holders of those FDs to close before
firing the 'close' event — so the tool hangs until the background
process exits (which for a server is never).
Fix: add rewriteBackgroundCommand() in bash.ts. Before exec, detect
commands with a trailing & background operator and inject
>/dev/null 2>&1 before the & when stdout is not already redirected.
This severs the pipe inheritance so Node gets 'close' immediately
when the shell exits.
Guards:
- Commands already redirecting stdout (>, >>, &>, |) are not rewritten
- && (logical AND) is not affected
- & inside single-quoted strings is not affected
- A brief onUpdate advisory is surfaced when rewrite happens so the
LLM knows to prefer nohup/setsid for robust detachment
Export rewriteBackgroundCommand from pi-coding-agent for testability.
Tests: bash-background.test.ts — 12 cases covering no-op paths,
rewrite paths, compound commands, and already-safe nohup patterns.
Closes#733
- Fix loadStoredEnvKeys divergent provider lists: add telegram_bot and
custom-openai to wizard.ts (the canonical copy used by CLI), remove
dead duplicate from onboarding.ts
- Security: add SAFE_COMMAND_PREFIXES allowlist to resolveConfigValue
to prevent arbitrary RCE via settings.json shell commands
- Security: add TOFU (Trust On First Use) model for project-local
extensions — skip untrusted .pi/extensions/ with stderr warning
- Performance: debounce sql.js MemoryStorage persistence (500ms window)
so rapid mutations coalesce into a single db.export()+writeFileSync
- Fix double lstatSync call in tool-bootstrap.ts isRegularFile
- Add 26 new tests covering all changes
Two fixes for the model configuration bleeding between simultaneous
GSD instances that share the same global settings.json.
## Root Cause
1. `setDefaultModelAndProvider()` always persisted to `~/.gsd/agent/settings.json`
(global), so when either instance's interactive mode changed models (via
Ctrl+P or /model), it overwrote the other instance's saved default.
2. When auto-mode dispatched a new unit (after context wipe), if no
per-unit-type model preference was configured, the session picked up
the default from the now-contaminated global settings file.
## Fix 1: Project-scoped model persistence (settings-manager.ts)
`setDefaultModelAndProvider()`, `setDefaultModel()`, and `setDefaultProvider()`
now persist to project-level settings (`.pi/settings.json`) when a project
settings file exists, falling back to global only when no project context
is available. This prevents concurrent instances from overwriting each
other's model choice.
Added `hasProjectSettingsFile()` helper to detect project context.
## Fix 2: Auto-mode model capture (auto.ts)
Captures the session's model at auto-mode start (`autoModeStartModel`).
At each unit dispatch, if no model preference is configured for the unit
type, the captured model is re-applied with `persist: false`. This
ensures each auto-mode session maintains its own model regardless of
what other instances write to the shared settings file.
## Tests
3 new tests covering:
- Project settings file isolates model from global
- Two projects have independent model configs
- autoModeStartModel concept prevents model drift
All 448 existing tests pass.
Fixes#650
Tools (write, read, edit, bash) capture cwd at creation time via
createWriteTool(cwd), createReadTool(cwd), etc. When auto-mode
enters a worktree, process.cwd() changes but tools were not
recreated — they continued resolving relative paths against the
original project root.
This caused artifacts to be written to the main project's .gsd/
directory instead of the worktree's .gsd/ directory. The dispatcher
then couldn't find the artifact at the expected worktree path and
retried the unit indefinitely.
Fix: detect cwd change in newSession() and call _buildRuntime()
to recreate tools with the updated cwd. This is a targeted rebuild
that only fires when cwd actually changed (typically once per
auto-mode session when entering/exiting a worktree).
Fixes#633
LSP was never activated in interactive sessions because the default
active tools list hardcoded only read/bash/edit/write. This adds lsp
to that list and ships four new capabilities alongside edit sync and
stronger prompt guidance.
- Add "lsp" to default active tools in agent-session.ts
- New actions: incoming_calls, outgoing_calls, format, signature
- Wire edit/write tools to notify LSP clients on file changes
- Strengthen system prompt and GSD prompt with full LSP operation catalog
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: allow extensions to use 'yaml' and rework frontmatter parsing
* feat: add run-hook command for manual hook execution
* fix: sanitize slashes in unitType for runtime file paths
* fix: avoid native hangs in gsd auto paths
* fix: use .js extension in edit-diff.test.ts import for tsc compatibility
* fix: prevent OOM on large file diffs and implement context-line windowing
- Add size guard (MAX_DP_CELLS=4M) to buildLineDiff that falls back to a
linear-time prefix/suffix matching algorithm for large files, preventing
the O(n*m) DP table from causing OOM crashes
- Implement contextLines parameter in generateDiffString so only lines
within N lines of a change are rendered (with "..." separators), matching
unified diff behavior — the parameter was previously accepted but ignored
- Add tests for both context windowing and large-file fallback
---------
Co-authored-by: TÂCHES <afromanguy@me.com>
Add "success" to notify type union across ExtensionUIContext, interactive
mode, and RPC mode implementations. Fix null safety for readFileSync and
contextUsage.percent in auto.ts. Add discriminated union narrowing for
dispatch results. Add string type guards for select() return values in
commands.ts. Align ProviderErrorPauseUI notify signature. Simplify
AuthStorage return type.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When auto-mode creates a worktree and chdir's into it, the Node process
cwd changes but AgentSession._cwd stays frozen at the original path.
Every newSession() builds a system prompt telling the LLM "Current
working directory: /original/path", so the LLM cd's back there and
writes files to the wrong location.
Update _cwd = process.cwd() at the start of newSession() so the system
prompt reflects the actual working directory after chdir.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add startup performance analysis and optimization plan
Profiled GSD CLI startup finding 2.2s for --version and ~3.8s for
interactive mode. Identified 5 root causes with measured timings and
created a phased optimization plan targeting <0.2s for --version
and ~0.8s for interactive startup.
* perf: speed up GSD startup with lazy loading and fast paths
- Fast-path --version/-v and --help/-h in loader.ts before importing
any heavy dependencies (2.2s → 0.15s, 14x faster)
- Lazy-load undici (~200ms) only when HTTP_PROXY env vars are set
- Skip initResources cpSync when managed-resources.json version
matches current GSD version (~128ms saved per launch)
- Lazy-load Mistral SDK (~369ms) on first API call instead of startup
- Lazy-load Google GenAI SDK (~186ms) on first API call instead of
startup
- Parallelize extension loading with Promise.all() instead of
sequential for-loop
---------
Co-authored-by: TÂCHES <afromanguy@me.com>
GitHub Copilot users with Claude models got 400 errors because the native
Anthropic web_search_20250305 tool was injected into requests to Copilot's
API proxy, which doesn't support it. The root cause was that model_select
never fires before the first API request on new sessions, so the fallback
heuristic (model name starts with "claude-") couldn't distinguish direct
Anthropic from proxied providers.
Fix: pass the resolved Model object through to the before_provider_request
event so extensions can check model.provider directly instead of relying on
model name heuristics.
Moves extension tool_call/tool_result interception from wrapToolsWithExtensions
(which fires inside the agent loop, bypassing event settlement) to
beforeToolCall/afterToolCall hooks that await _agentEventQueue. This ensures
extensions always see settled state — including the appended assistant message —
even when tools execute in parallel.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Ollama Cloud (ollama.com) as a built-in provider with both model
hosting and web search/fetch capabilities.
Model provider:
- 13 curated models via OpenAI-compatible API (Llama 3.1, Qwen 3,
DeepSeek R1, Gemma 3, Mistral, Phi-4, GPT-OSS)
- Auth via OLLAMA_API_KEY environment variable
- Registered in onboarding, env hydration, and model resolver
Web tool provider:
- Search via POST ollama.com/api/web_search
- Page fetch via POST ollama.com/api/web_fetch (fallback after Jina)
- Added as third search provider option alongside Tavily and Brave
- /search-provider command updated with ollama option
Closes#430
When all credentials for a provider are exhausted, the system now
automatically falls back to the next available provider in a
user-configured fallback chain. Higher-priority providers are
restored automatically when their backoff expires.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Anthropic's 429 responses include retry-after and x-ratelimit-reset-*
headers that tell us exactly when to retry. Previously we ignored these
and used exponential backoff (2s, 4s, 8s), which is both wrong and
misleading in the UI countdown.
- Add retryAfterMs to AssistantMessage as the structured carrier
- Extract retry-after / x-ratelimit-reset-requests / x-ratelimit-reset-tokens
from Anthropic SDK APIError.headers in the provider catch block
- Session uses retryAfterMs when present (capped by maxDelayMs=60s),
falls back to exponential backoff for errors with no timing hint
The UI countdown now shows the actual Anthropic reset time. No UI changes needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: replace pure-JS xxHash32 with native Rust implementation via napi
The hashline edit tool calls xxHash32 on every line of every file read/edit.
Moving this to a native Rust implementation (xxhash-rust crate) eliminates
JS overhead for this hot path. Hash output is identical -- verified by tests
comparing native vs JS reference across 11 input vectors including empty
strings, short/long inputs, unicode, and seeded variants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: use typed native interface and remove version-drag comment in xxhash wrapper
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the multi-pass JS pipeline (TextDecoder → stripAnsi → sanitizeBinaryOutput)
in bash-executor.ts with a single native Rust call that handles UTF-8 decoding,
ANSI stripping, binary sanitization, and CR removal in one pass.
Key features:
- StreamState tracks incomplete UTF-8 and ANSI sequences across chunk boundaries
- Standalone stripAnsiNative() and sanitizeBinaryOutputNative() for use elsewhere
- Comprehensive test coverage for split multibyte, split ANSI, binary data
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add the 1M context variant of Claude Opus 4.6 to the model registry
and fix model resolver to try exact match before glob detection, so
model IDs containing bracket characters (like [1m]) are not
misinterpreted as glob patterns.
* feat: add task isolation for subagent filesystem safety
Subagents can run in isolated git worktrees (or FUSE overlays on Linux)
so concurrent tasks don't stomp on each other's files. Changes are
captured as unified diffs and merged back via git apply.
- New isolation.ts module with worktree and FUSE overlay backends
- TaskIsolationSettings in settings-manager (mode + merge strategy)
- isolated parameter on the subagent tool schema
- Baseline capture/apply mirrors the parent repo's dirty state
- Process exit handler for best-effort cleanup of stale worktrees
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct delta capture to exclude parent baseline state
The worktree backend now commits a baseline snapshot after applying the
parent's dirty state, so captureDeltaPatch diffs only the subagent's
actual changes against the post-baseline HEAD (not the original HEAD).
The FUSE overlay backend tracks the parent's dirty file set at mount
time and filters the upper dir during delta capture to exclude inherited
dirty files.
Also removes dead code: findGitRoot (unused), readIsolationMergeStrategy
(exported but never called).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The async-jobs PR (#260) accidentally dropped `bashInterceptor` from the
Settings interface and the getBashInterceptorEnabled/getBashInterceptorRules
methods from SettingsManager, breaking the TypeScript build on main.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix cat rule to exclude heredoc syntax (cat <<EOF) via negative lookahead
- Fix write rule: exclude >> append and digit-prefixed fd redirects (2>)
using lookbehind (?<![|>\d])>(?!>)
- Add compileInterceptor() — pre-compiles rules once at construction time
instead of on every bash call; export CompiledInterceptor type
- Update createBashTool to use pre-compiled interceptor instance
- Add 33 unit tests covering all rules, edge cases, and pass-throughs
Regex-based pre-execution check in the bash tool blocks shell commands
(grep, cat, sed -i, etc.) when the dedicated replacement tool is available
in the session. Configurable via bashInterceptor settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support multiple API keys per provider with automatic rotation:
- AuthStorageData accepts single credential or array per provider
- Round-robin selection across credentials (no sessionId)
- Session-sticky hashing when sessionId is provided
- Credential backoff on rate limits (30s), quota exhaustion (30min),
server errors (20s)
- markUsageLimitReached() backs off failing credential and returns
whether an alternate is available
- Login accumulation: duplicate provider logins append API keys
instead of replacing
- Agent retry handler tries credential fallback before counting
against retry budget (immediate retry, no delay)
- All getApiKey call sites thread sessionId for sticky selection
Backward compatible: single credentials work unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the edit tool's hot-path diffing operations from JS to native Rust:
- `normalizeForFuzzyMatch`: single-pass Unicode normalization (smart quotes,
dashes, special spaces, trailing whitespace)
- `fuzzyFindText`: exact-then-fuzzy substring search with UTF-16 index
conversion for JS compatibility
- `generateDiff`: unified diff generation using the `similar` crate
(Myers' algorithm with optimizations)
The Rust module at `native/crates/engine/src/diff.rs` exposes three napi
functions. The TypeScript wrapper at `packages/native/src/diff/` follows
the existing module pattern. `edit-diff.ts` now delegates to native
implementations while keeping line-ending handling and file I/O in JS.
18 tests covering normalization, fuzzy matching (including UTF-16 index
correctness with emoji/surrogate pairs), and diff generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The find tool's default path spawned `fd` and used the `glob` npm package
to discover nested .gitignore files. The native @gsd/native glob module
handles gitignore traversal natively via Rust's `ignore` crate, making
both dependencies unnecessary for this code path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Check if the destination file exists before performing a move in
hashline-edit. If it does, return an error instead of silently
overwriting the file.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The delete operation in hashline-edit.ts wrapped both access() and
unlink() in a single try/catch. If access succeeded but unlink failed
(e.g., permissions), the error was silently swallowed and "Deleted" was
falsely reported. Now access and unlink have separate error handling:
access failures indicate the file doesn't exist, while unlink failures
propagate to the caller.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>