@f5-sales-demo/xcsh 21.2.0 → 21.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "21.2.0",
21
- "commit": "d217f62f68fae54fe18d88c7d90a8f7dc4703c0d",
22
- "shortCommit": "d217f62",
20
+ "version": "21.4.0",
21
+ "commit": "f4df1169036003716fae08f7825efe0483b03fb4",
22
+ "shortCommit": "f4df116",
23
23
  "branch": "main",
24
- "tag": "v21.2.0",
25
- "commitDate": "2026-08-30T03:18:04+00:00",
26
- "buildDate": "2026-08-30T03:46:25.618Z",
24
+ "tag": "v21.4.0",
25
+ "commitDate": "2026-09-01T02:20:18+00:00",
26
+ "buildDate": "2026-09-01T02:51:00.403Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/d217f62f68fae54fe18d88c7d90a8f7dc4703c0d",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.2.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/f4df1169036003716fae08f7825efe0483b03fb4",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.4.0"
33
33
  };
@@ -112,7 +112,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
112
112
  "de/tui/tui.md": "---\ntitle: TUI-Integration für Erweiterungen und benutzerdefinierte Tools\ndescription: >-\n TUI-Integrationsvertrag für Erweiterungen, benutzerdefinierte Tools und\n benutzerdefinierte Renderer.\nsidebar:\n order: 1\n label: Erweiterungsintegration\ni18n:\n sourceHash: 47f8f2b2045e\n translator: machine\n---\n\n# TUI-Integration für Erweiterungen und benutzerdefinierte Tools\n\nDieses Dokument behandelt den **aktuellen** TUI-Vertrag, der von `packages/coding-agent` und `packages/tui` für Erweiterungs-UI, benutzerdefinierte Tool-UI und benutzerdefinierte Renderer verwendet wird.\n\n## Was dieses Subsystem ist\n\nDie Laufzeitumgebung hat zwei Schichten:\n\n- **Rendering-Engine (`packages/tui`)**: differenzieller Terminal-Renderer, Eingabeverteilung, Fokus, Overlays, Cursorplatzierung.\n- **Integrationsschicht (`packages/coding-agent`)**: bindet Erweiterungs-/benutzerdefinierte-Tool-Komponenten ein, verbindet Tastenkürzel/Theme und stellt den Editor-Zustand wieder her.\n\n## Laufzeitverhalten nach Modus\n\n| Modus | Verfügbarkeit von `ctx.ui.custom(...)` | Hinweise |\n| --- | --- | --- |\n| Interaktive TUI | Unterstützt | Die Komponente wird im Editor-Bereich eingebunden, fokussiert und muss `done(result)` aufrufen, um aufzulösen. |\n| Hintergrund/Headless | Nicht interaktiv | UI-Kontext ist ein No-Op (`hasUI === false`). |\n| RPC-Modus | Nicht unterstützt | `custom()` gibt `Promise<never>` zurück und bindet keine TUI-Komponenten ein. |\n\nWenn Ihre Erweiterung/Ihr Tool im nicht-interaktiven Modus laufen kann, prüfen Sie mit `ctx.hasUI` / `pi.hasUI`.\n\n## Kern-Komponentenvertrag (`@f5-sales-demo/pi-tui`)\n\n`packages/tui/src/tui.ts` definiert:\n\n```ts\nexport interface Component {\n render(width: number): string[];\n handleInput?(data: string): void;\n wantsKeyRelease?: boolean;\n invalidate(): void;\n}\n```\n\n`Focusable` ist separat:\n\n```ts\nexport interface Focusable {\n focused: boolean;\n}\n```\n\nDas Cursorverhalten verwendet `CURSOR_MARKER` (nicht `getCursorPosition`). Fokussierte Komponenten geben den Marker im gerenderten Text aus; `TUI` extrahiert ihn und positioniert den Hardware-Cursor.\n\n## Rendering-Einschränkungen (Terminal-Sicherheit)\n\nIhre `render(width)`-Ausgabe muss terminal-sicher sein:\n\n1. **Überschreiten Sie niemals `width` in einer Zeile**. Der Renderer wirft einen Fehler, wenn eine Nicht-Bild-Zeile überläuft.\n2. **Messen Sie die visuelle Breite**, nicht die String-Länge: verwenden Sie `visibleWidth()`.\n3. **Kürzen/umbrechen Sie ANSI-bewussten Text** mit `truncateToWidth()` / `wrapTextWithAnsi()`.\n4. **Bereinigen Sie Tabs/Inhalte** aus externen Quellen mit `replaceTabs()` (und übergeordneten Bereinigungsfunktionen in den Render-Pfaden von coding-agent).\n\nMinimales Muster:\n\n```ts\nimport { replaceTabs, truncateToWidth } from \"@f5-sales-demo/pi-tui\";\n\nrender(width: number): string[] {\n return this.lines.map(line => truncateToWidth(replaceTabs(line), width));\n}\n```\n\n## Eingabeverarbeitung und Tastenkürzel\n\n### Rohe Tastenerkennung\n\nVerwenden Sie `matchesKey(data, \"...\")` für Navigationstasten und Kombinationen.\n\n### Benutzer-konfigurierte App-Tastenkürzel beachten\n\nErweiterungs-UI-Factories erhalten einen `KeybindingsManager` (interaktiver Modus), damit Sie zugeordnete Aktionen statt hartcodierter Tasten verwenden können:\n\n```ts\nif (keybindings.matches(data, \"interrupt\")) {\n done(undefined);\n return;\n}\n```\n\n### Tastenfreigabe-/Wiederholungsereignisse\n\nTastenfreigabe-Ereignisse werden gefiltert, es sei denn, Ihre Komponente setzt:\n\n```ts\nwantsKeyRelease = true;\n```\n\nVerwenden Sie dann bei Bedarf `isKeyRelease()` / `isKeyRepeat()`.\n\n## Fokus, Overlays und Cursor\n\n- `TUI.setFocus(component)` leitet Eingaben an diese Komponente weiter.\n- Overlay-APIs existieren in `TUI` (`showOverlay`, `OverlayHandle`), aber die Einbindung über `ctx.ui.custom` einer Erweiterung im interaktiven Modus ersetzt derzeit direkt den Editor-Komponentenbereich.\n- Die Option `custom(..., options?: { overlay?: boolean })` existiert in Erweiterungstypen; die interaktive Erweiterungseinbindung ignoriert diese Option derzeit.\n\n## Einbindungspunkte und Rückgabeverträge\n\n## 1) Erweiterungs-UI (`ExtensionUIContext`)\n\nAktuelle Signatur (`extensibility/extensions/types.ts`):\n\n```ts\ncustom<T>(\n factory: (\n tui: TUI,\n theme: Theme,\n keybindings: KeybindingsManager,\n done: (result: T) => void,\n ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,\n options?: { overlay?: boolean },\n): Promise<T>\n```\n\nVerhalten im interaktiven Modus (`extension-ui-controller.ts`):\n\n- Speichert den Editor-Text.\n- Ersetzt die Editor-Komponente durch Ihre Komponente.\n- Fokussiert Ihre Komponente.\n- Bei `done(result)`: ruft `component.dispose?.()` auf, stellt Editor + Text wieder her, fokussiert den Editor, löst das Promise auf.\n\nDaher ist `done(...)` für die Fertigstellung zwingend erforderlich.\n\n## 2) Hook/Custom-Tool-UI-Kontext (Legacy-Typisierung)\n\n`HookUIContext.custom` ist in Hook/Custom-Tool-Typen als `(tui, theme, done)` typisiert.\nDie zugrunde liegende interaktive Implementierung ruft Factories mit `(tui, theme, keybindings, done)` auf. JS-Konsumenten können das zusätzliche Argument verwenden; die Typebene-Kompatibilität spiegelt weiterhin die 3-Argument-Legacy-Signatur wider.\n\nBenutzerdefinierte Tools verwenden typischerweise denselben UI-Einstiegspunkt über das factory-bezogene `pi.ui`-Objekt und geben dann den ausgewählten Wert im normalen Tool-Inhalt zurück:\n\n```ts\nasync execute(toolCallId, params, onUpdate, ctx, signal) {\n if (!pi.hasUI) {\n return { content: [{ type: \"text\", text: \"UI unavailable\" }] };\n }\n\n const picked = await pi.ui.custom<string | undefined>((tui, theme, done) => {\n const component = new MyPickerComponent(done, signal);\n return component;\n });\n\n return { content: [{ type: \"text\", text: picked ? `Picked: ${picked}` : \"Cancelled\" }] };\n}\n```\n\n## 3) Benutzerdefinierte Tool-Aufruf-/Ergebnis-Renderer\n\nBenutzerdefinierte Tools und Erweiterungstools können Komponenten zurückgeben von:\n\n- `renderCall(args, theme)`\n- `renderResult(result, options, theme, args?)`\n\n`options` enthält derzeit:\n\n- `expanded: boolean`\n- `isPartial: boolean`\n- `spinnerFrame?: number`\n\nDiese Renderer werden von `ToolExecutionComponent` eingebunden.\n\n## Lebenszyklus und Abbruch\n\n- `dispose()` ist auf Typebene optional, sollte aber implementiert werden, wenn Sie Timer, Unterprozesse, Watcher, Sockets oder Overlays besitzen.\n- `done(...)` sollte genau einmal aus Ihrem Komponentenfluss aufgerufen werden.\n- Für abbrechbare, langlebige UI kombinieren Sie `CancellableLoader` mit `AbortSignal` und rufen `done(...)` von `onAbort` auf.\n\nBeispiel für ein Abbruchmuster:\n\n```ts\nconst loader = new CancellableLoader(tui, theme.fg(\"accent\"), theme.fg(\"muted\"), \"Working...\");\nloader.onAbort = () => done(undefined);\nvoid doWork(loader.signal).then(result => done(result));\nreturn loader;\n```\n\n## Realistisches Beispiel einer benutzerdefinierten Komponente (Erweiterungsbefehl)\n\n```ts\nimport type { Component } from \"@f5-sales-demo/pi-tui\";\nimport { SelectList, matchesKey, replaceTabs, truncateToWidth } from \"@f5-sales-demo/pi-tui\";\nimport { getSelectListTheme, type ExtensionAPI } from \"@f5-sales-demo/xcsh\";\n\nclass Picker implements Component {\n list: SelectList;\n keybindings: any;\n done: (value: string | undefined) => void;\n\n constructor(\n items: Array<{ value: string; label: string }>,\n keybindings: any,\n done: (value: string | undefined) => void,\n ) {\n this.list = new SelectList(items, 8, getSelectListTheme());\n this.keybindings = keybindings;\n this.done = done;\n this.list.onSelect = item => this.done(item.value);\n this.list.onCancel = () => this.done(undefined);\n }\n\n handleInput(data: string): void {\n if (this.keybindings.matches(data, \"interrupt\")) {\n this.done(undefined);\n return;\n }\n this.list.handleInput(data);\n }\n\n render(width: number): string[] {\n return this.list.render(width).map(line => truncateToWidth(replaceTabs(line), width));\n }\n\n invalidate(): void {\n this.list.invalidate();\n }\n}\n\nexport default function extension(pi: ExtensionAPI): void {\n pi.registerCommand(\"pick-model\", {\n description: \"Pick a model profile\",\n handler: async (_args, ctx) => {\n if (!ctx.hasUI) return;\n\n const selected = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {\n const items = [\n { value: \"fast\", label: theme.fg(\"accent\", \"Fast\") },\n { value: \"balanced\", label: \"Balanced\" },\n { value: \"quality\", label: \"Quality\" },\n ];\n return new Picker(items, keybindings, done);\n });\n\n if (selected) ctx.ui.notify(`Selected profile: ${selected}`, \"info\");\n },\n });\n}\n```\n\n## Wichtige Implementierungsdateien\n\n- `packages/tui/src/tui.ts` — `Component`, `Focusable`, Cursor-Marker, Fokus, Overlay, Eingabeverteilung.\n- `packages/tui/src/utils.ts` — Breiten-/Kürzungs-/Bereinigungsprimitive.\n- `packages/tui/src/keys.ts` / `keybindings.ts` — Tastenanalyse und konfigurierbare Aktionszuordnung.\n- `packages/coding-agent/src/modes/controllers/extension-ui-controller.ts` — interaktive Ein-/Ausbindung für Erweiterungs-/Hook-/Custom-Tool-UI.\n- `packages/coding-agent/src/extensibility/extensions/types.ts` — Erweiterungs-UI- und Renderer-Verträge.\n- `packages/coding-agent/src/extensibility/hooks/types.ts` — Hook-UI-Vertrag (Legacy-Custom-Signatur).\n- `packages/coding-agent/src/extensibility/custom-tools/types.ts` — Verträge für Ausführung/Rendering benutzerdefinierter Tools.\n- `packages/coding-agent/src/modes/components/tool-execution.ts` — Einbindung von `renderCall`/`renderResult`-Komponenten und Partial-State-Optionen.\n- `packages/coding-agent/src/tools/context.ts` — Tool-UI-Kontextpropagierung (`hasUI`, `ui`).\n",
113
113
  "en/configuration/blob-artifact-architecture.md": "---\ntitle: Blob and Artifact Storage Architecture\ndescription: Content-addressable blob store and artifact registry for session media, screenshots, and tool outputs.\nsidebar:\n order: 7\n label: Blob & artifact storage\n---\n\nThis document describes how the coding agent stores large and binary payloads outside session JSONL files, how truncated tool output persists, and how internal URLs (`artifact://`, `agent://`) resolve back to stored data.\n\n## Why two storage systems exist\n\nThe runtime uses two distinct persistence mechanisms for different data structures:\n\n- **Content-addressed blobs** (`blob:sha256:<hash>`): global, binary-oriented storage used to externalize large image base64 payloads from persisted session entries.\n- **Session-scoped artifacts** (files under `<sessionFile-without-.jsonl>/`): per-session text files used for full tool outputs and subagent outputs.\n\nThe two mechanisms serve distinct optimization goals:\n\n- Blob storage optimizes deduplication and stable references by content hash.\n- Artifact storage optimizes append-only session tooling and fast retrieval by local identifiers.\n\n## Storage boundaries and on-disk layout\n\n### Blob store boundary (global)\n\n`SessionManager` constructs `BlobStore(getBlobsDir())`, which places blob files in a shared global blob directory rather than in an individual session directory.\n\nBlob file naming:\n\n- File path: `<blobsDir>/<sha256-hex>`\n- Extension: None\n- Reference string stored in entries: `blob:sha256:<sha256-hex>`\n\nKey characteristics:\n\n- Identical binary content across sessions resolves to the same hash and path.\n- Writes are idempotent at the content level.\n- Blobs outlive any individual session file.\n\n### Artifact boundary (session-local)\n\n`ArtifactManager` derives the artifact directory directly from the session file path:\n\n- Session file: `.../<timestamp>_<sessionId>.jsonl`\n- Artifacts directory: `.../<timestamp>_<sessionId>/` (with the `.jsonl` extension removed)\n\nArtifact types stored in this directory include:\n\n- Truncated tool output files: `<numericId>.<toolType>.log` (resolved by `artifact://`)\n- Subagent output files: `<outputId>.md` (resolved by `agent://`)\n\n## Identifier and name allocation schemes\n\n### Blob identifiers: content hash\n\n`BlobStore.put()` computes a SHA-256 digest over the raw binary bytes and returns:\n\n- `hash`: Hex digest\n- `path`: `<blobsDir>/<hash>`\n- `ref`: `blob:sha256:<hash>`\n\nThe blob store does not use session-local counters.\n\n### Artifact identifiers: session-local monotonic integer\n\n`ArtifactManager` scans existing `*.log` artifact files on first use to determine the maximum existing numeric identifier, then sets `nextId = max + 1`.\n\nAllocation behavior:\n\n- File format: `{id}.{toolType}.log`\n- Identifiers are sequential strings (`\"0\"`, `\"1\"`, and so on)\n- Resuming a session does not overwrite existing artifacts because directory scanning completes before allocation.\n\nIf the artifact directory is missing, scanning yields an empty list and allocation begins at `0`.\n\n### Agent output identifiers (`agent://`)\n\n`AgentOutputManager` allocates identifiers for subagent outputs in the format `<index>-<requestedId>` (or nested under a parent prefix, such as `0-Parent.1-Child`). It scans existing `.md` files during initialization to continue from the next index upon resume.\n\n## Persistence dataflow\n\n### 1. Session entry persistence rewrite path\n\nBefore session entries are written (`#rewriteFile` or incremental persistence), `SessionManager` calls `prepareEntryForPersistence()` through `truncateForPersistence`.\n\nKey behaviors:\n\n1. **Large string truncation**: Oversized strings are trimmed and appended with `\"[Session persistence truncated large content]\"`.\n2. **Transient field stripping**: `partialJson` and `jsonlEvents` are removed from persisted entries.\n3. **Image externalization to blobs**:\n - Applies only to image blocks in `content` arrays.\n - Executes only when `data` is not already a blob reference.\n - Triggers only when the base64 string length reaches or exceeds the threshold (`BLOB_EXTERNALIZE_THRESHOLD = 1024`).\n - Replaces inline base64 content with `blob:sha256:<hash>`.\n\nThis process maintains compact session JSONL files while preserving full data recoverability.\n\n### 2. Session load rehydration path\n\nWhen opening a session (`setSessionFile`), `SessionManager` runs `resolveBlobRefsInEntries()` after migrations complete.\n\nFor each message or custom-message image block containing `blob:sha256:<hash>`:\n\n- Reads blob bytes from the blob store.\n- Converts the bytes back to base64.\n- Mutates the in-memory entry to inline base64 for runtime consumers.\n\nIf a blob is missing:\n\n- `resolveImageData()` logs a warning.\n- The reference string remains unchanged.\n- Session loading continues without throwing a fatal exception.\n\n### 3. Tool output spill and truncation path\n\n`OutputSink` manages streaming output across bash, python, SSH, and related executors.\n\nExecution sequence:\n\n1. The sink sanitizes each chunk and appends it to an in-memory tail buffer.\n2. When in-memory bytes exceed the spill threshold (`DEFAULT_MAX_BYTES`, 50 KB), the sink marks output as truncated.\n3. If an artifact path is available, the sink opens a file writer and writes:\n - The existing buffered content (once).\n - All subsequent stream chunks.\n4. The in-memory buffer is trimmed to the tail display window.\n5. `dump()` returns a summary containing `artifactId` only when the file sink was created successfully.\n\nResulting behavior:\n\n- The UI and tool return values display the truncated tail.\n- Full output is preserved in the artifact file and accessible via `artifact://<id>`.\n\nIf file sink creation fails (due to an I/O error or missing path), the sink falls back to in-memory truncation, and the full output is not persisted.\n\n## URL access model\n\n### `blob:` references\n\nThe `blob:sha256:<hash>` format is an internal persistence reference within session payloads rather than a routing protocol scheme. `SessionManager` resolves these references during session load.\n\n### `artifact://<id>`\n\n`ArtifactProtocolHandler` processes `artifact://<id>` URLs:\n\n- Requires an active session artifact directory.\n- Requires a numeric identifier.\n- Resolves by matching the filename prefix `<id>.`.\n- Returns raw text (`text/plain`) from the matched `.log` file.\n- If not found, returns an error listing the available artifact identifiers.\n\nIf the artifacts directory does not exist, the handler throws `No artifacts directory found`.\n\n### `agent://<id>`\n\n`AgentProtocolHandler` resolves paths in `<artifactsDir>/<id>.md`:\n\n- Standard form returns raw Markdown text.\n- Subpath (`/path`) or query (`?q=`) forms perform JSON extraction.\n- Path and query extraction modes cannot be combined.\n- When extraction is requested, the file content must be valid JSON.\n\nIf the artifacts directory does not exist, the handler throws `No artifacts directory found`. If the output identifier is missing, it throws `Not found: <id>` along with a list of available identifiers.\n\nRead tool integration:\n\n- `read` supports offset and limit pagination for standard internal URL reads.\n- `read` rejects `offset` and `limit` arguments when using `agent://` JSON extraction.\n\n## Resume, fork, and move semantics\n\n### Resume\n\n- `ArtifactManager` scans existing `{id}.*.log` files on first allocation and continues numeric sequence.\n- `AgentOutputManager` scans existing `.md` output files and continues index numbering.\n- `SessionManager` rehydrates blob references back to base64 upon session load.\n\n### Fork\n\n`SessionManager.fork()` creates a new session file with a unique session ID and a `parentSession` link, returning the old and new file paths. `AgentSession.fork()` coordinates artifact directory copying:\n\n- Performs a recursive copy of the previous artifact directory to the new directory.\n- Tolerates a missing source directory without failing.\n- Logs non-ENOENT copy errors as warnings while allowing the fork operation to complete.\n\nIdentifier allocation after fork:\n\n- If directory copy succeeds, new artifact counters continue from the maximum copied identifier.\n- If copy fails or is skipped, artifact identifiers restart from `0`.\n\nBlob handling after fork:\n\n- Blobs remain globally content-addressed, requiring no copying between session directories.\n\n### Move to a new working directory\n\n`SessionManager.moveTo()` moves both the session file and the artifact directory to the new target location, with automated rollback if a step fails. This preserves artifact identities while updating the session working directory.\n\n## Failure handling and fallback paths\n\n| Condition | Behavior |\n| --- | --- |\n| Blob file missing during rehydration | Logs warning and retains the in-memory `blob:sha256:` reference string |\n| Blob read ENOENT via `BlobStore.get` | Returns `null` |\n| Artifact directory missing (`ArtifactManager.listFiles`) | Returns empty list; allocation starts from `0` |\n| Artifact directory missing (`artifact://` or `agent://`) | Throws explicit `No artifacts directory found` error |\n| Artifact ID not found | Throws error listing available artifact IDs |\n| OutputSink artifact writer initialization fails | Continues with tail-only truncation without full-output persistence |\n| No session file (subagent execution paths) | Task tool falls back to a temporary artifact directory for subagent output |\n\n## Binary blob externalization compared to text artifacts\n\n- **Blob externalization** handles binary image payloads within persisted session entries, replacing inline base64 data in JSONL files with stable content references.\n- **Artifacts** are plain text files that capture tool and subagent output, addressable by session-local identifiers through internal URLs.\n\nBoth mechanisms reduce session JSONL file size while maintaining distinct storage lifecycles and retrieval paths.\n\n## Implementation files\n\n- [`src/session/blob-store.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/session/blob-store.ts) — Blob reference format, hashing, put and get operations, externalize and resolve helpers.\n- [`src/session/artifacts.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/session/artifacts.ts) — Session artifact directory management and numeric identifier allocation.\n- [`src/session/streaming-output.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/session/streaming-output.ts) — `OutputSink` stream truncation, file spillover, and summary metadata.\n- [`src/session/session-manager.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/session/session-manager.ts) — Persistence transformations, blob rehydration, session fork, and session relocation.\n- [`src/session/agent-session.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/session/agent-session.ts) — Artifact directory duplication during interactive session fork.\n- [`src/tools/output-utils.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/tools/output-utils.ts) — Tool artifact manager initialization and artifact path allocation.\n- [`src/internal-urls/artifact-protocol.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://` URL protocol handler.\n- [`src/internal-urls/agent-protocol.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/internal-urls/agent-protocol.ts) — `agent://` URL protocol handler and JSON extractor.\n- [`src/sdk.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/sdk.ts) — Internal URL router configuration and artifact directory resolution.\n- [`src/task/output-manager.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/task/output-manager.ts) — Agent output identifier allocation for `agent://`.\n- [`src/task/executor.ts`](https://github.com/f5-sales-demo/xcsh/blob/main/packages/coding-agent/src/task/executor.ts) — Subagent output persistence (`<id>.md`) and temporary directory fallback.\n",
114
114
  "en/configuration/config-usage.md": "---\ntitle: Configuration Discovery and Resolution\ndescription: How xcsh discovers, resolves, and layers configuration from project, user, and enterprise roots.\nsidebar:\n order: 1\n label: Configuration\n---\n\nThis document describes how the coding agent resolves configuration: which roots are scanned, how precedence works, and how resolved settings are consumed by capabilities, skills, hooks, tools, and extensions.\n\n## Scope\n\nPrimary implementation:\n\n- `src/config.ts`\n- `src/config/settings.ts`\n- `src/config/settings-schema.ts`\n- `src/discovery/builtin.ts`\n- `src/discovery/helpers.ts`\n\nKey integration points:\n\n- `src/capability/index.ts`\n- `src/discovery/index.ts`\n- `src/extensibility/skills.ts`\n- `src/extensibility/hooks/loader.ts`\n- `src/extensibility/custom-tools/loader.ts`\n- `src/extensibility/extensions/loader.ts`\n\n---\n\n## Resolution flow\n\n```text\n Config roots (ordered)\n┌───────────────────────────────────────┐\n│ 1) ~/.xcsh/agent + <cwd>/.xcsh │\n│ 2) ~/.claude + <cwd>/.claude │\n│ 3) ~/.codex + <cwd>/.codex │\n│ 4) ~/.gemini + <cwd>/.gemini │\n└───────────────────────────────────────┘\n │\n ▼\n config.ts helper resolution\n (getConfigDirs/findConfigFile/findNearest...)\n │\n ▼\n capability providers enumerate items\n (native, claude, codex, gemini, agents, etc.)\n │\n ▼\n priority sort + per-capability deduplication\n │\n ▼\n subsystem-specific consumption\n (settings, skills, hooks, tools, extensions)\n```\n\n## Config roots and source order\n\n### Canonical roots\n\n`src/config.ts` defines a fixed source priority order:\n\n1. `.xcsh` (native)\n2. `.claude`\n3. `.codex`\n4. `.gemini`\n\nUser-level configuration bases:\n\n- `~/.xcsh/agent`\n- `~/.claude`\n- `~/.codex`\n- `~/.gemini`\n\nProject-level configuration bases:\n\n- `<cwd>/.xcsh`\n- `<cwd>/.claude`\n- `<cwd>/.codex`\n- `<cwd>/.gemini`\n\nThe default `CONFIG_DIR_NAME` constant is `.xcsh` (`packages/utils/src/dirs.ts`).\n\n### Source discovery constraints\n\nThe generic helpers in `src/config.ts` do not include `.pi` in automatic source discovery order.\n\n---\n\n## Core discovery helpers\n\n### `getConfigDirs(subpath, options)`\n\nReturns ordered directory entries:\n\n- User-level entries first (sorted by source priority).\n- Project-level entries second (sorted by source priority).\n\nSupported options:\n\n- `user` (default: `true`)\n- `project` (default: `true`)\n- `cwd` (default: `getProjectDir()`)\n- `existingOnly` (default: `false`)\n\nThis API handles directory-based configuration lookups (commands, hooks, tools, agents, and related assets).\n\n### `findConfigFile(subpath, options)` and `findConfigFileWithMeta(...)`\n\nSearches for the first existing configuration file across ordered base paths, returning the first match (as path-only or path with metadata).\n\n### `findAllNearestProjectConfigDirs(subpath, cwd)`\n\nTraverses parent directories upward and returns the nearest existing directory per source base (`.xcsh`, `.claude`, `.codex`, `.gemini`), then sorts the results by source priority.\n\nUse this helper when project configuration should be inherited from ancestor directories in monorepos or nested workspaces.\n\n---\n\n## File configuration wrapper\n\n`ConfigFile<T>` (`src/config.ts`) provides schema-validated loading for individual configuration files.\n\nSupported file formats:\n\n- `.yml` and `.yaml`\n- `.json` and `.jsonc`\n\nRuntime behavior:\n\n- Validates parsed data with AJV against a provided TypeBox schema.\n- Caches the load result in memory until `invalidate()` is called.\n- Returns a tri-state result from `tryLoad()`:\n - `ok`: Valid parsed configuration data.\n - `not-found`: Configuration file does not exist.\n - `error`: Returns `ConfigError` containing schema validation or parse error details.\n\nAutomatic migration support:\n\n- If the target path is `.yml` or `.yaml`, a sibling `.json` file is automatically migrated once (`migrateJsonToYml`).\n\n---\n\n## Settings resolution model\n\nThe runtime settings model applies layered resolution:\n\n1. Global settings: `~/.xcsh/agent/config.yml`\n2. Project settings: Discovered via settings capability (`settings.json` from capability providers)\n3. Runtime overrides: In-memory, non-persistent overrides\n4. Schema defaults: Defined in `SETTINGS_SCHEMA`\n\nEffective resolution order:\n\n`defaults <— global <— project <— overrides`\n\nPersistence behavior:\n\n- `settings.set(...)` writes to the global layer (`config.yml`) and queues an asynchronous background save.\n- Project settings discovered from capabilities are read-only at runtime.\n\n### Active migration behaviors\n\nOn startup, if `config.yml` is missing:\n\n1. Migrates settings from `~/.xcsh/agent/settings.json` (renaming the original to `.bak` upon success).\n2. Merges settings with legacy database values from `agent.db`.\n3. Writes the combined result to `config.yml`.\n\nField-level migrations executed in `#migrateRawSettings`:\n\n- `queueMode` to `steeringMode`\n- `ask.timeout` milliseconds converted to seconds when the existing value exceeds `1000`\n- Legacy flat `theme: \"...\"` converted to `{ theme: { dark, light } }` structure\n\n---\n\n## Capability and discovery integration\n\nNon-core configuration loading flows through the capability registry (`src/capability/index.ts` and `src/discovery/index.ts`).\n\n### Provider ordering\n\nProviders are sorted by numeric priority in descending order (higher numeric values take precedence). Standard priority values include:\n\n- Native provider (`builtin.ts`): `100`\n- Claude: `80`\n- Codex, agents, and Claude marketplace: `70`\n- Gemini: `60`\n\n```text\nProvider precedence (higher priority wins)\n\nnative (.xcsh) priority 100\nclaude priority 80\ncodex / agents / ... priority 70\ngemini priority 60\n```\n\n### Deduplication semantics\n\nCapabilities define a `key(item)` function:\n\n- Matching key: The first item encountered wins (retaining the higher-priority or earlier-loaded item).\n- Undefined key: No deduplication occurs; all items are preserved.\n\nDeduplication keys by capability:\n\n- Skills: `name`\n- Tools: `name`\n- Hooks: `${type}:${tool}:${name}`\n- Extension modules: `name`\n- Extensions: `name`\n- Settings: No deduplication (all items are preserved)\n\n---\n\n## Native `.xcsh` provider behavior\n\nThe native provider (`id: native`, defined in `src/discovery/builtin.ts`) reads from:\n\n- Project roots: `<cwd>/.xcsh/...`\n- User roots: `~/.xcsh/agent/...`\n\n### Directory admission rule\n\n`builtin.ts` includes a configuration root only if the directory exists and is non-empty (`ifNonEmptyDir`).\n\n### Scope-specific loading paths\n\n- Skills: `skills/*/SKILL.md`\n- Slash commands: `commands/*.md`\n- Rules: `rules/*.{md,mdc}`\n- Prompts: `prompts/*.md`\n- Instructions: `instructions/*.md`\n- Hooks: `hooks/pre/*` and `hooks/post/*`\n- Tools: `tools/*.json`, `tools/*.md`, and `tools/<name>/index.ts`\n- Extension modules: Discovered under `extensions/` (plus legacy `settings.json.extensions` string array)\n- Extensions: `extensions/<name>/gemini-extension.json`\n- Settings capability: `settings.json`\n\n### Nearest-project lookup behavior\n\nFor `SYSTEM.md` and `XCSH.md`, the native provider searches parent project `.xcsh` directories upward, requiring the matched `.xcsh` directory to be non-empty.\n\n---\n\n## How major subsystems consume configuration\n\n### Settings subsystem\n\n- `Settings.init()` loads global `config.yml` alongside discovered project `settings.json` capability items.\n- Only capability items with `level === \"project\"` merge into the project layer.\n\n### Skills subsystem\n\n- `extensibility/skills.ts` loads skills using `loadCapability(skillCapability.id, { cwd })`.\n- Applies source toggles and filters (`ignoredSkills`, `includeSkills`, custom directories).\n- Legacy toggle names (`skills.enablePiUser`, `skills.enablePiProject`) gate the native provider (`provider === \"native\"`).\n\n### Hooks subsystem\n\n- `discoverAndLoadHooks()` resolves hook paths from the hook capability and explicitly configured paths.\n- Loads modules using dynamic Bun imports.\n\n### Tools subsystem\n\n- `discoverAndLoadCustomTools()` resolves tool paths from tool capabilities, plugin tool paths, and explicit paths.\n- Declarative `.md` and `.json` tool files provide metadata only; executable loading requires TypeScript/JavaScript modules.\n\n### Extensions subsystem\n\n- `discoverAndLoadExtensions()` resolves extension modules from extension-module capabilities and explicit paths.\n- The loader filters capability items to those with `_source.provider === \"native\"` before loading.\n\n---\n\n## Precedence rules and priority resolution\n\nWhen reasoning about configuration precedence, apply this sequence:\n\n1. Source directory ordering from `config.ts` determines candidate path order.\n2. Capability provider priority determines cross-provider precedence.\n3. Capability key deduplication determines collision behavior (the first item encountered wins for keyed capabilities).\n4. Subsystem-specific merge logic determines the effective settings layer.\n\n### Settings merge caveat\n\nSettings capability items are not deduplicated; `Settings.#loadProjectSettings()` deep-merges project items in their returned order. Because deep merge overwrites earlier values with later values, effective overrides depend on provider emission order rather than capability key semantics alone.\n\n---\n\n## Legacy and compatibility behaviors\n\nThe runtime preserves the following backward-compatibility behaviors:\n\n- `ConfigFile` JSON-to-YAML migration for YAML configuration files.\n- Settings migration from `settings.json` and `agent.db` to `config.yml`.\n- Settings key migrations (`queueMode`, `ask.timeout`, and flat `theme`).\n- Extension manifest compatibility: The loader accepts both `package.json.xcsh` and `package.json.pi` manifest sections.\n- Legacy setting names `skills.enablePiUser` and `skills.enablePiProject` remain active gates for the native skill provider.\n\nWhen deprecating any compatibility path in code, update this document to maintain alignment with active runtime behavior.\n",
115
- "en/configuration/environment-variables.md": "---\ntitle: Environment Variables\ndescription: Runtime environment variable reference for xcsh configuration and behavior control.\nsidebar:\n order: 2\n label: Environment variables\n---\n\nThis reference documents the runtime environment variables used by the coding agent runtime, derived from code paths across:\n\n- `packages/coding-agent/src/**`\n- `packages/ai/src/**` (provider and authentication resolution)\n- `packages/utils/src/**` and `packages/tui/src/**` (runtime and user-interface controls)\n\n## Resolution model and precedence\n\nRuntime lookups use the `$env` helper from `@f5-sales-demo/pi-utils` (`packages/utils/src/env.ts`).\n\n`$env` resolves variables in the following order of precedence:\n\n1. Existing process environment (`Bun.env`)\n2. Project-level `.env` file (`$PWD/.env`) for unset keys\n3. User home `.env` file (`~/.env`) for unset keys\n\nDuring `.env` parsing, keys with the `XCSH_*` prefix automatically mirror to corresponding `PI_*` keys.\n\n---\n\n## Model and provider authentication\n\nThe runtime consumes these variables via `getEnvApiKey()` (`packages/ai/src/stream.ts`) unless specified otherwise.\n\n### Core provider credentials\n\n| Variable | Used for | Required when | Notes and precedence |\n| --- | --- | --- | --- |\n| `ANTHROPIC_OAUTH_TOKEN` | Anthropic API authentication | Using Anthropic with OAuth tokens | Takes precedence over `ANTHROPIC_API_KEY` for provider authentication resolution |\n| `ANTHROPIC_API_KEY` | Anthropic API authentication | Using Anthropic without an OAuth token | Fallback credential when `ANTHROPIC_OAUTH_TOKEN` is unset |\n| `ANTHROPIC_FOUNDRY_API_KEY` | Anthropic via Azure Foundry or enterprise gateway | `CLAUDE_CODE_USE_FOUNDRY` is enabled | Takes precedence over `ANTHROPIC_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` when Foundry mode is active |\n| `OPENAI_API_KEY` | OpenAI authentication | Using OpenAI-family providers without explicit `apiKey` parameters | Used by OpenAI Completions and Responses providers |\n| `GEMINI_API_KEY` | Google Gemini authentication | Using `google` provider models | Primary key for Gemini provider mapping |\n| `GOOGLE_API_KEY` | Gemini image tool authentication fallback | Using the `gemini_image` tool without `GEMINI_API_KEY` | Fallback key for image generation tools |\n| `GROQ_API_KEY` | Groq authentication | Using Groq models | |\n| `CEREBRAS_API_KEY` | Cerebras authentication | Using Cerebras models | |\n| `TOGETHER_API_KEY` | Together authentication | Using the `together` provider | |\n| `HUGGINGFACE_HUB_TOKEN` | Hugging Face authentication | Using the `huggingface` provider | Primary Hugging Face token variable |\n| `HF_TOKEN` | Hugging Face authentication | Using the `huggingface` provider | Fallback token when `HUGGINGFACE_HUB_TOKEN` is unset |\n| `SYNTHETIC_API_KEY` | Synthetic authentication | Using Synthetic models | |\n| `NVIDIA_API_KEY` | NVIDIA authentication | Using the `nvidia` provider | |\n| `NANO_GPT_API_KEY` | NanoGPT authentication | Using the `nanogpt` provider | |\n| `VENICE_API_KEY` | Venice authentication | Using the `venice` provider | |\n| `LITELLM_API_KEY` | LiteLLM authentication | Using the `litellm` provider | OpenAI-compatible LiteLLM proxy key; when set alongside `LITELLM_BASE_URL`, enables automatic configuration of `models.yml` |\n| `LM_STUDIO_API_KEY` | LM Studio authentication (optional) | Using the `lm-studio` provider with authenticated hosts | Local LM Studio instances typically run without authentication; any non-empty string works when a key is required |\n| `OLLAMA_API_KEY` | Ollama authentication (optional) | Using the `ollama` provider with authenticated hosts | Local Ollama instances typically run without authentication; any non-empty string works when a key is required |\n| `LLAMA_CPP_API_KEY` | Llama.cpp authentication (optional) | Using `llama-server` configured with `--api-key` | Local llama.cpp instances typically run without authentication; any non-empty string works when a key is configured |\n| `XIAOMI_API_KEY` | Xiaomi MiMo authentication | Using the `xiaomi` provider | |\n| `MOONSHOT_API_KEY` | Moonshot authentication | Using the `moonshot` provider | |\n| `XAI_API_KEY` | xAI authentication | Using xAI models | |\n| `OPENROUTER_API_KEY` | OpenRouter authentication | Using OpenRouter models | Also used by the image tool when the preferred provider is OpenRouter |\n| `MISTRAL_API_KEY` | Mistral authentication | Using Mistral models | |\n| `ZAI_API_KEY` | z.ai authentication | Using z.ai models | Also used by the z.ai web search provider |\n| `MINIMAX_API_KEY` | MiniMax authentication | Using the `minimax` provider | |\n| `MINIMAX_CODE_API_KEY` | MiniMax Code authentication | Using the `minimax-code` provider | |\n| `MINIMAX_CODE_CN_API_KEY` | MiniMax Code CN authentication | Using the `minimax-code-cn` provider | |\n| `OPENCODE_API_KEY` | OpenCode authentication | Using OpenCode models | |\n| `QIANFAN_API_KEY` | Qianfan authentication | Using the `qianfan` provider | |\n| `QWEN_OAUTH_TOKEN` | Qwen Portal authentication | Using `qwen-portal` with OAuth token authentication | Takes precedence over `QWEN_PORTAL_API_KEY` |\n| `QWEN_PORTAL_API_KEY` | Qwen Portal authentication | Using `qwen-portal` with API key authentication | Fallback key when `QWEN_OAUTH_TOKEN` is unset |\n| `ZENMUX_API_KEY` | ZenMux authentication | Using the `zenmux` provider | Used for ZenMux OpenAI and Anthropic-compatible routing |\n| `VLLM_API_KEY` | vLLM authentication and discovery | Using the `vllm` provider (local OpenAI-compatible endpoints) | Any non-empty string satisfies local unauthenticated servers |\n| `CURSOR_ACCESS_TOKEN` | Cursor provider authentication | Using the Cursor provider | |\n| `AI_GATEWAY_API_KEY` | Vercel AI Gateway authentication | Using the `vercel-ai-gateway` provider | |\n| `CLOUDFLARE_AI_GATEWAY_API_KEY` | Cloudflare AI Gateway authentication | Using the `cloudflare-ai-gateway` provider | Requires base URL formatted as `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic` |\n\n### GitHub and Copilot token chains\n\n| Variable | Used for | Resolution chain |\n| --- | --- | --- |\n| `COPILOT_GITHUB_TOKEN` | GitHub Copilot provider authentication | `COPILOT_GITHUB_TOKEN` —> `GH_TOKEN` —> `GITHUB_TOKEN` |\n| `GH_TOKEN` | Copilot fallback; GitHub API authentication in web scraper | In web scraper: `GITHUB_TOKEN` —> `GH_TOKEN` |\n| `GITHUB_TOKEN` | Copilot fallback; GitHub API authentication in web scraper | In web scraper: Checked before `GH_TOKEN` |\n\n---\n\n## Provider-specific runtime configuration\n\n### Anthropic Foundry Gateway (Azure and enterprise proxy)\n\nWhen `CLAUDE_CODE_USE_FOUNDRY` is active, Anthropic requests route to Foundry mode:\n\n- Base URL resolves from `FOUNDRY_BASE_URL` (falls back to model default if unset).\n- API key resolution order for the `anthropic` provider becomes:\n `ANTHROPIC_FOUNDRY_API_KEY` —> `ANTHROPIC_OAUTH_TOKEN` —> `ANTHROPIC_API_KEY`.\n- `ANTHROPIC_CUSTOM_HEADERS` is parsed as comma-separated or newline-separated `key: value` pairs and merged into request headers.\n- TLS client and server credentials can be injected from environment variables:\n `NODE_EXTRA_CA_CERTS`, `CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`.\n Each variable accepts either a filesystem path to a PEM file or an inline PEM string (including escaped `\\n` sequences).\n\n| Variable | Value type | Behavior |\n| --- | --- | --- |\n| `CLAUDE_CODE_USE_FOUNDRY` | Boolean string (`1`, `true`, `yes`, `on`) | Enables Foundry mode for the Anthropic provider |\n| `FOUNDRY_BASE_URL` | URL string | Anthropic endpoint base URL in Foundry mode |\n| `ANTHROPIC_FOUNDRY_API_KEY` | Token string | Used for `Authorization: Bearer <token>` |\n| `ANTHROPIC_CUSTOM_HEADERS` | Header list string | Additional headers; formatted as `header-a: value, header-b: value` or newline-separated |\n| `NODE_EXTRA_CA_CERTS` | PEM path or inline PEM | Additional CA certificate bundle for server validation |\n| `CLAUDE_CODE_CLIENT_CERT` | PEM path or inline PEM | Mutual TLS (mTLS) client certificate |\n| `CLAUDE_CODE_CLIENT_KEY` | PEM path or inline PEM | Mutual TLS (mTLS) client private key (paired with client certificate) |\n\n### Amazon Bedrock\n\n| Variable | Default or behavior |\n| --- | --- |\n| `AWS_REGION` | Primary AWS region source |\n| `AWS_DEFAULT_REGION` | Fallback region when `AWS_REGION` is unset |\n| `AWS_PROFILE` | Named AWS profile authentication path |\n| `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` | IAM access key and secret key authentication path |\n| `AWS_BEARER_TOKEN_BEDROCK` | Bearer token authentication path |\n| `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` / `AWS_CONTAINER_CREDENTIALS_FULL_URI` | ECS task role credential path |\n| `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` | Web identity token authentication path |\n| `AWS_BEDROCK_SKIP_AUTH` | When set to `1`, injects dummy credentials for proxy or non-authenticated scenarios |\n| `AWS_BEDROCK_FORCE_HTTP1` | When set to `1`, forces the Node.js HTTP/1 request handler |\n\nRegion fallback sequence: `options.region` —> `AWS_REGION` —> `AWS_DEFAULT_REGION` —> `us-east-1`.\n\n### Azure OpenAI Responses\n\n| Variable | Default or behavior |\n| --- | --- |\n| `AZURE_OPENAI_API_KEY` | Required unless the API key is passed via options |\n| `AZURE_OPENAI_API_VERSION` | Defaults to `v1` |\n| `AZURE_OPENAI_BASE_URL` | Direct base URL override |\n| `AZURE_OPENAI_RESOURCE_NAME` | Constructs base URL: `https://<resource>.openai.azure.com/openai/v1` |\n| `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` | Deployment mapping string: `modelId=deploymentName,model2=deployment2` |\n\nBase URL resolution sequence: `options.azureBaseUrl` —> `AZURE_OPENAI_BASE_URL` —> resource name derivation —> `model.baseUrl`.\n\n### Google Vertex AI\n\n| Variable | Required | Notes |\n| --- | --- | --- |\n| `GOOGLE_CLOUD_PROJECT` | Yes (unless passed via options) | Fallback: `GCLOUD_PROJECT` |\n| `GCLOUD_PROJECT` | Optional fallback | Alternative project ID source |\n| `GOOGLE_CLOUD_LOCATION` | Yes (unless passed via options) | No implicit default in provider |\n| `GOOGLE_APPLICATION_CREDENTIALS` | Conditional | File path must exist if set; otherwise application default credentials (ADC) path is checked (`~/.config/gcloud/application_default_credentials.json`) |\n\n### Kimi\n\n| Variable | Default or behavior |\n| --- | --- |\n| `KIMI_CODE_OAUTH_HOST` | Primary OAuth host override |\n| `KIMI_OAUTH_HOST` | Fallback OAuth host override |\n| `KIMI_CODE_BASE_URL` | Overrides the Kimi usage endpoint base URL (`usage/kimi.ts`) |\n\nOAuth host resolution chain: `KIMI_CODE_OAUTH_HOST` —> `KIMI_OAUTH_HOST` —> `https://auth.kimi.com`.\n\n### Antigravity and Gemini image compatibility\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_AI_ANTIGRAVITY_VERSION` | Overrides the Antigravity user-agent version header in the Gemini CLI provider |\n\n### OpenAI Codex responses (feature and debug controls)\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_CODEX_DEBUG` | Set to `1` or `true` to enable Codex provider debug logging |\n| `PI_CODEX_WEBSOCKET` | Set to `1` or `true` to enable WebSocket transport preference |\n| `PI_CODEX_WEBSOCKET_V2` | Set to `1` or `true` to enable the WebSocket v2 path |\n| `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer overriding idle timeout in milliseconds (default: `300000`) |\n| `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer overriding retry attempts (default: `5`) |\n| `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer overriding base backoff delay in milliseconds (default: `500`) |\n\n### Cursor provider debug\n\n| Variable | Behavior |\n| --- | --- |\n| `DEBUG_CURSOR` | Enables provider debug logs; set to `2` or `verbose` for detailed payload snippets |\n| `DEBUG_CURSOR_LOG` | Optional file path for JSONL debug log output |\n\n### Prompt cache compatibility\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_CACHE_RETENTION` | When set to `long`, enables extended cache retention where supported (`anthropic`, `openai-responses`, and Bedrock) |\n\n---\n\n## Web search subsystem\n\n### Search provider credentials\n\n| Variable | Used by |\n| --- | --- |\n| `EXA_API_KEY` | Exa search provider and Exa MCP tools |\n| `BRAVE_API_KEY` | Brave search provider |\n| `PERPLEXITY_API_KEY` | Perplexity search provider in API-key mode |\n| `TAVILY_API_KEY` | Tavily search provider |\n| `ZAI_API_KEY` | z.ai search provider (also checks stored OAuth credentials in `agent.db`) |\n| `OPENAI_API_KEY` | Codex search provider availability and authentication (or OAuth credentials in `agent.db`) |\n\n### Anthropic web search authentication chain\n\n`packages/coding-agent/src/web/search/auth.ts` resolves Anthropic web search credentials in this order:\n\n1. `ANTHROPIC_SEARCH_API_KEY` (alongside optional `ANTHROPIC_SEARCH_BASE_URL`)\n2. `models.json` provider entry with `api: \"anthropic-messages\"`\n3. Anthropic OAuth credentials from `agent.db` (valid with at least a 5-minute buffer before expiry)\n4. Generic Anthropic environment variables: `ANTHROPIC_FOUNDRY_API_KEY` —> `ANTHROPIC_OAUTH_TOKEN` —> `ANTHROPIC_API_KEY`, with optional `ANTHROPIC_BASE_URL` (`FOUNDRY_BASE_URL` when Foundry mode is active)\n\nRelated search variables:\n\n| Variable | Default or behavior |\n| --- | --- |\n| `ANTHROPIC_SEARCH_API_KEY` | Highest-priority explicit search API key |\n| `ANTHROPIC_SEARCH_BASE_URL` | Defaults to `https://api.anthropic.com` when omitted |\n| `ANTHROPIC_SEARCH_MODEL` | Defaults to `claude-haiku-4-5` |\n| `ANTHROPIC_BASE_URL` | Generic fallback base URL for tier-4 authentication |\n\n### Perplexity OAuth flow behavior\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_AUTH_NO_BORROW` | When set, disables the macOS native application token borrowing path during Perplexity login |\n\n---\n\n## Python tooling and kernel runtime\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_PY` | Python tool mode override: `0` or `bash` (`bash-only`), `1` or `py` (`ipy-only`), `mix` or `both` (`both`); invalid values are ignored |\n| `PI_PYTHON_SKIP_CHECK` | When set to `1`, skips Python kernel availability and warm checks |\n| `PI_PYTHON_GATEWAY_URL` | When set, routes kernel execution to an external kernel gateway instead of the local shared gateway |\n| `PI_PYTHON_GATEWAY_TOKEN` | Optional authentication token for the external gateway (`Authorization: token <value>`) |\n| `PI_PYTHON_IPC_TRACE` | When set to `1`, enables low-level IPC trace logging in the kernel module |\n| `VIRTUAL_ENV` | Highest-priority virtual environment path for Python runtime resolution |\n\nAdditional runtime rules:\n\n- When `BUN_ENV=test` or `NODE_ENV=test`, Python availability checks pass automatically and kernel warming is skipped.\n- The Python execution environment filters out common API keys while preserving safe base variables and prefixes matching `LC_*`, `XDG_*`, and `PI_*`.\n\n---\n\n## Agent and runtime behavior toggles\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_SMOL_MODEL` | Ephemeral model role override for `smol` (CLI `--smol` takes precedence) |\n| `PI_SLOW_MODEL` | Ephemeral model role override for `slow` (CLI `--slow` takes precedence) |\n| `PI_PLAN_MODEL` | Ephemeral model role override for `plan` (CLI `--plan` takes precedence) |\n| `PI_NO_TITLE` | When set to any non-empty value, disables automatic session title generation on the first user message |\n| `NULL_PROMPT` | When set to `true`, the system prompt builder returns an empty string |\n| `PI_BLOCKED_AGENT` | Blocks a specific subagent type from being spawned by the task tool |\n| `PI_SUBPROCESS_CMD` | Overrides the subagent spawn command (bypassing `xcsh` or `xcsh.cmd` resolution) |\n| `PI_TASK_MAX_OUTPUT_BYTES` | Maximum captured output bytes per subagent (default: `500000`) |\n| `PI_TASK_MAX_OUTPUT_LINES` | Maximum captured output lines per subagent (default: `5000`) |\n| `PI_TIMING` | When set to `1`, enables startup and tool execution timing instrumentation logs |\n| `PI_DEBUG_STARTUP` | Enables startup stage debug logging to standard error |\n| `PI_PACKAGE_DIR` | Overrides package asset base directory resolution for documentation, examples, and changelogs |\n| `PI_DISABLE_LSPMUX` | When set to `1`, disables lspmux detection and forces direct LSP server spawning |\n| `LITELLM_BASE_URL` | LiteLLM proxy base URL; when set alongside `LITELLM_API_KEY`, generates `models.yml` on first run and validates configuration on startup |\n| `LM_STUDIO_BASE_URL` | Base URL override for implicit LM Studio discovery (defaults to `http://127.0.0.1:1234/v1`) |\n| `OLLAMA_BASE_URL` | Base URL override for implicit Ollama discovery (defaults to `http://127.0.0.1:11434`) |\n| `LLAMA_CPP_BASE_URL` | Base URL override for implicit Llama.cpp discovery (defaults to `http://127.0.0.1:8080`) |\n| `PI_EDIT_VARIANT` | When set to `hashline`, forces hashline read and grep display mode when the edit tool is active |\n| `PI_NO_PTY` | When set to `1`, disables the interactive PTY path for the bash tool (also set internally by `--no-pty`) |\n\n---\n\n## Storage and configuration root paths\n\nThese variables are consumed via `@f5-sales-demo/pi-utils/dirs` and govern data storage locations:\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_CONFIG_DIR` | Configuration root directory name under the user home directory (defaults to `.xcsh`) |\n| `PI_CODING_AGENT_DIR` | Full path override for the agent directory (defaults to `~/<PI_CONFIG_DIR or .xcsh>/agent`) |\n| `PWD` | Used when matching canonical current working directories in path resolution helpers |\n\n---\n\n## Shell and tool execution environment\n\nThese variables govern subprocess management in `packages/utils/src/procmgr.ts` and the bash tool integration:\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_BASH_NO_CI` | Suppresses automatic `CI=true` injection into spawned shell environments |\n| `CLAUDE_BASH_NO_CI` | Legacy fallback alias for `PI_BASH_NO_CI` |\n| `PI_BASH_NO_LOGIN` | Disables login shell mode when spawning bash processes |\n| `CLAUDE_BASH_NO_LOGIN` | Legacy fallback alias for `PI_BASH_NO_LOGIN` |\n| `PI_SHELL_PREFIX` | Optional command prefix wrapper for shell executions |\n| `CLAUDE_CODE_SHELL_PREFIX` | Legacy fallback alias for `PI_SHELL_PREFIX` |\n| `VISUAL` | Preferred external editor command |\n| `EDITOR` | Fallback external editor command |\n\n---\n\n## Terminal and session environment variables\n\nThe runtime detects the following environment variables automatically from the host environment:\n\n| Variable | Used for |\n| --- | --- |\n| `COLORTERM`, `TERM`, `WT_SESSION` | Terminal color capability detection and theme selection |\n| `COLORFGBG` | Terminal background light and dark mode auto-detection |\n| `TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `TERMINAL_EMULATOR` | Terminal identity detection for system prompt context |\n| `KDE_FULL_SESSION`, `XDG_CURRENT_DESKTOP`, `DESKTOP_SESSION`, `XDG_SESSION_DESKTOP`, `GDMSESSION`, `WINDOWMANAGER` | Desktop environment and window manager detection for system prompt context |\n| `KITTY_WINDOW_ID`, `TMUX_PANE`, `TERM_SESSION_ID`, `WT_SESSION` | Stable per-terminal session breadcrumb identifiers |\n| `SHELL`, `ComSpec`, `TERM_PROGRAM`, `TERM` | System diagnostics and shell detection |\n| `APPDATA`, `XDG_CONFIG_HOME` | lspmux configuration path resolution |\n| `HOME` | Path shortening in MCP command user interface |\n\n---\n\n## Native loader and debug flags\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_DEV` | Enables verbose native addon load diagnostics in `packages/natives` |\n\n---\n\n## Terminal user interface runtime flags\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_NOTIFICATIONS` | Set to `off`, `0`, or `false` to suppress desktop notifications |\n| `PI_TUI_WRITE_LOG` | When set, logs TUI write operations to a specified file |\n| `PI_HARDWARE_CURSOR` | When set to `1`, enables hardware cursor mode |\n| `PI_CLEAR_ON_SHRINK` | When set to `1`, clears empty rows when rendered content shrinks |\n| `PI_DEBUG_REDRAW` | When set to `1`, enables redraw debug logging |\n| `PI_TUI_DEBUG` | When set to `1`, enables deep TUI debug dump logging |\n\n---\n\n## Commit generation controls\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_COMMIT_TEST_FALLBACK` | When set to `true` (case-insensitive), forces the commit fallback generation path |\n| `PI_COMMIT_NO_FALLBACK` | When set to `true`, disables fallback when the commit agent returns no proposal |\n| `PI_COMMIT_MAP_REDUCE` | When set to `false`, disables the map-reduce commit analysis path |\n| `DEBUG` | When set, prints commit agent error stack traces to standard error |\n\n---\n\n## Security-sensitive variables\n\nProtect the following variables as sensitive secrets; do not log or commit them:\n\n- Provider API keys and OAuth tokens (`*_API_KEY`, `*_TOKEN`, OAuth access and refresh tokens).\n- Cloud credentials (`AWS_*`, `GOOGLE_APPLICATION_CREDENTIALS`).\n- Web search authentication keys (`EXA_API_KEY`, `BRAVE_API_KEY`, `PERPLEXITY_API_KEY`, Anthropic search keys).\n- Foundry mutual TLS credentials (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `NODE_EXTRA_CA_CERTS`).\n\nThe Python runtime automatically strips these sensitive keys before spawning kernel subprocesses (`packages/coding-agent/src/ipy/runtime.ts`).\n",
115
+ "en/configuration/environment-variables.md": "---\ntitle: Environment Variables\ndescription: Runtime environment variable reference for xcsh configuration and behavior control.\nsidebar:\n order: 2\n label: Environment variables\n---\n\nThis reference documents the runtime environment variables used by the coding agent runtime, derived from code paths across:\n\n- `packages/coding-agent/src/**`\n- `packages/ai/src/**` (provider and authentication resolution)\n- `packages/utils/src/**` and `packages/tui/src/**` (runtime and user-interface controls)\n\n## Resolution model and precedence\n\nRuntime lookups use the `$env` helper from `@f5-sales-demo/pi-utils` (`packages/utils/src/env.ts`).\n\n`$env` resolves variables in the following order of precedence:\n\n1. Existing process environment (`Bun.env`)\n2. Project-level `.env` file (`$PWD/.env`) for unset keys\n3. User home `.env` file (`~/.env`) for unset keys\n\nDuring `.env` parsing, keys with the `XCSH_*` prefix automatically mirror to corresponding `PI_*` keys.\n\n---\n\n## Model and provider authentication\n\nThe runtime consumes these variables via `getEnvApiKey()` (`packages/ai/src/stream.ts`) unless specified otherwise.\n\n### Core provider credentials\n\n| Variable | Used for | Required when | Notes and precedence |\n| --- | --- | --- | --- |\n| `ANTHROPIC_OAUTH_TOKEN` | Anthropic API authentication | Using Anthropic with OAuth tokens | Takes precedence over `ANTHROPIC_API_KEY` for provider authentication resolution |\n| `ANTHROPIC_API_KEY` | Anthropic API authentication | Using Anthropic without an OAuth token | Fallback credential when `ANTHROPIC_OAUTH_TOKEN` is unset |\n| `ANTHROPIC_FOUNDRY_API_KEY` | Anthropic via Azure Foundry or enterprise gateway | `CLAUDE_CODE_USE_FOUNDRY` is enabled | Takes precedence over `ANTHROPIC_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` when Foundry mode is active |\n| `OPENAI_API_KEY` | OpenAI authentication | Using OpenAI-family providers without explicit `apiKey` parameters | Used by OpenAI Completions and Responses providers |\n| `GEMINI_API_KEY` | Google Gemini authentication | Using `google` provider models | Primary key for Gemini provider mapping |\n| `GOOGLE_API_KEY` | Gemini image tool authentication fallback | Using the `gemini_image` tool without `GEMINI_API_KEY` | Fallback key for image generation tools |\n| `GROQ_API_KEY` | Groq authentication | Using Groq models | |\n| `CEREBRAS_API_KEY` | Cerebras authentication | Using Cerebras models | |\n| `TOGETHER_API_KEY` | Together authentication | Using the `together` provider | |\n| `HUGGINGFACE_HUB_TOKEN` | Hugging Face authentication | Using the `huggingface` provider | Primary Hugging Face token variable |\n| `HF_TOKEN` | Hugging Face authentication | Using the `huggingface` provider | Fallback token when `HUGGINGFACE_HUB_TOKEN` is unset |\n| `SYNTHETIC_API_KEY` | Synthetic authentication | Using Synthetic models | |\n| `NVIDIA_API_KEY` | NVIDIA authentication | Using the `nvidia` provider | |\n| `NANO_GPT_API_KEY` | NanoGPT authentication | Using the `nanogpt` provider | |\n| `VENICE_API_KEY` | Venice authentication | Using the `venice` provider | |\n| `LITELLM_API_KEY` | LiteLLM authentication | Using the `litellm` provider | OpenAI-compatible LiteLLM proxy key; when set alongside `LITELLM_BASE_URL`, enables automatic configuration of `models.yml` |\n| `LM_STUDIO_API_KEY` | LM Studio authentication (optional) | Using the `lm-studio` provider with authenticated hosts | Local LM Studio instances typically run without authentication; any non-empty string works when a key is required |\n| `OLLAMA_API_KEY` | Ollama authentication (optional) | Using the `ollama` provider with authenticated hosts | Local Ollama instances typically run without authentication; any non-empty string works when a key is required |\n| `LLAMA_CPP_API_KEY` | Llama.cpp authentication (optional) | Using `llama-server` configured with `--api-key` | Local llama.cpp instances typically run without authentication; any non-empty string works when a key is configured |\n| `XIAOMI_API_KEY` | Xiaomi MiMo authentication | Using the `xiaomi` provider | |\n| `MOONSHOT_API_KEY` | Moonshot authentication | Using the `moonshot` provider | |\n| `XAI_API_KEY` | xAI authentication | Using xAI models | |\n| `OPENROUTER_API_KEY` | OpenRouter authentication | Using OpenRouter models | Also used by the image tool when the preferred provider is OpenRouter |\n| `MISTRAL_API_KEY` | Mistral authentication | Using Mistral models | |\n| `ZAI_API_KEY` | z.ai authentication | Using z.ai models | Also used by the z.ai web search provider |\n| `MINIMAX_API_KEY` | MiniMax authentication | Using the `minimax` provider | |\n| `MINIMAX_CODE_API_KEY` | MiniMax Code authentication | Using the `minimax-code` provider | |\n| `MINIMAX_CODE_CN_API_KEY` | MiniMax Code CN authentication | Using the `minimax-code-cn` provider | |\n| `OPENCODE_API_KEY` | OpenCode authentication | Using OpenCode models | |\n| `QIANFAN_API_KEY` | Qianfan authentication | Using the `qianfan` provider | |\n| `QWEN_OAUTH_TOKEN` | Qwen Portal authentication | Using `qwen-portal` with OAuth token authentication | Takes precedence over `QWEN_PORTAL_API_KEY` |\n| `QWEN_PORTAL_API_KEY` | Qwen Portal authentication | Using `qwen-portal` with API key authentication | Fallback key when `QWEN_OAUTH_TOKEN` is unset |\n| `ZENMUX_API_KEY` | ZenMux authentication | Using the `zenmux` provider | Used for ZenMux OpenAI and Anthropic-compatible routing |\n| `VLLM_API_KEY` | vLLM authentication and discovery | Using the `vllm` provider (local OpenAI-compatible endpoints) | Any non-empty string satisfies local unauthenticated servers |\n| `CURSOR_ACCESS_TOKEN` | Cursor provider authentication | Using the Cursor provider | |\n| `AI_GATEWAY_API_KEY` | Vercel AI Gateway authentication | Using the `vercel-ai-gateway` provider | |\n| `CLOUDFLARE_AI_GATEWAY_API_KEY` | Cloudflare AI Gateway authentication | Using the `cloudflare-ai-gateway` provider | Requires base URL formatted as `https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic` |\n\n### GitHub and Copilot token chains\n\n| Variable | Used for | Resolution chain |\n| --- | --- | --- |\n| `COPILOT_GITHUB_TOKEN` | GitHub Copilot provider authentication | `COPILOT_GITHUB_TOKEN` —> `GH_TOKEN` —> `GITHUB_TOKEN` |\n| `GH_TOKEN` | Copilot fallback; GitHub API authentication in web scraper | In web scraper: `GITHUB_TOKEN` —> `GH_TOKEN` |\n| `GITHUB_TOKEN` | Copilot fallback; GitHub API authentication in web scraper | In web scraper: Checked before `GH_TOKEN` |\n\n---\n\n## Provider-specific runtime configuration\n\n### Anthropic Foundry Gateway (Azure and enterprise proxy)\n\nWhen `CLAUDE_CODE_USE_FOUNDRY` is active, Anthropic requests route to Foundry mode:\n\n- Base URL resolves from `FOUNDRY_BASE_URL` (falls back to model default if unset).\n- API key resolution order for the `anthropic` provider becomes:\n `ANTHROPIC_FOUNDRY_API_KEY` —> `ANTHROPIC_OAUTH_TOKEN` —> `ANTHROPIC_API_KEY`.\n- `ANTHROPIC_CUSTOM_HEADERS` is parsed as comma-separated or newline-separated `key: value` pairs and merged into request headers.\n- TLS client and server credentials can be injected from environment variables:\n `NODE_EXTRA_CA_CERTS`, `CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`.\n Each variable accepts either a filesystem path to a PEM file or an inline PEM string (including escaped `\\n` sequences).\n\n| Variable | Value type | Behavior |\n| --- | --- | --- |\n| `CLAUDE_CODE_USE_FOUNDRY` | Boolean string (`1`, `true`, `yes`, `on`) | Enables Foundry mode for the Anthropic provider |\n| `FOUNDRY_BASE_URL` | URL string | Anthropic endpoint base URL in Foundry mode |\n| `ANTHROPIC_FOUNDRY_API_KEY` | Token string | Used for `Authorization: Bearer <token>` |\n| `ANTHROPIC_CUSTOM_HEADERS` | Header list string | Additional headers; formatted as `header-a: value, header-b: value` or newline-separated |\n| `NODE_EXTRA_CA_CERTS` | PEM path or inline PEM | Additional CA certificate bundle for server validation |\n| `CLAUDE_CODE_CLIENT_CERT` | PEM path or inline PEM | Mutual TLS (mTLS) client certificate |\n| `CLAUDE_CODE_CLIENT_KEY` | PEM path or inline PEM | Mutual TLS (mTLS) client private key (paired with client certificate) |\n\n### Amazon Bedrock\n\n| Variable | Default or behavior |\n| --- | --- |\n| `AWS_REGION` | Primary AWS region source |\n| `AWS_DEFAULT_REGION` | Fallback region when `AWS_REGION` is unset |\n| `AWS_PROFILE` | Named AWS profile authentication path |\n| `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` | IAM access key and secret key authentication path |\n| `AWS_BEARER_TOKEN_BEDROCK` | Bearer token authentication path |\n| `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` / `AWS_CONTAINER_CREDENTIALS_FULL_URI` | ECS task role credential path |\n| `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` | Web identity token authentication path |\n| `AWS_BEDROCK_SKIP_AUTH` | When set to `1`, injects dummy credentials for proxy or non-authenticated scenarios |\n| `AWS_BEDROCK_FORCE_HTTP1` | When set to `1`, forces the Node.js HTTP/1 request handler |\n\nRegion fallback sequence: `options.region` —> `AWS_REGION` —> `AWS_DEFAULT_REGION` —> `us-east-1`.\n\n### Azure OpenAI Responses\n\n| Variable | Default or behavior |\n| --- | --- |\n| `AZURE_OPENAI_API_KEY` | Required unless the API key is passed via options |\n| `AZURE_OPENAI_API_VERSION` | Defaults to `v1` |\n| `AZURE_OPENAI_BASE_URL` | Direct base URL override |\n| `AZURE_OPENAI_RESOURCE_NAME` | Constructs base URL: `https://<resource>.openai.azure.com/openai/v1` |\n| `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` | Deployment mapping string: `modelId=deploymentName,model2=deployment2` |\n\nBase URL resolution sequence: `options.azureBaseUrl` —> `AZURE_OPENAI_BASE_URL` —> resource name derivation —> `model.baseUrl`.\n\n### Google Vertex AI\n\n| Variable | Required | Notes |\n| --- | --- | --- |\n| `GOOGLE_CLOUD_PROJECT` | Yes (unless passed via options) | Fallback: `GCLOUD_PROJECT` |\n| `GCLOUD_PROJECT` | Optional fallback | Alternative project ID source |\n| `GOOGLE_CLOUD_LOCATION` | Optional | Defaults to `global`; corporate `/login` persists `global` explicitly |\n| `GOOGLE_APPLICATION_CREDENTIALS` | Conditional | File path must exist if set; otherwise application default credentials (ADC) path is checked (`~/.config/gcloud/application_default_credentials.json`) |\n\n### Kimi\n\n| Variable | Default or behavior |\n| --- | --- |\n| `KIMI_CODE_OAUTH_HOST` | Primary OAuth host override |\n| `KIMI_OAUTH_HOST` | Fallback OAuth host override |\n| `KIMI_CODE_BASE_URL` | Overrides the Kimi usage endpoint base URL (`usage/kimi.ts`) |\n\nOAuth host resolution chain: `KIMI_CODE_OAUTH_HOST` —> `KIMI_OAUTH_HOST` —> `https://auth.kimi.com`.\n\n### Antigravity and Gemini image compatibility\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_AI_ANTIGRAVITY_VERSION` | Overrides the Antigravity user-agent version header in the Gemini CLI provider |\n\n### OpenAI Codex responses (feature and debug controls)\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_CODEX_DEBUG` | Set to `1` or `true` to enable Codex provider debug logging |\n| `PI_CODEX_WEBSOCKET` | Set to `1` or `true` to enable WebSocket transport preference |\n| `PI_CODEX_WEBSOCKET_V2` | Set to `1` or `true` to enable the WebSocket v2 path |\n| `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer overriding idle timeout in milliseconds (default: `300000`) |\n| `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer overriding retry attempts (default: `5`) |\n| `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer overriding base backoff delay in milliseconds (default: `500`) |\n\n### Cursor provider debug\n\n| Variable | Behavior |\n| --- | --- |\n| `DEBUG_CURSOR` | Enables provider debug logs; set to `2` or `verbose` for detailed payload snippets |\n| `DEBUG_CURSOR_LOG` | Optional file path for JSONL debug log output |\n\n### Prompt cache compatibility\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_CACHE_RETENTION` | When set to `long`, enables extended cache retention where supported (`anthropic`, `openai-responses`, and Bedrock) |\n\n---\n\n## Web search subsystem\n\n### Search provider credentials\n\n| Variable | Used by |\n| --- | --- |\n| `EXA_API_KEY` | Exa search provider and Exa MCP tools |\n| `BRAVE_API_KEY` | Brave search provider |\n| `PERPLEXITY_API_KEY` | Perplexity search provider in API-key mode |\n| `TAVILY_API_KEY` | Tavily search provider |\n| `ZAI_API_KEY` | z.ai search provider (also checks stored OAuth credentials in `agent.db`) |\n| `OPENAI_API_KEY` | Codex search provider availability and authentication (or OAuth credentials in `agent.db`) |\n\n### Anthropic web search authentication chain\n\n`packages/coding-agent/src/web/search/auth.ts` resolves Anthropic web search credentials in this order:\n\n1. `ANTHROPIC_SEARCH_API_KEY` (alongside optional `ANTHROPIC_SEARCH_BASE_URL`)\n2. `models.json` provider entry with `api: \"anthropic-messages\"`\n3. Anthropic OAuth credentials from `agent.db` (valid with at least a 5-minute buffer before expiry)\n4. Generic Anthropic environment variables: `ANTHROPIC_FOUNDRY_API_KEY` —> `ANTHROPIC_OAUTH_TOKEN` —> `ANTHROPIC_API_KEY`, with optional `ANTHROPIC_BASE_URL` (`FOUNDRY_BASE_URL` when Foundry mode is active)\n\nRelated search variables:\n\n| Variable | Default or behavior |\n| --- | --- |\n| `ANTHROPIC_SEARCH_API_KEY` | Highest-priority explicit search API key |\n| `ANTHROPIC_SEARCH_BASE_URL` | Defaults to `https://api.anthropic.com` when omitted |\n| `ANTHROPIC_SEARCH_MODEL` | Defaults to `claude-haiku-4-5` |\n| `ANTHROPIC_BASE_URL` | Generic fallback base URL for tier-4 authentication |\n\n### Perplexity OAuth flow behavior\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_AUTH_NO_BORROW` | When set, disables the macOS native application token borrowing path during Perplexity login |\n\n---\n\n## Python tooling and kernel runtime\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_PY` | Python tool mode override: `0` or `bash` (`bash-only`), `1` or `py` (`ipy-only`), `mix` or `both` (`both`); invalid values are ignored |\n| `PI_PYTHON_SKIP_CHECK` | When set to `1`, skips Python kernel availability and warm checks |\n| `PI_PYTHON_GATEWAY_URL` | When set, routes kernel execution to an external kernel gateway instead of the local shared gateway |\n| `PI_PYTHON_GATEWAY_TOKEN` | Optional authentication token for the external gateway (`Authorization: token <value>`) |\n| `PI_PYTHON_IPC_TRACE` | When set to `1`, enables low-level IPC trace logging in the kernel module |\n| `VIRTUAL_ENV` | Highest-priority virtual environment path for Python runtime resolution |\n\nAdditional runtime rules:\n\n- When `BUN_ENV=test` or `NODE_ENV=test`, Python availability checks pass automatically and kernel warming is skipped.\n- The Python execution environment filters out common API keys while preserving safe base variables and prefixes matching `LC_*`, `XDG_*`, and `PI_*`.\n\n---\n\n## Agent and runtime behavior toggles\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_SMOL_MODEL` | Ephemeral model role override for `smol` (CLI `--smol` takes precedence) |\n| `PI_SLOW_MODEL` | Ephemeral model role override for `slow` (CLI `--slow` takes precedence) |\n| `PI_PLAN_MODEL` | Ephemeral model role override for `plan` (CLI `--plan` takes precedence) |\n| `PI_NO_TITLE` | When set to any non-empty value, disables automatic session title generation on the first user message |\n| `NULL_PROMPT` | When set to `true`, the system prompt builder returns an empty string |\n| `PI_BLOCKED_AGENT` | Blocks a specific subagent type from being spawned by the task tool |\n| `PI_SUBPROCESS_CMD` | Overrides the subagent spawn command (bypassing `xcsh` or `xcsh.cmd` resolution) |\n| `PI_TASK_MAX_OUTPUT_BYTES` | Maximum captured output bytes per subagent (default: `500000`) |\n| `PI_TASK_MAX_OUTPUT_LINES` | Maximum captured output lines per subagent (default: `5000`) |\n| `PI_TIMING` | When set to `1`, enables startup and tool execution timing instrumentation logs |\n| `PI_DEBUG_STARTUP` | Enables startup stage debug logging to standard error |\n| `PI_PACKAGE_DIR` | Overrides package asset base directory resolution for documentation, examples, and changelogs |\n| `PI_DISABLE_LSPMUX` | When set to `1`, disables lspmux detection and forces direct LSP server spawning |\n| `LITELLM_BASE_URL` | LiteLLM proxy base URL; when set alongside `LITELLM_API_KEY`, generates `models.yml` on first run and validates configuration on startup |\n| `LM_STUDIO_BASE_URL` | Base URL override for implicit LM Studio discovery (defaults to `http://127.0.0.1:1234/v1`) |\n| `OLLAMA_BASE_URL` | Base URL override for implicit Ollama discovery (defaults to `http://127.0.0.1:11434`) |\n| `LLAMA_CPP_BASE_URL` | Base URL override for implicit Llama.cpp discovery (defaults to `http://127.0.0.1:8080`) |\n| `PI_EDIT_VARIANT` | When set to `hashline`, forces hashline read and grep display mode when the edit tool is active |\n| `PI_NO_PTY` | When set to `1`, disables the interactive PTY path for the bash tool (also set internally by `--no-pty`) |\n\n---\n\n## Storage and configuration root paths\n\nThese variables are consumed via `@f5-sales-demo/pi-utils/dirs` and govern data storage locations:\n\n| Variable | Default or behavior |\n| --- | --- |\n| `PI_CONFIG_DIR` | Configuration root directory name under the user home directory (defaults to `.xcsh`) |\n| `PI_CODING_AGENT_DIR` | Full path override for the agent directory (defaults to `~/<PI_CONFIG_DIR or .xcsh>/agent`) |\n| `PWD` | Used when matching canonical current working directories in path resolution helpers |\n\n---\n\n## Shell and tool execution environment\n\nThese variables govern subprocess management in `packages/utils/src/procmgr.ts` and the bash tool integration:\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_BASH_NO_CI` | Suppresses automatic `CI=true` injection into spawned shell environments |\n| `CLAUDE_BASH_NO_CI` | Legacy fallback alias for `PI_BASH_NO_CI` |\n| `PI_BASH_NO_LOGIN` | Disables login shell mode when spawning bash processes |\n| `CLAUDE_BASH_NO_LOGIN` | Legacy fallback alias for `PI_BASH_NO_LOGIN` |\n| `PI_SHELL_PREFIX` | Optional command prefix wrapper for shell executions |\n| `CLAUDE_CODE_SHELL_PREFIX` | Legacy fallback alias for `PI_SHELL_PREFIX` |\n| `VISUAL` | Preferred external editor command |\n| `EDITOR` | Fallback external editor command |\n\n---\n\n## Terminal and session environment variables\n\nThe runtime detects the following environment variables automatically from the host environment:\n\n| Variable | Used for |\n| --- | --- |\n| `COLORTERM`, `TERM`, `WT_SESSION` | Terminal color capability detection and theme selection |\n| `COLORFGBG` | Terminal background light and dark mode auto-detection |\n| `TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `TERMINAL_EMULATOR` | Terminal identity detection for system prompt context |\n| `KDE_FULL_SESSION`, `XDG_CURRENT_DESKTOP`, `DESKTOP_SESSION`, `XDG_SESSION_DESKTOP`, `GDMSESSION`, `WINDOWMANAGER` | Desktop environment and window manager detection for system prompt context |\n| `KITTY_WINDOW_ID`, `TMUX_PANE`, `TERM_SESSION_ID`, `WT_SESSION` | Stable per-terminal session breadcrumb identifiers |\n| `SHELL`, `ComSpec`, `TERM_PROGRAM`, `TERM` | System diagnostics and shell detection |\n| `APPDATA`, `XDG_CONFIG_HOME` | lspmux configuration path resolution |\n| `HOME` | Path shortening in MCP command user interface |\n\n---\n\n## Native loader and debug flags\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_DEV` | Enables verbose native addon load diagnostics in `packages/natives` |\n\n---\n\n## Terminal user interface runtime flags\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_NOTIFICATIONS` | Set to `off`, `0`, or `false` to suppress desktop notifications |\n| `PI_TUI_WRITE_LOG` | When set, logs TUI write operations to a specified file |\n| `PI_HARDWARE_CURSOR` | When set to `1`, enables hardware cursor mode |\n| `PI_CLEAR_ON_SHRINK` | When set to `1`, clears empty rows when rendered content shrinks |\n| `PI_DEBUG_REDRAW` | When set to `1`, enables redraw debug logging |\n| `PI_TUI_DEBUG` | When set to `1`, enables deep TUI debug dump logging |\n\n---\n\n## Commit generation controls\n\n| Variable | Behavior |\n| --- | --- |\n| `PI_COMMIT_TEST_FALLBACK` | When set to `true` (case-insensitive), forces the commit fallback generation path |\n| `PI_COMMIT_NO_FALLBACK` | When set to `true`, disables fallback when the commit agent returns no proposal |\n| `PI_COMMIT_MAP_REDUCE` | When set to `false`, disables the map-reduce commit analysis path |\n| `DEBUG` | When set, prints commit agent error stack traces to standard error |\n\n---\n\n## Security-sensitive variables\n\nProtect the following variables as sensitive secrets; do not log or commit them:\n\n- Provider API keys and OAuth tokens (`*_API_KEY`, `*_TOKEN`, OAuth access and refresh tokens).\n- Cloud credentials (`AWS_*`, `GOOGLE_APPLICATION_CREDENTIALS`).\n- Web search authentication keys (`EXA_API_KEY`, `BRAVE_API_KEY`, `PERPLEXITY_API_KEY`, Anthropic search keys).\n- Foundry mutual TLS credentials (`CLAUDE_CODE_CLIENT_CERT`, `CLAUDE_CODE_CLIENT_KEY`, `NODE_EXTRA_CA_CERTS`).\n\nThe Python runtime automatically strips these sensitive keys before spawning kernel subprocesses (`packages/coding-agent/src/ipy/runtime.ts`).\n",
116
116
  "en/configuration/fs-scan-cache-architecture.md": "---\ntitle: Filesystem Scan Cache Architecture\ndescription: Filesystem scan cache contract for fast file discovery with stale-while-revalidate semantics.\nsidebar:\n order: 8\n label: Filesystem scan cache\n---\n\nThis document defines the contract for the shared filesystem scan cache implemented in Rust (`crates/pi-natives/src/fs_cache.rs`) and consumed by native discovery and search APIs exposed to `packages/coding-agent`.\n\n## Purpose and design goals\n\nThe cache stores full directory-scan entry lists (`GlobMatch[]`) keyed by scan scope and traversal policy, enabling higher-level operations (such as glob filtering, fuzzy scoring, and grep candidate selection) to run against cached entries.\n\nPrimary goals:\n\n- Eliminate redundant filesystem traversals across repeated discovery and search operations.\n- Maintain consistency across `glob`, `fuzzyFind`, and `grep` when sharing the same scan policy.\n- Support explicit staleness recovery for empty results and explicit invalidation following filesystem mutations.\n\n## Ownership and public surface\n\n- Cache implementation and policy: `crates/pi-natives/src/fs_cache.rs`\n- Native consumers:\n - `crates/pi-natives/src/glob.rs`\n - `crates/pi-natives/src/fd.rs` (`fuzzyFind`)\n - `crates/pi-natives/src/grep.rs`\n- JavaScript and TypeScript bindings and exports:\n - `packages/natives/src/glob/index.ts` (`invalidateFsScanCache`)\n - `packages/natives/src/glob/types.ts`\n - `packages/natives/src/grep/types.ts`\n- Coding agent mutation invalidation helpers:\n - `packages/coding-agent/src/tools/fs-cache-invalidation.ts`\n\n## Cache key partitioning\n\nEach entry is keyed by:\n\n- Canonicalized `root` directory path\n- `include_hidden` boolean\n- `use_gitignore` boolean\n\nKey contract implications:\n\n- Scans with and without hidden files do **not** share cache entries.\n- Scans with and without `.gitignore` filtering do **not** share cache entries.\n- Consumers must supply stable boolean flags for hidden and `.gitignore` behavior; changing either flag resolves to a distinct cache partition.\n\nThe cache key does **not** partition on `node_modules` inclusion. The cache always captures entries with `node_modules` included, deferring filtering to consumer-specific post-processing.\n\n## Scan collection behavior\n\nCache population uses a deterministic directory walker (`ignore::WalkBuilder`) configured by `include_hidden` and `use_gitignore`:\n\n- Symlink traversal disabled (`follow_links(false)`).\n- Entries sorted deterministically by file path.\n- The `.git` directory is always excluded.\n- `node_modules` is collected during cache scan and filtered subsequently if requested.\n- Entry file types and modification timestamps (`mtime`) are captured via `symlink_metadata`.\n\nSearch roots are resolved through `resolve_search_path`:\n\n- Relative paths resolve against the current working directory.\n- The target path must reference an existing directory.\n- The root path is canonicalized when possible.\n\n## Freshness and eviction policy\n\nGlobal policy configuration (overridable via environment variables):\n\n- `FS_SCAN_CACHE_TTL_MS` (default: `1000`)\n- `FS_SCAN_EMPTY_RECHECK_MS` (default: `200`)\n- `FS_SCAN_CACHE_MAX_ENTRIES` (default: `16`)\n\nRuntime behavior:\n\n- `get_or_scan(...)`:\n - When TTL is `0`, bypasses the cache and performs a fresh scan (`cache_age_ms = 0`).\n - On a cache hit within TTL, returns cached entries along with non-zero `cache_age_ms`.\n - On an expired hit, evicts the entry, rescans, and caches the fresh result.\n- Maximum entry limits enforce oldest-first eviction based on `created_at`.\n\n## Empty-result fast recheck\n\nNormal cache hit behavior:\n\n- A cache hit within the TTL window returns cached entries directly.\n\nEmpty-result fast recheck behavior:\n\n- Represents a **caller-side** policy evaluated using `ScanResult.cache_age_ms`.\n- If the filtered or queried result is empty and the cached scan age is at least `empty_recheck_ms()`, the caller triggers a single `force_rescan(...)` and retries.\n- Reduces stale negative results when files were added recently while the cache remains within its TTL window.\n\nActive consumers:\n\n- `glob`: Rechecks when filtered matches are empty and scan age exceeds the threshold.\n- `fuzzyFind` (`fd.rs`): Rechecks when the query is non-empty and scored matches return empty.\n- `grep`: Rechecks when the selected candidate file list is empty.\n\n## Consumer defaults and cache usage\n\nCache usage is opt-in across all exposed APIs (`cache?: boolean`, defaulting to `false`).\n\nDefault settings in native APIs:\n\n- `glob`: `hidden=false`, `gitignore=true`, `cache=false`\n- `fuzzyFind`: `hidden=false`, `gitignore=true`, `cache=false`\n- `grep`: `hidden=true`, `cache=false`, with cache scan enforcing `use_gitignore=true`\n\nCoding agent callers:\n\n- High-volume file mention discovery enables caching:\n - File: `packages/coding-agent/src/utils/file-mentions.ts`\n - Profile: `hidden=true`, `gitignore=true`, `includeNodeModules=true`, `cache=true`\n- Tool-level `grep` integration disables scan caching (`cache: false`):\n - File: `packages/coding-agent/src/tools/grep.ts`\n\n## Invalidation contract\n\nNative invalidation entrypoint:\n\n- `invalidateFsScanCache(path?: string)`:\n - When provided with a `path`, invalidates cache entries whose root matches a prefix of the target path.\n - When called without arguments, clears all scan cache entries.\n\nPath handling behavior:\n\n- Relative paths resolve against the current working directory.\n- Invalidation attempts path canonicalization.\n- If the target path does not exist (such as following a file deletion), the fallback canonicalizes the parent directory and reattaches the filename when possible.\n- Preserves accurate invalidation behavior across file creation, deletion, and rename operations.\n\n## Mutation flow responsibilities\n\nCoding agent execution paths must invoke invalidation helpers after successful filesystem mutations.\n\nPrimary helpers:\n\n- `invalidateFsScanAfterWrite(path)`\n- `invalidateFsScanAfterDelete(path)`\n- `invalidateFsScanAfterRename(oldPath, newPath)` (invalidates both source and destination paths)\n\nCurrent tool callsites:\n\n- `packages/coding-agent/src/tools/write.ts`\n- `packages/coding-agent/src/patch/index.ts` (hashline, patch, and replace pipelines)\n\nRule: Any workflow that mutates filesystem contents or paths without calling these helpers introduces cache staleness.\n\n## Adding a new cache consumer\n\nWhen introducing cache usage in a new scanning or search path:\n\n1. **Use stable scan policy inputs**: Define hidden file and `.gitignore` semantics first, and pass them consistently to `get_or_scan` and `force_rescan`.\n2. **Treat cached data as pre-filtered only by traversal policy**: Apply tool-specific filtering (glob patterns, file types, and `node_modules` exclusions) after retrieving cached entries.\n3. **Implement empty-result fast rechecks only for stale negative risks**: Check `scan.cache_age_ms >= empty_recheck_ms()`, retry once with `force_rescan(..., store=true, ...)`, and isolate this path from standard cache hit handling.\n4. **Respect no-cache mode explicitly**: When a caller disables caching, invoke `force_rescan(..., store=false, ...)` to avoid populating the shared cache.\n5. **Wire mutation invalidation into write paths**: Call coding agent invalidation helpers following successful write, edit, delete, or rename actions. For renames and moves, invalidate both paths.\n6. **Rely on global TTL configuration**: Adhere to environment-configured global TTL policies rather than introducing per-call TTL parameters.\n\n## Known boundaries\n\n- Cache scope is process-local and held in memory (`DashMap`), without persistence across process restarts.\n- The cache stores directory scan entries rather than final tool evaluation results.\n- `glob`, `fuzzyFind`, and `grep` share entries only when all key dimensions (`root`, `hidden`, `gitignore`) match exactly.\n- The `.git` directory is always excluded during scan collection regardless of caller options.\n",
117
117
  "en/configuration/herdr.md": "---\ntitle: Herdr terminals\ndescription: Run xcsh conversations with isolated, conversation-owned support terminals.\n---\n\nThe `xcsh` runtime binds individual conversations to dedicated Herdr workspaces and exposes named support terminals as tabs. The recommended rich-media stack consists of Ghostty with Kitty graphics support, Herdr configured with `experimental.kitty_graphics=true`, and FFmpeg 6 or later. Other terminal emulators receive static text fallbacks automatically.\n\n## Media and graphics protocol negotiation\n\nWhen a released Herdr version exposes `HERDR_KITTY_GRAPHICS=1` alongside `HERDR_ENV=1`, `xcsh` enables the Kitty image protocol for TTY output even though Herdr reports `TERM=xterm-256color`. Both environment markers must be set to `1`; if either marker is missing or contains any other value, image rendering remains disabled for that generic terminal type.\n\nThe `PI_FORCE_IMAGE_PROTOCOL` variable overrides automatic detection. If you modify your Herdr configuration, restart your terminal applications so child processes inherit the updated environment variables.\n\n## Launching conversation-owned workspaces\n\nLaunch a conversation-owned workspace from outside Herdr:\n\n```bash\nxcsh herdr --session my-organization --label project-task -- --model openai/gpt-5\n```\n\nThe launcher executes the following sequence:\n\n1. Creates the Herdr workspace.\n2. Writes a mode-`0600` `HerdrBindingV1` file under the user state directory.\n3. Starts `xcsh` in the root pane.\n4. Attaches to the named Herdr session.\n\nStandard `xcsh` invocations remain supported; terminal management operations report as unavailable unless both a launcher-issued owner binding and `HERDR_SOCKET_PATH` exist in the runtime environment.\n\n## Terminal management actions\n\nWithin a conversation, users invoke `/terminal` and agents invoke `herdr_terminal`. Both interfaces support the following actions:\n\n- `list`: Enumerate active terminals.\n- `create`: Create a new terminal tab (creation is non-focusing).\n- `run`: Execute a command in a terminal.\n- `send`: Send input characters or keystrokes to a terminal.\n- `read`: Read captured output from a terminal.\n- `wait`: Wait for a running command to finish.\n- `status`: Inspect terminal process status.\n- `focus`: Explicitly switch focus to the target terminal.\n- `close`: Terminate and close a terminal.\n\nClosing a busy terminal requires explicit confirmation; the initial close request is rejected unless repeated with `force: true` (or `/terminal close <TERMINAL_NAME> --force`).\n\n## Security and isolation\n\nOwnership isolation is enforced through a randomly generated `xcsh_owner` metadata token attached to the workspace and each managed pane. `xcsh` only lists, reads, interacts with, focuses, and terminates panes bearing the active binding token. Terminals belonging to other conversations or users remain hidden.\n\nSession creation, resumption, switching, branching, and tree navigation retain the existing workspace and tabs while updating the stored `xcsh` session reference.\n\n## Socket protocol and resilience\n\nThe socket client communicates using Herdr protocol 18 over newline-delimited JSON across `HERDR_SOCKET_PATH`. Each request establishes an independent connection, validates the `ping` protocol version, enforces a 4 MiB response cap, and applies bounded timeouts to prevent Herdr restarts from blocking the primary conversation process.\n",
118
118
  "en/configuration/hooks.md": "---\ntitle: Hooks\ndescription: Hook system for pre/post event automation in the coding agent lifecycle.\nsidebar:\n order: 4\n label: Hooks\n---\n\nThis document describes the hook subsystem implementation located in `src/extensibility/hooks/*`.\n\n## Current runtime status\n\nThe hook package (`src/extensibility/hooks/`) provides a public API surface, while the default CLI runtime initializes the extension runner path:\n\n- `--hook` operates as an alias for `--extension` (CLI paths merge into `additionalExtensionPaths`).\n- Tools are wrapped by `ExtensionToolWrapper` rather than `HookToolWrapper`.\n- Context transformations and lifecycle event emissions route through `ExtensionRunner`.\n\nThis document covers the hook subsystem implementation (types, loader, runner, and wrapper), including backward-compatibility behaviors and constraints.\n\n## Key files\n\n- `src/extensibility/hooks/types.ts` — Hook context, event types, and result contracts.\n- `src/extensibility/hooks/loader.ts` — Module loading and hook discovery bridge.\n- `src/extensibility/hooks/runner.ts` — Event dispatch, command lookup, and error signaling.\n- `src/extensibility/hooks/tool-wrapper.ts` — Pre-execution and post-execution tool interception wrapper.\n- `src/extensibility/hooks/index.ts` — Public module exports and re-exports.\n\n## Hook module anatomy\n\nA hook module must provide a default export of a factory function:\n\n```ts\nimport type { HookAPI } from \"@f5-sales-demo/xcsh\";\n\nexport default function hook(pi: HookAPI): void {\n pi.on(\"tool_call\", async (event, ctx) => {\n if (event.toolName === \"bash\" && String(event.input.command ?? \"\").includes(\"rm -rf\")) {\n return { block: true, reason: \"blocked by policy\" };\n }\n });\n}\n```\n\nThe factory function supports the following operations:\n\n- Register event handlers with `pi.on(...)`.\n- Send persistent custom messages with `pi.sendMessage(...)`.\n- Persist non-LLM state entries with `pi.appendEntry(...)`.\n- Register slash commands via `pi.registerCommand(...)`.\n- Register custom message renderers via `pi.registerMessageRenderer(...)`.\n- Execute shell commands via `pi.exec(...)`.\n\n## Discovery and loading\n\n`discoverAndLoadHooks(configuredPaths, cwd)` executes the following sequence:\n\n1. Loads discovered hooks from the capability registry (`loadCapability(\"hooks\")`).\n2. Appends explicitly configured paths (deduplicating by canonical absolute path).\n3. Invokes `loadHooks(allPaths, cwd)`.\n\n`loadHooks` imports each path dynamically and expects a `default` export function.\n\n### Path resolution\n\n`loader.ts` resolves hook paths as follows:\n\n- Absolute paths: Used directly.\n- Home directory (`~`) paths: Expanded to the user home directory.\n- Relative paths: Resolved against `cwd`.\n\n### Module format requirements\n\nCapability discovery for `hookCapability` scans pre-execution and post-execution shell hooks (such as `.claude/hooks/pre/*` and `.xcsh/.../hooks/pre/*`).\n\nThe hook loader uses dynamic module imports requiring a default JavaScript or TypeScript export function. If a discovered file path cannot be imported as a JavaScript/TypeScript module, loading fails and records the failure in `LoadHooksResult.errors`.\n\n## Event surfaces\n\nHook events are strongly typed in `types.ts`.\n\n### Session events\n\n- `session_start`\n- `session_before_switch` — Can return `{ cancel?: boolean }`\n- `session_switch`\n- `session_before_branch` — Can return `{ cancel?: boolean; skipConversationRestore?: boolean }`\n- `session_branch`\n- `session_before_compact` — Can return `{ cancel?: boolean; compaction?: CompactionResult }`\n- `session.compacting` — Can return `{ context?: string[]; prompt?: string; preserveData?: Record<string, unknown> }`\n- `session_compact`\n- `session_before_tree` — Can return `{ cancel?: boolean; summary?: { summary: string; details?: unknown } }`\n- `session_tree`\n- `session_shutdown`\n\n### Agent and context events\n\n- `context` — Can return `{ messages?: Message[] }`\n- `before_agent_start` — Can return `{ message?: { customType; content; display; details } }`\n- `agent_start`\n- `agent_end`\n- `turn_start`\n- `turn_end`\n- `auto_compaction_start`\n- `auto_compaction_end`\n- `auto_retry_start`\n- `auto_retry_end`\n- `ttsr_triggered`\n- `todo_reminder`\n\n### Tool interception events\n\n- `tool_call` (pre-execution) — Can return `{ block?: boolean; reason?: string }`\n- `tool_result` (post-execution) — Can return `{ content?; details?; isError? }`\n\n```text\nHook tool interception flow\n\ntool_call handlers\n │\n ├─ any { block: true }? ── yes ──> throw (tool blocked)\n │\n └─ no\n │\n ▼\n execute underlying tool\n │\n ├─ success ──> tool_result handlers can override { content, details }\n │\n └─ error ──> emit tool_result(isError=true) then rethrow original error\n```\n\n## Execution model and mutation semantics\n\n### 1. Pre-execution: `tool_call`\n\n`HookToolWrapper.execute()` emits `tool_call` before executing the underlying tool:\n\n- If any handler returns `{ block: true }`, execution halts immediately.\n- If a handler throws an unhandled error, the wrapper fails closed and blocks execution.\n- The returned `reason` string becomes the thrown error message.\n\n### 2. Tool execution\n\nThe underlying tool executes normally if no handler blocks the call.\n\n### 3. Post-execution: `tool_result`\n\nUpon successful execution, the wrapper emits `tool_result` containing:\n\n- `toolName`, `toolCallId`, and `input`\n- `content`\n- `details`\n- `isError: false`\n\nIf a handler returns overrides:\n\n- `content` can replace the returned result content.\n- `details` can replace the returned result details.\n\nOn tool failure, the wrapper emits `tool_result` with `isError: true` and the error text content, then rethrows the original error.\n\n### Supported mutations\n\nHooks can perform the following modifications:\n\n- Mutate LLM context for a single prompt via `context` (message replacement chain).\n- Override tool output content and details on successful tool executions (`tool_result`).\n- Inject pre-execution messages via `before_agent_start`.\n- Control session compaction, branch, and tree operations via `session_before_*` and `session.compacting`.\n\n### Unsupported mutations\n\nThis hook implementation does not support:\n\n- Mutating raw tool input parameters in place (handlers can only permit or block via `tool_call`).\n- Suppressing thrown tool errors (error handling paths rethrow after emission).\n- Overriding the final tool status (`isError`) within `HookToolWrapper`.\n\n## Ordering and conflict resolution\n\n### Discovery-level ordering\n\nCapability providers are sorted in descending order by numeric priority. Deduplication uses the capability key, where the first item encountered wins.\n\nFor `hooks`, the capability key is `${type}:${tool}:${name}`. Shadowed duplicates from lower-priority providers are excluded from the active discovery list.\n\n### Load ordering\n\n`discoverAndLoadHooks` constructs a flat `allPaths` list deduplicated by resolved absolute path, and `loadHooks` processes them in order. File ordering within individual directories reflects filesystem `readdir` order without additional sorting.\n\n### Runtime handler ordering\n\nInside `HookRunner`, handlers execute deterministically in registration sequence:\n\n1. Hook module loading order.\n2. Handler registration order per hook and event.\n\nConflict resolution rules by event type:\n\n- `tool_call`: The last returned result applies unless a handler blocks; any block short-circuits immediately.\n- `tool_result`: The last returned override applies without short-circuiting.\n- `context`: Chained sequentially; each handler receives the message output of the previous handler.\n- `before_agent_start`: The first returned message persists; subsequent messages are ignored.\n- `session_before_*`: The latest returned result applies; `cancel: true` short-circuits immediately.\n- `session.compacting`: The latest returned result applies.\n\nCommand and renderer conflict rules:\n\n- `getCommand(name)` returns the first match across registered hooks.\n- `getMessageRenderer(customType)` returns the first match across registered hooks.\n- `getRegisteredCommands()` returns all registered commands without deduplication.\n\n## User-interface interactions\n\n`HookUIContext` exposes the following methods:\n\n- `select`, `confirm`, `input`, `editor`\n- `notify`\n- `setStatus`\n- `custom`\n- `setEditorText`, `getEditorText`\n- `theme` getter\n\n`ctx.hasUI` indicates whether interactive UI capabilities are available.\n\nWhen running in headless or non-interactive mode:\n\n- `select`, `input`, and `editor` return `undefined`.\n- `confirm` returns `false`.\n- `notify`, `setStatus`, and `setEditorText` operate as no-ops.\n- `getEditorText` returns an empty string (`\"\"`).\n\n### Status line behavior\n\nStatus text registered via `ctx.ui.setStatus(key, text)`:\n\n- Persists in a key-value store per status key.\n- Sorts alphabetically by key name.\n- Sanitizes whitespace (`\\r`, `\\n`, `\\t` convert to spaces; contiguous spaces collapse).\n- Joins and truncates content to fit terminal display width.\n\n## Error handling and propagation\n\n### Load-time errors\n\n- Invalid modules or missing default exports are recorded in `LoadHooksResult.errors`.\n- Loading continues for remaining valid hook modules.\n\n### Event-time errors\n\n`HookRunner.emit(...)` catches handler errors for most event types, emits a `HookError` event (`hookPath`, `event`, `error`) to registered listeners, and continues execution.\n\n`emitToolCall(...)` applies stricter handling: handler errors propagate to the caller without suppression. In `HookToolWrapper`, any thrown error blocks the tool call fail-closed.\n\n## API examples\n\n### Block unsafe commands\n\n```ts\nimport type { HookAPI } from \"@f5-sales-demo/xcsh\";\n\nexport default function (pi: HookAPI): void {\n pi.on(\"tool_call\", async (event, ctx) => {\n if (event.toolName !== \"bash\") return;\n const cmd = String(event.input.command ?? \"\");\n if (!cmd.includes(\"rm -rf\")) return;\n\n if (!ctx.hasUI) return { block: true, reason: \"rm -rf blocked in headless mode\" };\n const confirmed = await ctx.ui.confirm(\"Dangerous command detected\", `Allow execution: ${cmd}`);\n if (!confirmed) return { block: true, reason: \"User denied execution\" };\n });\n}\n```\n\n### Redact sensitive tool output\n\n```ts\nimport type { HookAPI } from \"@f5-sales-demo/xcsh\";\n\nexport default function (pi: HookAPI): void {\n pi.on(\"tool_result\", async event => {\n if (event.toolName !== \"read\" || event.isError) return;\n\n const redacted = event.content.map(chunk => {\n if (chunk.type !== \"text\") return chunk;\n return { ...chunk, text: chunk.text.replaceAll(/API_KEY=\\S+/g, \"API_KEY=[REDACTED]\") };\n });\n\n return { content: redacted };\n });\n}\n```\n\n### Modify model context\n\n```ts\nimport type { HookAPI } from \"@f5-sales-demo/xcsh\";\n\nexport default function (pi: HookAPI): void {\n pi.on(\"context\", async event => {\n const filtered = event.messages.filter(msg => !(msg.role === \"custom\" && msg.customType === \"debug-only\"));\n return { messages: filtered };\n });\n}\n```\n\n### Register slash commands\n\n```ts\nimport type { HookAPI } from \"@f5-sales-demo/xcsh\";\n\nexport default function (pi: HookAPI): void {\n pi.registerCommand(\"handoff\", {\n description: \"Create a new session with an initial setup message\",\n handler: async (_args, ctx) => {\n await ctx.waitForIdle();\n await ctx.newSession({\n parentSession: ctx.sessionManager.getSessionFile(),\n setup: async sm => {\n sm.appendMessage({\n role: \"user\",\n content: [{ type: \"text\", text: \"Continue from prior session summary.\" }],\n timestamp: Date.now(),\n });\n },\n });\n },\n });\n}\n```\n\n## Exported package surface\n\n`src/extensibility/hooks/index.ts` exports:\n\n- Loading APIs (`discoverAndLoadHooks`, `loadHooks`)\n- Runner and wrapper classes (`HookRunner`, `HookToolWrapper`)\n- All TypeScript hook types\n- Re-export of `execCommand`\n\nThe package root (`src/index.ts`) re-exports hook TypeScript types for backward compatibility.\n",
@@ -143,8 +143,8 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
143
143
  "en/natives/natives-text-search-pipeline.md": "---\ntitle: Natives Text and Search Pipeline\ndescription: Native text search pipeline with grep, glob, and ripgrep-based file content indexing.\nsidebar:\n order: 6\n label: Text & search pipeline\n---\n\nThis document maps the text and search subsystems (`grep`, `glob`, `text`, and `highlight`) in `@f5-sales-demo/pi-natives` from TypeScript wrappers to Rust Node-API exports and result structures.\n\n## Implementation files\n\n- `packages/natives/src/grep/index.ts`: TypeScript wrapper for regex search and fuzzy find.\n- `packages/natives/src/glob/index.ts`: TypeScript wrapper for filesystem globbing.\n- `packages/natives/src/text/index.ts`: TypeScript wrapper for ANSI-aware text layout utilities.\n- `packages/natives/src/highlight/index.ts`: TypeScript wrapper for code syntax highlighting.\n- `crates/pi-natives/src/grep.rs`: Multithreaded regular expression search engine (`grep-regex`).\n- `crates/pi-natives/src/glob.rs`: High-performance filesystem glob matcher.\n- `crates/pi-natives/src/glob_util.rs`: Glob compilation and syntax sanitization helpers.\n- `crates/pi-natives/src/fs_cache.rs`: In-memory filesystem directory scan cache.\n- `crates/pi-natives/src/fd.rs`: Path scoring and fuzzy search implementation.\n- `crates/pi-natives/src/text.rs`: ANSI escape sequence parsing and Unicode column width calculations.\n- `crates/pi-natives/src/highlight.rs`: Syntax highlighting engine powered by `syntect`.\n\n## Subsystem architectures\n\n### 1. Regular expression search (`grep`, `searchContent`, `hasMatch`)\n\n- **In-memory search**: `searchContent` and `hasMatch` execute regular expressions against provided strings or `Uint8Array` buffers without filesystem access.\n- **Filesystem search**: `grep` scans files or directories, streams matches through callbacks, and utilizes `fs_cache` for directory entry indexing.\n- **Regex engine**: Powered by `grep-regex` with support for multiline matching and case-insensitivity. Automatically sanitizes unmatched template braces (such as `${VAR}`) to prevent syntax compilation errors.\n\n### 2. Filesystem pattern matching (`glob`) and fuzzy find (`fuzzyFind`)\n\n- **`glob`**: Resolves paths, compiles pattern strings into glob matchers, excludes standard ignored directories (`.git`, `node_modules`), and optionally sorts results by modification timestamp (`sortByMtime`).\n- **`fuzzyFind`**: Executes fuzzy path scoring across directory entries, prioritizing directory prefixes and exact subsequence matches.\n\n### 3. Shared filesystem scan cache (`fs_cache`)\n\n`fs_cache` accelerates repeated directory lookups:\n\n- Caches relative directory entries keyed by canonical root paths and filter flags (`include_hidden`, `use_gitignore`).\n- Supports explicit cache invalidation via `invalidateFsScanCache(path?)`.\n\n### 4. ANSI text layout utilities (`text`)\n\n- **`wrapTextWithAnsi(text, width)`**: Wraps text to column limits while preserving active SGR color codes across line breaks.\n- **`truncateToWidth(text, width, ellipsis)`**: Truncates strings to visible terminal cell limits.\n- **`visibleWidth(text)`**: Calculates visible column width, correctly parsing full-width Unicode characters and ignoring non-printable escape codes.\n\n### 5. Syntax highlighting (`highlight`)\n\n- Uses `syntect` to tokenize code into scopes and map them to standard ANSI color sequences.\n- Supports automatic language resolution by name, extension, or alias table (`ts`, `tsx`, `js` -> `JavaScript`).\n\n## TypeScript API and native export mapping\n\n| TypeScript API | Native Node-API export | Rust module | Description |\n| --- | --- | --- | --- |\n| `grep(options, onMatch?)` | `grep` | `grep.rs` | Multithreaded regular expression search across files or directories. |\n| `searchContent(content, options)` | `search` | `grep.rs` | In-memory regular expression search. |\n| `hasMatch(content, pattern, opts?)` | `hasMatch` | `grep.rs` | Fast boolean regex match test. |\n| `fuzzyFind(options)` | `fuzzyFind` | `fd.rs` | Fuzzy path matching across filesystem trees. |\n| `glob(options, onMatch?)` | `glob` | `glob.rs` | Filesystem pattern matching and discovery. |\n| `invalidateFsScanCache(path?)` | `invalidateFsScanCache` | `fs_cache.rs` | Clears directory scan caches. |\n| `wrapTextWithAnsi(text, width)` | `wrapTextWithAnsi` | `text.rs` | ANSI-preserving text column wrapping. |\n| `truncateToWidth(text, width, ellipsis)` | `truncateToWidth` | `text.rs` | Terminal cell width truncation. |\n| `visibleWidth(text)` | `visibleWidth` | `text.rs` | Calculates visible column width. |\n| `highlightCode(code, lang, colors)` | `highlightCode` | `highlight.rs` | Generates ANSI-highlighted source code. |\n| `supportsLanguage(lang)` | `supportsLanguage` | `highlight.rs` | Checks syntax highlighting support for a language. |\n| `getSupportedLanguages()` | `getSupportedLanguages` | `highlight.rs` | Lists all supported syntax languages. |\n",
144
144
  "en/natives/porting-to-natives.md": "---\ntitle: Porting to pi-natives (Node-API) — Field notes\ndescription: Field notes for migrating Node.js child_process and shell code to the Rust N-API native layer.\nsidebar:\n order: 9\n label: Porting to pi-natives\n---\n\nThis guide describes how to migrate performance-critical execution paths to Rust in `crates/pi-natives` and expose them through TypeScript bindings in `@f5-sales-demo/pi-natives`.\n\n## When to port to native modules\n\nPort execution paths to Rust when:\n\n- Execution paths execute within high-frequency rendering loops or large batch data pipelines.\n- JavaScript memory allocations dominate execution time (excessive string allocations, regex backtracking, large array transformations).\n- The operation is CPU-bound or performs blocking I/O that can run on libuv worker threads (`task::blocking`).\n- The operation orchestrates asynchronous I/O that benefits from the Tokio runtime (`task::future`).\n\nAvoid porting operations that require access to JavaScript runtime state or dynamic module imports. Native exports must remain pure, data-in/data-out functions.\n\n## Native export architecture\n\n### Rust implementation layer (`crates/pi-natives/`)\n\n1. Implement core functionality in `crates/pi-natives/src/<MODULE>.rs`.\n2. Register the module in `crates/pi-natives/src/lib.rs`.\n3. Annotate functions with `#[napi]`. Rust `snake_case` function names convert to JavaScript `camelCase` identifiers automatically.\n4. Use `task::blocking` for CPU-bound computation and `task::future` for asynchronous operations. Pass `CancelToken` instances when supporting timeouts or `AbortSignal`.\n\n### TypeScript binding layer (`packages/natives/`)\n\n1. Define interface augmentations in `packages/natives/src/<MODULE>/types.ts` extending `NativeBindings` via declaration merging.\n2. Import `<MODULE>/types.ts` in `packages/natives/src/native.ts` to activate type definitions.\n3. Add validation entries in `validateNative` within `packages/natives/src/native.ts` to enforce required exports at startup.\n4. Implement ergonomic wrappers in `packages/natives/src/<MODULE>/index.ts` and re-export them from `packages/natives/src/index.ts`.\n\n## Step-by-step porting workflow\n\n1. **Implement Rust logic**: Create functions with owned types (`String`, `Vec<String>`, `Uint8Array`) and wrap long loops with `ct.heartbeat()?`.\n2. **Expose TypeScript bindings**: Add types in `<MODULE>/types.ts` and export wrapper functions.\n3. **Register native validation**: Add `checkFn(\"exportName\")` to `validateNative` in `packages/natives/src/native.ts`.\n4. **Create performance benchmarks**: Benchmark JavaScript and Rust implementations side-by-side using `Bun.nanoseconds()`.\n5. **Compile native binaries**: Run `bun --cwd=packages/natives run build`.\n6. **Validate performance**: Verify that the native path outperforms JavaScript before switching production call sites.\n\n## Troubleshooting common issues\n\n### Stale native binaries\n\nThe runtime loader prioritizes platform-tagged binaries (`pi_natives.<PLATFORM>-<ARCH>.node`). If exports fail to update:\n\n```bash\nrm packages/natives/native/pi_natives.*.node\nbun --cwd=packages/natives run build\n```\n\nIf testing against pre-compiled binaries, remove the extraction cache:\n\n```bash\nrm -rf ~/.xcsh/natives/\n```\n\n### Missing export validation errors\n\nIf `validateNative` raises an error indicating missing exports:\n\n- Verify that the Rust export name matches the expected JavaScript camelCase name.\n- Verify that the binary was rebuilt after adding the `#[napi]` attribute.\n- Never disable or weaken `validateNative` checks.\n",
145
145
  "en/office-test-ownership.md": "# Office add-in test ownership\n\nThis document defines the canonical test ownership and execution responsibilities for the `office-pane` package and Office add-in task pane integration tests.\n\n## Test suites and domain ownership\n\n| Test suite / harness | Location | Canonical owner | Execution tier | Description |\n| --- | --- | --- | --- | --- |\n| **Office Task Pane UI Unit Tests** | `packages/office-pane/src/**/*.test.ts` | Frontend / UI Core Team | Local and PR CI | Core Office.js state management and task pane rendering |\n| **Office Host Integration Harness** | `packages/office-pane/test/integration/` | Ecosystem Integrations Team | PR CI Matrix | Simulated Office.js API host binding and event handling |\n| **Acceptance End-to-End Suite** | `packages/office-pane/test/e2e/` | QA and Release Engineering | Nightly Release CI | Headless browser execution against Word and Excel task pane mocks |\n\n## Maintenance responsibilities\n\n- **Frontend / UI Core Team**: Responsible for unit test coverage on UI components in `office-pane`.\n- **Ecosystem Integrations Team**: Responsible for host mock fidelity and API contract alignment.\n- **Release Engineering**: Responsible for end-to-end environment stability and CI matrix runtimes.\n",
146
- "en/providers/models.md": "---\ntitle: Model and Provider Configuration\ndescription: Model registry and provider configuration via models.yml with routing, fallback, and pricing.\nsidebar:\n order: 1\n label: Models & providers\n---\n\nThis document describes how xcsh loads model registries, applies configuration overrides, resolves API credentials, and manages model selection at runtime.\n\n## Core implementation architecture\n\nModel configuration and runtime resolution are implemented across the following modules:\n\n- `src/config/model-registry.ts`: Loads built-in and custom models, manages provider overrides, discovers local models, and integrates authentication.\n- `src/config/model-resolver.ts`: Parses model patterns, handles canonical coalescing, and selects default, small, and reasoning models.\n- `src/config/settings-schema.ts`: Defines settings schemas for `modelRoles` and transport options.\n- `src/session/auth-storage.ts`: Resolves API keys and OAuth tokens across configuration sources.\n- `packages/ai/src/models.ts` and `packages/ai/src/types.ts`: Defines built-in models and compatibility contracts.\n\n## Configuration file path\n\nDefault configuration path:\n\n- `~/.xcsh/agent/models.yml`\n\n> [!NOTE]\n> If `models.yml` is missing and a legacy `models.json` file exists in the configuration directory, xcsh automatically migrates the settings to `models.yml`.\n\n## Schema structure\n\n```yaml\nconfigVersion: 1\nproviders:\n <PROVIDER_ID>:\n baseUrl: https://api.example.com/v1\n apiKey: MY_API_KEY_ENV_VAR\n api: openai-completions\n headers:\n X-Custom-Header: value\n auth: apiKey\n discovery:\n type: ollama\n modelOverrides:\n <MODEL_ID>:\n name: Custom Model Display Name\n models:\n - id: custom-model-id\n name: Custom Model Name\n api: openai-completions\n contextWindow: 128000\n maxTokens: 16384\n cost:\n input: 0\n output: 0\n cacheRead: 0\n cacheWrite: 0\nequivalence:\n overrides:\n <PROVIDER_ID>/<MODEL_ID>: <CANONICAL_MODEL_ID>\n exclude:\n - <PROVIDER_ID>/<MODEL_ID>\n```\n\n### Supported API protocol types\n\n- `openai-completions`\n- `openai-responses`\n- `openai-codex-responses`\n- `azure-openai-responses`\n- `anthropic-messages`\n- `google-generative-ai`\n- `google-vertex`\n\n## Validation rules\n\n### Custom provider definitions (`models` defined)\n\nThe following fields are required:\n\n- `baseUrl`: Base endpoint URL.\n- `apiKey`: Required unless `auth: none` is explicitly configured.\n- `api`: Protocol type specified at the provider or model level.\n\n### Override-only providers (`models` omitted)\n\nMust specify at least one of:\n\n- `baseUrl`\n- `modelOverrides`\n- `discovery`\n\n## Merge and override hierarchy\n\nWhen initializing or refreshing the model registry, xcsh applies configuration in the following order:\n\n1. **Built-in catalog**: Loads default providers and models from `@f5-sales-demo/pi-ai`.\n2. **Custom configuration**: Parses `~/.xcsh/agent/models.yml`.\n3. **Provider overrides**: Applies custom `baseUrl` and default `headers` to built-in models.\n4. **Model overrides**: Merges custom settings from `modelOverrides`.\n5. **Custom models**: Appends or replaces model definitions matching existing `provider/id` pairs.\n6. **Runtime discovery**: Queries active local endpoints (such as Ollama or LM Studio) and registers discovered models.\n\n## Canonical model equivalence and coalescing\n\nxcsh groups equivalent model checkpoints from different providers under canonical upstream identifiers (for example, `claude-sonnet-4`, `gpt-5.3-codex`):\n\n- **Overrides**: Maps specific provider identifiers to standard canonical names via `equivalence.overrides`.\n- **Exclusions**: Removes specific variants from automatic grouping via `equivalence.exclude`.\n- **Resolution priority**: Selects concrete providers based on credential availability and `modelProviderOrder` precedence.\n\n## Runtime model discovery\n\nxcsh detects local inference runtimes automatically:\n\n- **Ollama**: Probes `http://127.0.0.1:11434/api/tags` (or `OLLAMA_BASE_URL`).\n- **LM Studio**: Probes `http://127.0.0.1:1234/v1/models` (or `LM_STUDIO_BASE_URL`).\n- **llama.cpp**: Probes `http://127.0.0.1:8080/v1/models` (or `LLAMA_CPP_BASE_URL`).\n\n## Credential resolution hierarchy\n\nWhen resolving API keys for a provider, xcsh evaluates sources in the following priority order:\n\n1. CLI flag `--api-key`\n2. Stored API keys in `agent.db`\n3. Stored OAuth tokens in `agent.db`\n4. Standard environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`)\n5. Provider `apiKey` field in `models.yml` (evaluated first as an environment variable name, then as a literal token)\n\n## Model roles and selection\n\nxcsh supports logical role aliases configured via `settings.modelRoles`:\n\n- `default`: Primary interaction model.\n- `smol`: Fast, lightweight model for metadata tasks and short tool evaluations.\n- `slow`: High-reasoning model for complex multi-step planning and analysis.\n- `plan`: Model assigned to interactive planning modes.\n- `commit`: Model assigned to git commit generation.\n\nEach role definition can include thinking intensity suffixes (for example, `pi/smol:minimal`, `claude-sonnet-4:high`).\n\n## Context promotion and fallback\n\nWhen a model context window overflows during a conversation turn (`context_length_exceeded`), xcsh automatically promotes the session to a larger-context sibling model before initiating context compaction:\n\n1. Evaluates explicit `contextPromotionTarget` configurations.\n2. Identifies the smallest available model with a larger context window on the same provider.\n3. Switches the session model temporarily and retries the turn.\n",
147
- "en/providers/openai-api-access.md": "---\ntitle: OpenAI access\ndescription: Choose ChatGPT subscription or usage-based OpenAI Platform access in xcsh.\nsidebar:\n order: 4\n label: OpenAI access\n---\n\nxcsh supports two distinct access methods for OpenAI services, depending on whether you authenticate with a ChatGPT subscription or an OpenAI Platform API key.\n\n## 1. ChatGPT subscription (OAuth)\n\nTo use your ChatGPT Plus or Pro subscription, run `/login openai-codex` or select **ChatGPT Plus/Pro (Codex Subscription)** in the `/login` interactive picker. xcsh stores the OAuth credentials securely in `agent.db` and discovers advertised subscription models.\n\n### Headless and SSH remote login\n\nWhen running inside an SSH session or headless terminal, xcsh automatically initiates device-code authentication:\n\n1. xcsh displays `https://auth.openai.com/codex/device` and a one-time verification code.\n2. Open the URL in any web browser and log in with your ChatGPT account credentials.\n3. Enter the one-time verification code.\n4. The remote xcsh process detects authorization automatically and completes authentication.\n\n> [!NOTE]\n> Device-code authentication requires enabling the beta feature in ChatGPT under **Settings → Security**, or via workspace administrator settings under **Workspace settings → Permissions & roles**.\n\n### Local desktop browser callback\n\nOn local workstations with graphical desktop environments, run `/login openai-codex-browser` (or select **ChatGPT Plus/Pro (Browser callback)**). This workflow opens your default browser and receives tokens through a local loopback listener on port 1455 (`http://localhost:1455/auth/callback`).\n\n## 2. OpenAI Platform API (usage-based billing)\n\nTo use pay-as-you-go OpenAI Platform access, configure the `OPENAI_API_KEY` environment variable or provide it in `models.yml`:\n\n```bash\nexport OPENAI_API_KEY=\"<OPENAI_API_KEY>\"\nxcsh\n```\n\nAfter starting xcsh, select an OpenAI model using `/model`.\n",
146
+ "en/providers/models.md": "---\ntitle: Model and Provider Configuration\ndescription: Model registry and provider configuration via models.yml with routing, fallback, and pricing.\nsidebar:\n order: 1\n label: Models & providers\n---\n\nThis document describes how xcsh loads model registries, applies configuration overrides, resolves API credentials, and manages model selection at runtime.\n\n## Core implementation architecture\n\nModel configuration and runtime resolution are implemented across the following modules:\n\n- `src/config/model-registry.ts`: Loads built-in and custom models, manages provider overrides, discovers local models, and integrates authentication.\n- `src/config/model-resolver.ts`: Parses model patterns, handles canonical coalescing, and selects default, small, and reasoning models.\n- `src/config/settings-schema.ts`: Defines settings schemas for `modelRoles` and transport options.\n- `src/session/auth-storage.ts`: Resolves API keys and OAuth tokens across configuration sources.\n- `packages/ai/src/models.ts` and `packages/ai/src/types.ts`: Defines built-in models and compatibility contracts.\n\n## Configuration file path\n\nDefault configuration path:\n\n- `~/.xcsh/agent/models.yml`\n\n> [!NOTE]\n> If `models.yml` is missing and a legacy `models.json` file exists in the configuration directory, xcsh automatically migrates the settings to `models.yml`.\n\n## Schema structure\n\n```yaml\nconfigVersion: 1\nproviders:\n <PROVIDER_ID>:\n baseUrl: https://api.example.com/v1\n apiKey: MY_API_KEY_ENV_VAR\n api: openai-completions\n headers:\n X-Custom-Header: value\n auth: apiKey\n discovery:\n type: ollama\n modelOverrides:\n <MODEL_ID>:\n name: Custom Model Display Name\n models:\n - id: custom-model-id\n name: Custom Model Name\n api: openai-completions\n contextWindow: 128000\n maxTokens: 16384\n cost:\n input: 0\n output: 0\n cacheRead: 0\n cacheWrite: 0\nequivalence:\n overrides:\n <PROVIDER_ID>/<MODEL_ID>: <CANONICAL_MODEL_ID>\n exclude:\n - <PROVIDER_ID>/<MODEL_ID>\n```\n\n### Supported API protocol types\n\n- `openai-completions`\n- `openai-responses`\n- `openai-codex-responses`\n- `azure-openai-responses`\n- `anthropic-messages`\n- `google-generative-ai`\n- `google-vertex`\n\n## Validation rules\n\n### Custom provider definitions (`models` defined)\n\nThe following fields are required:\n\n- `baseUrl`: Base endpoint URL.\n- `apiKey`: Required unless `auth: none` is explicitly configured.\n- `api`: Protocol type specified at the provider or model level.\n\n### Override-only providers (`models` omitted)\n\nMust specify at least one of:\n\n- `baseUrl`\n- `modelOverrides`\n- `discovery`\n\n## Merge and override hierarchy\n\nWhen initializing or refreshing the model registry, xcsh applies configuration in the following order:\n\n1. **Built-in catalog**: Loads default providers and models from `@f5-sales-demo/pi-ai`.\n2. **Custom configuration**: Parses `~/.xcsh/agent/models.yml`.\n3. **Provider overrides**: Applies custom `baseUrl` and default `headers` to built-in models.\n4. **Model overrides**: Merges custom settings from `modelOverrides`.\n5. **Custom models**: Appends or replaces model definitions matching existing `provider/id` pairs.\n6. **Runtime discovery**: Queries active local endpoints (such as Ollama or LM Studio) and registers discovered models.\n\n## Canonical model equivalence and coalescing\n\nxcsh groups equivalent model checkpoints from different providers under canonical upstream identifiers (for example, `claude-sonnet-4`, `gpt-5.3-codex`):\n\n- **Overrides**: Maps specific provider identifiers to standard canonical names via `equivalence.overrides`.\n- **Exclusions**: Removes specific variants from automatic grouping via `equivalence.exclude`.\n- **Resolution priority**: Selects concrete providers based on credential availability and `modelProviderOrder` precedence.\n\n## Runtime model discovery\n\nxcsh detects local inference runtimes automatically:\n\n- **Ollama**: Probes `http://127.0.0.1:11434/api/tags` (or `OLLAMA_BASE_URL`).\n- **LM Studio**: Probes `http://127.0.0.1:1234/v1/models` (or `LM_STUDIO_BASE_URL`).\n- **llama.cpp**: Probes `http://127.0.0.1:8080/v1/models` (or `LLAMA_CPP_BASE_URL`).\n\n## Credential resolution hierarchy\n\nWhen resolving API keys for a provider, xcsh evaluates sources in the following priority order:\n\n1. CLI flag `--api-key`\n2. Stored API keys in `agent.db`\n3. Stored OAuth tokens in `agent.db`\n4. Standard environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`)\n5. Provider `apiKey` field in `models.yml` (evaluated first as an environment variable name, then as a literal token)\n\n## Corporate Google Cloud Vertex AI login\n\n`/login` lists **Google Cloud Vertex AI (Corporate)** first. This route uses Application Default Credentials (ADC) from `gcloud`, validates the selected Google Cloud project and Gemini 3.7 Flash access in `global`, then selects `google-vertex/gemini-3.7-flash:high`.\n\nThe confirmed project and `global` location are stored as non-secret xcsh provider preferences and injected into Vertex requests. The route never falls back to a Gemini API key, consumer Gemini login, or Antigravity OAuth.\n\nIf ADC is missing, the wizard offers `gcloud auth application-default login` (with `--no-launch-browser` in headless terminals). Enable the service with `gcloud services enable aiplatform.googleapis.com --project PROJECT_ID`; billing, IAM, and model-access errors are reported before model state is changed.\n\n**Google Antigravity Enterprise (Advanced OAuth)** remains a separate route. It uses its own entitlement-discovery OAuth flow and does not assert that its discovered model is Vertex Gemini 3.7. Consumer and free-tier Google routes remain optional, but are never selected automatically.\n\n## Model roles and selection\n\nxcsh supports logical role aliases configured via `settings.modelRoles`:\n\n- `default`: Primary interaction model.\n- `smol`: Fast, lightweight model for metadata tasks and short tool evaluations.\n- `slow`: High-reasoning model for complex multi-step planning and analysis.\n- `plan`: Model assigned to interactive planning modes.\n- `commit`: Model assigned to git commit generation.\n\nEach role definition can include thinking intensity suffixes (for example, `pi/smol:minimal`, `claude-sonnet-4:high`).\n\n## Context promotion and fallback\n\nWhen a model context window overflows during a conversation turn (`context_length_exceeded`), xcsh automatically promotes the session to a larger-context sibling model before initiating context compaction:\n\n1. Evaluates explicit `contextPromotionTarget` configurations.\n2. Identifies the smallest available model with a larger context window on the same provider.\n3. Switches the session model temporarily and retries the turn.\n",
147
+ "en/providers/openai-api-access.md": "---\ntitle: OpenAI access\ndescription: Choose ChatGPT subscription or usage-based OpenAI Platform access in xcsh.\nsidebar:\n order: 4\n label: OpenAI access\n---\n\nxcsh supports two distinct access methods for OpenAI services, depending on whether you authenticate with a ChatGPT subscription or an OpenAI Platform API key.\n\n## 1. ChatGPT subscription (OAuth)\n\nTo use your ChatGPT Plus or Pro subscription, run `/login openai-codex` or select **ChatGPT Plus/Pro (Codex Subscription)** in the `/login` interactive picker. xcsh stores the OAuth credentials securely in `agent.db` and discovers advertised subscription models.\n\n### Headless and SSH remote login\n\nWhen running inside an SSH session or headless terminal, xcsh automatically initiates device-code authentication:\n\n1. xcsh displays `https://auth.openai.com/codex/device` and a one-time verification code.\n2. Open the URL in any web browser and log in with your ChatGPT account credentials.\n3. Enter the one-time verification code.\n4. The remote xcsh process detects authorization automatically and completes authentication.\n\n> [!NOTE]\n> Device-code authentication requires enabling the beta feature in ChatGPT under **Settings → Security**, or via workspace administrator settings under **Workspace settings → Permissions & roles**.\n\n### Local desktop browser callback\n\nOn local workstations with graphical desktop environments, the same `/login openai-codex` command opens your default browser and receives tokens through a local loopback listener on port 1455 (`http://localhost:1455/auth/callback`).\n\nIf device authorization is unavailable in an SSH or headless session, the same login flow offers browser/manual redirect authentication. Open the displayed URL locally, then paste the redirect URL into `/login <redirect URL>` on the remote host.\n\n## 2. OpenAI Platform API (usage-based billing)\n\nTo use pay-as-you-go OpenAI Platform access, configure the `OPENAI_API_KEY` environment variable or provide it in `models.yml`:\n\n```bash\nexport OPENAI_API_KEY=\"<OPENAI_API_KEY>\"\nxcsh\n```\n\nAfter starting xcsh, select an OpenAI model using `/model`.\n",
148
148
  "en/providers/provider-streaming-internals.md": "---\ntitle: Provider Streaming Internals\ndescription: Provider streaming implementation with SSE parsing, token counting, and backpressure handling.\nsidebar:\n order: 2\n label: Streaming internals\n---\n\nThis document describes how token and tool call streams from diverse LLM providers are normalized in `@f5-sales-demo/pi-ai` and propagated through `@f5-sales-demo/pi-agent-core` to `coding-agent` session events.\n\n## End-to-end streaming architecture\n\n1. **Stream dispatch**: `streamSimple()` in `packages/ai/src/stream.ts` maps provider-agnostic request options and dispatches them to the selected provider driver.\n2. **Provider normalization**: Provider stream drivers (`anthropic.ts`, `openai-responses.ts`, `google.ts`) translate vendor-specific Server-Sent Events (SSE) into a unified `AssistantMessageEvent` stream.\n3. **Event throttling**: `AssistantMessageEventStream` (`packages/ai/src/utils/event-stream.ts`) buffers and coalesces rapid delta events (~50ms cadence) to smooth UI rendering.\n4. **Agent loop consumption**: `agentLoop` (`packages/agent/src/agent-loop.ts`) processes events, updates in-flight message state, and emits `message_update` events.\n5. **Session event integration**: `AgentSession` (`packages/coding-agent/src/session/agent-session.ts`) handles user aborts, automated retries, context compaction, and tool call execution guards.\n\n## Unified stream contract (`AssistantMessageEvent`)\n\nAll provider drivers emit events conforming to the `AssistantMessageEvent` union:\n\n- `start`: Initiates stream processing.\n- Content block lifecycle triplets:\n - Text: `text_start` → `text_delta`* → `text_end`\n - Thinking / reasoning: `thinking_start` → `thinking_delta`* → `thinking_end`\n - Tool invocation: `toolcall_start` → `toolcall_delta`* → `toolcall_end`\n- Terminal events:\n - `done`: Emits termination reasons (`stop`, `length`, `toolUse`).\n - `error`: Emits failure reasons (`aborted`, `error`).\n\n## Provider-specific normalization logic\n\n### Anthropic (`anthropic-messages`)\n\n- Maps `message_start` to token usage metadata.\n- Maps `content_block_start` and `content_block_delta` to unified text, thinking, and tool invocation blocks.\n- Accumulates streamed JSON fragments in `partialJson` and reparses arguments incrementally via `parseStreamingJson()`.\n\n### OpenAI Responses (`openai-responses`)\n\n- Maps `response.output_item.added` to text or reasoning blocks.\n- Maps `response.reasoning_summary_text.delta` to `thinking_delta`.\n- Translates `response.function_call_arguments.delta` into `toolcall_delta`.\n- Normalizes tool call identifiers into `<CALL_ID>|<ITEM_ID>`.\n\n### Google Generative AI (`google-generative-ai`)\n\n- Parses `candidate.content.parts`, distinguishing thinking blocks via `isThinkingPart()`.\n- Emits synthetic `toolcall_delta` containing serialized JSON strings for structured tool call events.\n\n## Tool call JSON accumulation and error recovery\n\nIncremental tool arguments are parsed via `parseStreamingJson()` (`packages/ai/src/utils/json-parse.ts`):\n\n1. Attempts standard `JSON.parse`.\n2. Falls back to partial JSON parsing to evaluate incomplete streamed fragments.\n3. If partial parsing fails, returns `{}` temporarily until subsequent deltas provide valid JSON structures.\n4. Performs a final parse pass on `toolcall_end`.\n\n## Cancellation and lifecycle boundaries\n\n- **Provider HTTP request**: `options.signal` aborts the active HTTP transport connection.\n- **Agent loop**: Evaluates `signal.aborted` prior to processing each streamed event.\n- **Session abortion**: Calling `AgentSession.abort()` propagates cancellations to active tool subprocesses.\n- **Tool execution interrupts**: Tool runners listen to `AbortSignal.any([agentSignal, steeringAbortSignal])`, allowing users to interrupt long-running tools without discarding prior turns.\n\n## Related implementation files\n\n- `packages/ai/src/stream.ts`: Provider stream dispatcher and option normalizer.\n- `packages/ai/src/utils/event-stream.ts`: Stream queueing and delta event throttling.\n- `packages/ai/src/utils/json-parse.ts`: Incremental streaming JSON parser.\n- `packages/ai/src/providers/anthropic.ts`: Anthropic SSE event transformer.\n- `packages/ai/src/providers/openai-responses.ts`: OpenAI Responses event transformer.\n- `packages/ai/src/providers/google.ts`: Google Gemini event transformer.\n- `packages/agent/src/agent-loop.ts`: Agent event processing loop.\n- `packages/coding-agent/src/session/agent-session.ts`: Session lifecycle, retry policies, and persistence.\n",
149
149
  "en/providers/python-repl.md": "---\ntitle: Python Tool and IPython Runtime\ndescription: Python REPL tool runtime with IPython kernel management, execution, and output capture.\nsidebar:\n order: 3\n label: Python & IPython\n---\n\nThis document describes the Python execution architecture in `packages/coding-agent`, covering tool parameters, Jupyter Kernel Gateway lifecycles, environment isolation, execution semantics, and troubleshooting procedures.\n\n## Architecture overview\n\nPython execution is managed across several core modules:\n\n- `src/tools/python.ts`: Tool interface definition and interactive cell renderers.\n- `src/ipy/executor.ts`: Session-level kernel orchestration and execution scheduling.\n- `src/ipy/kernel.ts`: Kernel lifecycle management and WebSocket communication protocol.\n- `src/ipy/gateway-coordinator.ts`: Shared local Jupyter Kernel Gateway process coordinator.\n- `src/ipy/runtime.ts`: Python environment discovery, virtualenv resolution, and environment variable filtering.\n\n## Tool parameter schema\n\n```typescript\ninterface PythonToolParams {\n cells: Array<{\n code: string;\n title?: string;\n }>;\n timeout?: number; // Execution timeout in seconds (1–600, default: 30)\n cwd?: string; // Working directory for execution\n reset?: boolean; // Reset kernel state prior to executing first cell\n}\n```\n\nThe tool executes with `concurrency: \"exclusive\"`, ensuring sequential execution per session.\n\n## Gateway lifecycle management\n\n### Gateway operational modes\n\n1. **Local shared gateway (default)**:\n - Coordinates a single shared process under `~/.xcsh/agent/python-gateway/`.\n - Synchronizes access via `gateway.lock` and tracks state in `gateway.json`.\n - Starts `python -m kernel_gateway` bound to an ephemeral port on `127.0.0.1`.\n2. **External gateway**:\n - Configured via `PI_PYTHON_GATEWAY_URL`.\n - Authenticates requests with `PI_PYTHON_GATEWAY_TOKEN` when required.\n - Bypasses local process management.\n\n### Kernel lifecycle\n\n1. **Creation**: Allocates a new kernel session via `POST /api/kernels`.\n2. **Connection**: Establishes a WebSocket channel (`/api/kernels/:id/channels`).\n3. **Initialization**: Configures `cwd`, applies sanitized environment variables, and executes runtime preludes.\n4. **Module loading**: Imports custom modules from `~/.xcsh/agent/modules/*.py` and project-specific `<CWD>/.xcsh/modules/*.py`.\n5. **Termination**: Issues `DELETE /api/kernels/:id` and closes WebSocket connections upon session cleanup.\n\n## Environment isolation and security\n\nBefore launching Python runtimes, xcsh filters environment variables:\n\n- **Retained variables**: Core system variables (`PATH`, `HOME`, `VIRTUAL_ENV`, `PYTHONPATH`) and whitelisted prefixes (`LC_*`, `XDG_*`, `PI_*`).\n- **Sanitized variables**: Strips sensitive LLM API keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`) to prevent accidental leaks.\n\n### Python environment resolution order\n\n1. Active virtual environment (`VIRTUAL_ENV` or `<CWD>/.venv`)\n2. Managed xcsh virtual environment (`~/.xcsh/python-env`)\n3. System `python3` or `python` on `PATH`\n\n## Output capture and MIME rendering\n\nThe runtime captures structured output across multiple MIME types:\n\n- `text/markdown`: Rendered directly in interactive output panes.\n- `text/plain`: Standard console output and tracebacks.\n- `application/json`: Formatted JSON inspector trees.\n- `image/png`: Inline image payloads.\n- `application/x-xcsh-status`: Structured progress and execution status events.\n\n## Troubleshooting\n\n### Python tool unavailable\n\n- Verify that `jupyter_kernel_gateway` and `ipykernel` are installed in the resolved Python environment:\n\n ```bash\n python -m pip install jupyter_kernel_gateway ipykernel\n ```\n\n- Verify that `python.toolMode` or `PI_PY` is not set to `bash-only`.\n\n### Execution timeouts or hangs\n\n- Python does not support interactive standard input (`input()`). Avoid invoking interactive prompts.\n- To handle long-running workloads, increase the `timeout` parameter (up to 600 seconds).\n",
150
150
  "en/runtime-tools/bash-tool-runtime.md": "---\ntitle: Bash Tool Runtime\ndescription: Bash tool runtime with shell process management, sandboxing, timeout, and output streaming.\nsidebar:\n order: 1\n label: Bash tool\n---\n\nThis document describes the execution pipeline of the `bash` tool in `packages/coding-agent`, covering command normalization, interception rules, process sandboxing, output truncation, and UI rendering across execution modes.\n\n## Execution entry points\n\nxcsh provides two distinct shell execution interfaces:\n\n1. **Agent tool interface (`bash`)**: Invoked by LLMs during conversation turns. Supports command normalization, security interception, PTY emulation, and structured output formatting.\n - Entry point: `BashTool.execute()`\n2. **User shell execution (`!cmd`)**: Direct shell execution triggered by user input in the TUI or RPC mode.\n - Entry point: `AgentSession.executeBash()`\n\nBoth execution paths utilize the underlying `executeBash()` engine in `src/exec/bash-executor.ts` for non-interactive execution.\n\n## Execution pipeline\n\n### 1. Command normalization and argument parsing\n\nWhen a tool call occurs, `BashTool.execute()` normalizes command strings via `normalizeBashCommand()`:\n\n- Extracts trailing pipe limits (`| head -n N`, `| tail -n N`) into structured pagination parameters.\n- Trims outer whitespace while preserving internal arguments and heredocs.\n- Merges extracted limits with explicit `head` or `tail` parameters (explicit arguments take precedence).\n\n### 2. Command interception and rule enforcement\n\nIf `bashInterceptor.enabled` is active, the tool checks the command against configured regex rules before spawning processes:\n\n- **Rule evaluation**: Blocks commands when patterns match (e.g., using `cat` or `grep` when specialized tools exist) and the suggested alternative tool is present in active context (`ctx.toolNames`).\n- **Rejection behavior**: Raises a `ToolError` containing the blocking reason and guidance toward preferred tools (`view_file`, `grep_search`, `list_dir`, `replace_file_content`).\n\n### 3. Working directory validation and timeouts\n\n- Resolves working directories relative to session root (`resolveToCwd`).\n- Validates that directory paths exist and are accessible directories prior to execution.\n- Clamps timeout durations to the range of 1 to 3600 seconds (default: 30 seconds).\n\n### 4. Interactive PTY vs. non-interactive execution\n\nxcsh chooses PTY execution (`runInteractiveBashPty`) when all of the following conditions are met:\n\n- `bash.virtualTerminal` is configured to `on`.\n- `PI_NO_PTY` is not set to `1`.\n- The session has an active graphical/TUI terminal context (`ctx.hasUI === true`).\n\nIn headless, print, or RPC modes, xcsh always uses non-interactive execution.\n\n## Output streaming, truncation, and artifact spill\n\nOutput is processed through `OutputSink` in `src/session/streaming-output.ts`:\n\n- **Memory buffer**: Maintains a UTF-8-safe tail buffer (default: 50 KB).\n- **Artifact spillover**: When output exceeds the buffer threshold, xcsh writes the full stream to disk in artifact storage (`artifact://<ID>`).\n- **Truncation notice**: Injects truncation summaries into tool results, including total byte/line counts and artifact links for full-output retrieval.\n\n## Filesystem sandboxing and containment\n\nxcsh enforces filesystem containment boundaries using platform-native security primitives:\n\n| Platform | Kernel version | Security backend | Boundary enforcement |\n| -------- | -------------- | ---------------- | -------------------- |\n| macOS | Any | `seatbelt` | OS-enforced kernel sandbox |\n| Linux (modern) | Kernel ≥ 6.1 (Debian 12, Ubuntu 24.04, Fedora) | `landlock` (ABI ≥ 2) | OS-enforced kernel sandbox |\n| Linux (legacy) | Kernel < 6.1 (RHEL 9, Ubuntu 22.04 GA) | `scanner-only` | Command-text static analysis |\n\n> [!IMPORTANT]\n> On legacy Linux kernels with Landlock ABI 1 (kernel < 6.1), xcsh operates in `scanner-only` mode because ABI 1 lacks `LANDLOCK_ACCESS_FS_REFER` (which prevents `git` and `mv` operations). For full OS-enforced isolation, deploy on Linux kernels 6.1 or newer.\n\n## Related implementation files\n\n- `src/tools/bash.ts`: Tool definition, normalization, and rendering logic.\n- `src/tools/bash-normalize.ts`: Command string normalization and line limit extraction.\n- `src/tools/bash-interceptor.ts`: Pattern matching rules for command redirection.\n- `src/exec/bash-executor.ts`: Process execution engine and shell session reuse.\n- `src/tools/bash-interactive.ts`: Virtual PTY runtime and terminal input handling.\n- `src/session/streaming-output.ts`: `OutputSink` buffer management and artifact spillover.\n",
@@ -611,7 +611,7 @@ export const TERRAFORM_INDEX: TerraformIndex = {
611
611
  name: "service_policy_rule",
612
612
  category: "security",
613
613
  description: "Service_policy_rule creates a new object in the storage backend for metadata.namespace",
614
- required: ["name", "namespace"],
614
+ required: ["name", "namespace", "action"],
615
615
  minimal_config: {
616
616
  format: "terraform",
617
617
  source: "_llms-txt/resources/service_policy_rule.txt#minimal-valid-config",
@@ -67,6 +67,29 @@ interface ScopedModelItem {
67
67
  thinkingLevel?: string;
68
68
  }
69
69
 
70
+ export interface DefaultPickerModelPresentation {
71
+ model: Model;
72
+ displaySelector: string;
73
+ selector: string;
74
+ }
75
+
76
+ const OPENAI_CODEX_GPT56_TIERS = new Set(["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"]);
77
+
78
+ /** Collapse subscription tiers only in the ordinary picker; explicit --models scopes keep raw access. */
79
+ export function presentModelsForDefaultPicker(
80
+ models: readonly Model[],
81
+ explicitlyScoped = false,
82
+ ): DefaultPickerModelPresentation[] {
83
+ return models.flatMap(model => {
84
+ const selector = `${model.provider}/${model.id}`;
85
+ if (explicitlyScoped || model.provider !== "openai-codex" || !OPENAI_CODEX_GPT56_TIERS.has(model.id)) {
86
+ return [{ model, displaySelector: selector, selector }];
87
+ }
88
+ if (model.id !== "gpt-5.6-sol") return [];
89
+ return [{ model, displaySelector: "openai-codex/gpt-5.6", selector }];
90
+ });
91
+ }
92
+
70
93
  interface RoleAssignment {
71
94
  model: Model;
72
95
  thinkingLevel: ThinkingLevel;
@@ -359,12 +382,15 @@ export class ModelSelectorComponent extends Container {
359
382
 
360
383
  // Use scoped models if provided via --models flag
361
384
  if (this.#scopedModels.length > 0) {
362
- models = this.#scopedModels.map(scoped => ({
385
+ models = presentModelsForDefaultPicker(
386
+ this.#scopedModels.map(scoped => scoped.model),
387
+ true,
388
+ ).map(item => ({
363
389
  kind: "provider",
364
- provider: scoped.model.provider,
365
- id: scoped.model.id,
366
- model: scoped.model,
367
- selector: `${scoped.model.provider}/${scoped.model.id}`,
390
+ provider: item.model.provider,
391
+ id: item.displaySelector.slice(item.displaySelector.indexOf("/") + 1),
392
+ model: item.model,
393
+ selector: item.selector,
368
394
  }));
369
395
  } else {
370
396
  // Reload config and cached discovery state without blocking on live provider refresh
@@ -381,12 +407,12 @@ export class ModelSelectorComponent extends Container {
381
407
  // Load available models (built-in models still work even if models.json failed)
382
408
  try {
383
409
  const availableModels = this.#modelRegistry.getAvailable();
384
- models = availableModels.map((model: Model) => ({
410
+ models = presentModelsForDefaultPicker(availableModels).map(item => ({
385
411
  kind: "provider",
386
- provider: model.provider,
387
- id: model.id,
388
- model,
389
- selector: `${model.provider}/${model.id}`,
412
+ provider: item.model.provider,
413
+ id: item.displaySelector.slice(item.displaySelector.indexOf("/") + 1),
414
+ model: item.model,
415
+ selector: item.selector,
390
416
  }));
391
417
  } catch (error) {
392
418
  this.#allModels = [];
@@ -428,7 +454,28 @@ export class ModelSelectorComponent extends Container {
428
454
  compactSearchText: compactSearchText(searchText),
429
455
  };
430
456
  })
431
- .filter((item): item is CanonicalModelItem => item !== undefined);
457
+ .filter((item): item is CanonicalModelItem => item !== undefined)
458
+ .filter(
459
+ item =>
460
+ this.#scopedModels.length > 0 ||
461
+ item.model.provider !== "openai-codex" ||
462
+ !OPENAI_CODEX_GPT56_TIERS.has(item.model.id),
463
+ );
464
+ if (this.#scopedModels.length === 0) {
465
+ const friendly = models.find(item => item.provider === "openai-codex" && item.id === "gpt-5.6");
466
+ if (friendly) {
467
+ canonicalModels.push({
468
+ kind: "canonical",
469
+ id: "openai-codex/gpt-5.6",
470
+ model: friendly.model,
471
+ selector: friendly.selector,
472
+ variantCount: 1,
473
+ searchText: "openai-codex/gpt-5.6 GPT-5.6 Sol",
474
+ normalizedSearchText: normalizeSearchText("openai-codex/gpt-5.6 GPT-5.6 Sol"),
475
+ compactSearchText: compactSearchText("openai-codex/gpt-5.6 GPT-5.6 Sol"),
476
+ });
477
+ }
478
+ }
432
479
 
433
480
  this.#sortModels(models);
434
481
  this.#sortCanonicalModels(canonicalModels);
@@ -1,8 +1,9 @@
1
- import { getOAuthProviders, type OAuthProviderInfo } from "@f5-sales-demo/pi-ai";
1
+ import type { OAuthProviderInfo } from "@f5-sales-demo/pi-ai";
2
2
  import { Container, Input, matchesKey, Spacer, Text, TruncatedText } from "@f5-sales-demo/pi-tui";
3
3
  import { theme } from "../../modes/theme/theme";
4
4
  import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
5
5
  import type { AuthStorage } from "../../session/auth-storage";
6
+ import { getLoginOptions } from "../controllers/login-options";
6
7
  import { DynamicBorder } from "./dynamic-border";
7
8
  /**
8
9
  * Component that renders an OAuth provider selector.
@@ -71,7 +72,15 @@ export class OAuthSelectorComponent extends Container {
71
72
  this.#stopSpinner();
72
73
  }
73
74
  #loadProviders(): void {
74
- this.#allProviders = getOAuthProviders().filter(provider => this.#mode === "login" || !provider.loginOnly);
75
+ this.#allProviders =
76
+ this.#mode === "login"
77
+ ? getLoginOptions()
78
+ : getLoginOptions().filter(provider => !provider.loginOnly && provider.id !== "google-vertex");
79
+ // Keep the curated registry order stable except for explicit priorities.
80
+ this.#allProviders = this.#allProviders
81
+ .map((provider, index) => ({ provider, index }))
82
+ .sort((a, b) => (a.provider.loginOrder ?? 0) - (b.provider.loginOrder ?? 0) || a.index - b.index)
83
+ .map(({ provider }) => provider);
75
84
  this.#filteredProviders = this.#allProviders;
76
85
  }
77
86
 
@@ -188,6 +197,9 @@ export class OAuthSelectorComponent extends Container {
188
197
  line = text + statusIndicator;
189
198
  }
190
199
  this.#listContainer.addChild(new TruncatedText(line, 0, 0));
200
+ if (provider.description) {
201
+ this.#listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${provider.description}`), 0, 0));
202
+ }
191
203
  }
192
204
 
193
205
  // Show "no providers" if empty
@@ -1,5 +1,6 @@
1
1
  import { ThinkingLevel } from "@f5-sales-demo/pi-agent-core";
2
2
  import { canonicalizeOAuthProviderId, type Model } from "@f5-sales-demo/pi-ai";
3
+ import type { Settings } from "../../config/settings";
3
4
  import type { VllmDiscoveredModel } from "../../config/vllm-config";
4
5
  import { applySubscriptionProfileRoles, type SubscriptionProfileId } from "../../routing/subscription-profiles";
5
6
 
@@ -41,11 +42,19 @@ export const GOOGLE_ANTIGRAVITY_LOGIN_MODEL_CHOICE: LoginModelChoice = {
41
42
  thinkingLevel: ThinkingLevel.High,
42
43
  };
43
44
 
45
+ export const GOOGLE_VERTEX_LOGIN_MODEL_CHOICE: LoginModelChoice = {
46
+ label: "Gemini 3.7 Flash High",
47
+ description: "Corporate Vertex AI model with high reasoning",
48
+ provider: "google-vertex",
49
+ modelId: "gemini-3.7-flash",
50
+ thinkingLevel: ThinkingLevel.High,
51
+ };
52
+
44
53
  export const OPENAI_CODEX_LOGIN_MODEL_CHOICE: LoginModelChoice = {
45
- label: "GPT-5.6 Terra",
46
- description: "Balanced OpenAI Codex subscription model with medium reasoning",
54
+ label: "GPT-5.6",
55
+ description: "OpenAI Codex subscription model with medium reasoning",
47
56
  provider: "openai-codex",
48
- modelId: "gpt-5.6-terra",
57
+ modelId: "gpt-5.6-sol",
49
58
  thinkingLevel: ThinkingLevel.Medium,
50
59
  };
51
60
 
@@ -83,12 +92,7 @@ interface ModelApplicableSession extends BaseModelApplicableSession {
83
92
  getAll(): Model[];
84
93
  getProviderDiscoveryState?(provider: string): { status: string; stale: boolean } | undefined;
85
94
  };
86
- settings?: {
87
- getModelRoles(): Readonly<Record<string, string | undefined>>;
88
- get?(key: "routing.profile"): "none" | SubscriptionProfileId;
89
- set(key: "modelRoles", value: Record<string, string>): void;
90
- set(key: "routing.profile", value: "none" | SubscriptionProfileId): void;
91
- };
95
+ settings?: Pick<Settings, "getModelRoles" | "get" | "set">;
92
96
  }
93
97
 
94
98
  /**
@@ -138,7 +142,12 @@ export async function applyOAuthLoginModel(
138
142
  }
139
143
 
140
144
  const settings = session.settings;
141
- const previousProfile = settings?.get?.("routing.profile") ?? "none";
145
+ const storedProfile = settings?.get("routing.profile");
146
+ const previousProfile: "none" | SubscriptionProfileId =
147
+ storedProfile === "google-antigravity" || storedProfile === "openai-codex" ? storedProfile : "none";
148
+ const storedRoutingMode = settings?.get("routing.mode");
149
+ const previousRoutingMode =
150
+ storedRoutingMode === "shadow" || storedRoutingMode === "auto" ? storedRoutingMode : "off";
142
151
  const previousRoles = settings
143
152
  ? Object.fromEntries(
144
153
  Object.entries(settings.getModelRoles()).filter(
@@ -155,6 +164,7 @@ export async function applyOAuthLoginModel(
155
164
  );
156
165
  if (!profile.applied) return undefined;
157
166
  settings.set("modelRoles", profile.roles);
167
+ settings.set("routing.mode", "off");
158
168
  settings.set("routing.profile", canonicalProvider as SubscriptionProfileId);
159
169
  }
160
170
 
@@ -162,12 +172,14 @@ export async function applyOAuthLoginModel(
162
172
  const applied = await applyModelAfterLogin(session, choice);
163
173
  if (!applied && settings) {
164
174
  settings.set("modelRoles", previousRoles);
175
+ settings.set("routing.mode", previousRoutingMode);
165
176
  settings.set("routing.profile", previousProfile);
166
177
  }
167
178
  return applied ? choice : undefined;
168
179
  } catch (error) {
169
180
  if (settings) {
170
181
  settings.set("modelRoles", previousRoles);
182
+ settings.set("routing.mode", previousRoutingMode);
171
183
  settings.set("routing.profile", previousProfile);
172
184
  }
173
185
  throw error;