@gajae-code/coding-agent 0.16.0 → 0.16.3
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.
- package/CHANGELOG.md +52 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +156 -32
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +6 -1
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +497 -31
- package/src/sdk/host/session-runtime.ts +387 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +210 -94
- package/src/sdk/session.ts +59 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +158 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +4 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +86 -1
|
@@ -14,7 +14,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
14
14
|
"ai-schema-normalize.md": "# AI tool-schema normalization\n\n`@gajae-code/ai` exposes one unified schema normalizer that providers consume\nbefore tools are sent on the wire. All walkers live in\n`packages/ai/src/utils/schema/normalize.ts`; the operational contract is\n`packages/ai/src/utils/schema/CONSTRAINTS.md`.\n\nThere is no separate `strict-mode.ts` module any more — OpenAI strict-mode\nsanitization, OpenAI Responses `oneOf` rewriting, Google/Vertex/Gemini-CLI\nsanitization, Cloud Code Assist Anthropic sanitization, and MCP sanitization all\nshare the same option-driven walk.\n\n## Entry points\n\nAll exports live under `@gajae-code/ai/utils/schema`:\n\n- `normalizeSchema(value, options)` — generic option-driven walker.\n- `normalizeSchemaForGoogle(value)` — Gemini / Vertex / Gemini CLI.\n- `normalizeSchemaForCCA(value)` — Cloud Code Assist Anthropic (Antigravity + GCA).\n- `normalizeSchemaForMCP(value)` — MCP inputSchemas before they enter the\n custom-tool registry. `tool-bridge.ts` runs every MCP `inputSchema` through\n this dispatcher.\n- `normalizeSchemaForOpenAIResponses(schema)` (alias\n `sanitizeSchemaForOpenAIResponses`) — rewrites `oneOf` → `anyOf` for the\n Responses family.\n- `sanitizeSchemaForStrictMode(schema)` and\n `enforceStrictSchema(schema)` / `tryEnforceStrictSchema(schema)` — the\n OpenAI strict-mode pipeline (sanitize → enforce). All three are exported\n from `normalize.ts`.\n- `adaptSchemaForStrict(schema, strict)` from `./adapt` — thin composer that\n wraps `tryEnforceStrictSchema` for provider call sites and consults\n `GJC_NO_STRICT` (env `GJC_NO_STRICT`) for the global bypass.\n\nRemoved in the unified-flow refactor:\n\n- `strict-mode.ts` (merged into `normalize.ts`).\n- `sanitize-google.ts` and `normalize-cca.ts` (replaced by\n `normalizeSchemaFor*` dispatchers).\n- `StringEnum` helper — use `z.enum([...])` directly; Zod's emitted JSON\n Schema is already wire-compatible with Google and other providers.\n- `sanitizeSchemaFor{Google,CCA,MCP}` / `prepareSchemaForCCA` — renamed to\n `normalizeSchemaFor{Google,CCA,MCP}`.\n\n## Dispatcher mapping\n\n| Provider transport(s) | Dispatcher |\n| -------------------------------------------------------------------- | -------------------------------------------- |\n| `openai-completions`, `openai-responses`, `openai-code-responses` | `adaptSchemaForStrict` (sanitize + enforce) |\n| `openai-responses` family (`oneOf` → `anyOf` only) | `normalizeSchemaForOpenAIResponses` |\n| `google-generative-ai`, `google-vertex`, Gemini CLI | `normalizeSchemaForGoogle` |\n| Cloud Code Assist Anthropic (Antigravity + GCA, `anthropic-model-*` model ids) | `normalizeSchemaForCCA` |\n| MCP `inputSchema` ingestion | `normalizeSchemaForMCP` |\n| `anthropic-messages` (native, not CCA) | per-provider whitelist in `anthropic.ts` |\n\nGemini CLI / Antigravity CCA MUST run the full `normalizeSchemaForCCA`\npipeline (not just the first keyword-stripping pass) to keep parity with the\nshared Google Anthropic path.\n\n## Walk semantics\n\n`normalizeSchema` first upgrades the input to JSON Schema 2020-12, then\nwalks the tree with the option set pinned by the dispatcher. Each node:\n\n1. Inlines `$ref` (see \"Edge cases\" below).\n2. Renames `snake_case` combinator/property keys to camelCase\n (`any_of` → `anyOf`, etc.; collisions follow python-genai\n `pop(from)`/`set(to)` semantics — snake_case wins).\n3. Applies the `handle_null_fields` collapse for nullable unions before\n recursing into children.\n4. Strips keys the target provider does not support, optionally lifting\n human-meaningful keys (`pattern`, `format`, min/max, `default`,\n `examples`, ...) into the sibling `description` via the spill formatter\n (`spill.ts`). Structural/meta keys (`$ref`, `$defs`,\n `additionalProperties`) are not spilled.\n5. Normalizes type unions (`type: [\"T\", \"null\"]` → `type: \"T\"` + nullable\n marker on Google, plain `type: \"T\"` on CCA).\n6. Collapses object-only / same-type combiners, optionally lossy-collapses\n mixed-type combiners (CCA only), and runs the residual-combiner fixpoint.\n7. Validates against AJV 2020 when `validateAndFallback` is set (CCA path)\n and emits the per-tool fallback `{ \"type\": \"object\", \"properties\": {} }`\n on residual incompatibility — `type` array, `type: \"null\"`, `nullable`\n key, or any remaining `anyOf`/`oneOf`/`allOf`.\n\n## OpenAI strict-mode pipeline\n\n`adaptSchemaForStrict(schema, strict)` runs `tryEnforceStrictSchema`,\nwhich composes:\n\n1. **Sanitize** (`sanitizeSchemaForStrictMode`): strips non-structural\n keywords (`format`, `pattern`, min/max, `examples`, `default`,\n `if`/`then`/`else`, `not`, `unevaluated*`, `patternProperties`,\n `dependent*`, `content*`, `min/maxProperties`, `$dynamicRef`, etc.). The\n `default` value is inlined into the sibling `description` as\n ` (default: X)` before being dropped, unless `description` already\n contains `(default:` or no `description` exists.\n2. **Enforce** (`enforceStrictSchema`): every object node gets\n `additionalProperties: false`, every property goes into `required`, and\n optional properties become nullable unions\n (`anyOf: [<original>, { \"type\": \"null\" }]`). Tuple `prefixItems` are\n strictified recursively.\n\nThe two passes share node-level caches and the same epoch-based cycle\nguard, so a single walk on the wire path normalizes refs, allOf, and\nnullable wrapping consistently. `tryEnforceStrictSchema` is fail-open:\nif anything throws, it returns `{ strict: false, schema: original }` so\ncallers MUST emit `strict: true` only when enforcement actually succeeded.\n\n### Edge cases the strict-mode normalizer handles\n\n- **Local `$ref` inlining.** OpenAI strict mode rejects\n `{ \"$ref\": \"...\", \"description\": \"...\" }` with sibling keys. The\n sanitizer pre-resolves local `#/...` refs against the root and merges\n with **sibling keys winning** over the resolved def — same precedence\n as `openai-python`'s `_ensure_strict_json_schema`. Recursive refs are\n guarded by the per-walk epoch.\n- **Single-item `allOf`.** A `{ \"allOf\": [X], ...siblings }` collapses to\n `{ ...X, ...siblings }` with the inlined entry's keys winning over the\n original siblings (matches `openai-python`'s `_pydantic.py:79-83`). Multi-\n item `allOf` is left intact for the downstream validator to reject if\n needed.\n- **Type-array branches and nullable unions.** When a node has\n `type: [\"T\", \"U\"]`, the sanitizer emits one variant schema per type,\n pruning type-specific keywords (e.g. `properties`/`required` only stay on\n the `object` variant, `items` only on the `array` variant). The shared\n `description` is **hoisted onto the `anyOf` wrapper** instead of being\n duplicated on every branch — so a strict nullable union becomes\n `{ anyOf: [T, { type: \"null\" }], description: \"...\" }`, not\n `anyOf: [{ ..., description }, { ..., description }]`.\n- **Enum/const without a `type`.** Both sanitize and enforce paths call\n `inferStrictPrimitiveTypeFromEnumOrConst` to infer the primitive `type`\n from `enum` / `const` values. Mixed-primitive enums (`[1, \"two\", null]`),\n enums containing objects/arrays, and non-primitive `const` values\n (`{a:1}`, `[1,2,3]`) cannot be described by a single `type` keyword and\n trigger the strict-mode fail-open path — emitting a typeless schema\n would just be rejected on the wire by OpenAI.\n\n## Performance: static fingerprint cache\n\n`resolveProviderModels` in `packages/ai/src/model-manager.ts` and\n`readModelCache`/`writeModelCache` in `model-cache.ts` cooperate via a\nschema-v3 `static_fingerprint` column on the `model_cache` SQLite table.\n\n- `fingerprintStatic(staticModels)` hashes the static catalog slice\n (`Bun.hash(JSON.stringify(models))` in base36) and memoizes the result\n in a per-process `WeakMap` keyed by the array reference. Multiple\n cold-start arms calling `resolveProviderModels` with the same\n `staticModels` array pay the JSON+hash cost once.\n- On cache read, if the network fetch is being skipped, the cached row is\n fresh + authoritative, and the cached `static_fingerprint` matches the\n current one, `resolveProviderModels` returns the cached models verbatim\n — the cache already incorporates the same static state, so re-running\n `mergeDynamicModels(static, cache)` would just rebuild the same objects.\n- `mergeModelSources` and `mergeDynamicModels` short-circuit on\n empty-source inputs (the common shape after `(static, [])` or for\n providers without a static catalog), avoiding Map churn entirely.\n\nCache rows written before schema v3 are dropped by the cache-version\ncheck; the column defaults to `''` for any row that survives a version\nupgrade so the fingerprint-equality check naturally fails closed and the\nfull merge re-runs.\n\n## Related\n\n- `docs/models.md` — registry, equivalence, compat flags\n (`supportsStrictMode`, `toolStrictMode`, `disableStrictTools`).\n- `docs/provider-streaming-internals.md` — how the normalized schemas are\n used downstream during the provider stream loop.\n- `packages/ai/src/utils/schema/CONSTRAINTS.md` — operational contract for\n every normalization rule.\n",
|
|
15
15
|
"alibaba-token-plan-pro-profile-benchmark.md": "# Alibaba Token Plan Pro profile benchmark\n\nThis note records the evidence used to add GJC's opt-in `alibaba-token-plan-pro` profile while preserving `alibaba-token-plan-balanced`. It combines provider documentation, upstream model cards, and small live GJC agent-loop probes. The measurements are descriptive, not statistically significant.\n\n## Decision summary\n\n| Role | Model and effort | Rationale |\n|---|---|---|\n| Default | `qwen3.8-max-preview:medium` | Native Responses transport and tool-loop compatibility |\n| Executor | `deepseek-v4-flash-0731:max` | Strongest official agent/coding results of the three candidates and clean live edit loop |\n| Planner | `glm-5.2:high` | 1M context and a distinct model family for planning |\n| Critic | `glm-5.2:xhigh` | Fastest correct defect-selection probe and cross-family review of DeepSeek output |\n| Architect | `qwen3.8-max-preview:xhigh` | Responses transport and 1M context for high-budget design work |\n\nThe Pro profile assigns three model families by role and raises only the high-value delegated budgets; it does not replace the provider's recommended Balanced profile.\n\n## Environment\n\n- Date: 2026-08-02\n- GJC: 0.12.7 installed binary\n- Provider: Alibaba Cloud Model Studio Token Plan Personal Edition, Singapore endpoint\n- Models: `qwen3.8-max-preview`, `deepseek-v4-flash-0731`, `glm-5.2`\n- Execution path: GJC CLI only; no direct provider batch script\n- Attempts: one per model and task\n- Coding fixture: the same Python Hamilton allocator implementation task, followed by five public and three hidden tests\n- Critic fixture: the same six-candidate defect-selection prompt\n\n## Live GJC observations\n\n| Probe | Qwen 3.8 Max Preview | DeepSeek V4 Flash 0731 | GLM 5.2 |\n|---|---:|---:|---:|\n| Exact-output completion | 3.015s total, 2.511s TTFT | 1.326s total, 0.872s TTFT | 1.314s total, 1.234s TTFT |\n| Read/edit allocator task | 47.901s, 6/6 tool calls, 8/8 tests | 46.614s, 6/6 tool calls, 8/8 tests | 42.063s, 7/8 initial tool calls, one recovery, 8/8 tests |\n| Defect selection | Correct, 26.280s | Correct, 20.059s | Correct, 17.180s |\n\nAll three models solved the bounded coding and critic fixtures. These runs therefore support role fit and transport viability, not a broad claim that one model is universally better.\n\nThree exploratory long-form critic runs reached an external 184-second benchmark-shell limit. That limit was not GJC's prompt deadline and was not a provider error, so those observations are not counted as model failures. GJC allows a substantially longer prompt window; high-budget delegated roles should not be downgraded solely from that shell cap.\n\n## External evidence\n\nDeepSeek's official V4 Flash 0731 model card reports the following agent evaluations against GLM 5.2:\n\n| Evaluation | DeepSeek V4 Flash 0731 | GLM 5.2 |\n|---|---:|---:|\n| DeepSWE | 54.4 | 46.2 |\n| Toolathlon-Verified | 70.3 | 59.9 |\n| Agents' Last Exam | 25.2 | 23.8 |\n\nThe card's best agent configuration uses `reasoning_effort=max`, which is why the executor binding exposes and selects `max` rather than a lower alias. GLM 5.2's official card reports a 1M context window and SWE-bench Pro 62.1; the live defect-selection probe supports using it as the independent critic family.\n\n## Transport and catalog contract\n\n- `qwen3.8-max-preview` uses `openai-responses`.\n- `deepseek-v4-flash-0731` and `glm-5.2` use `openai-completions`.\n- DeepSeek V4 Flash 0731 is bundled with a 1M context window, 384K output limit, and the discrete `low`, `high`, and `max` effort set.\n- Qwen 3.8 is a preview model. Its availability and behavior can change, so this assignment should be revisited if Alibaba replaces or retires the selector.\n\n## Reproduction shape\n\nUse normal GJC provider authentication, then select each model through GJC rather than calling the provider directly:\n\n```sh\ngjc --model alibaba-token-plan/qwen3.8-max-preview --thinking medium --no-tools -p \"<exact-output prompt>\"\ngjc --model alibaba-token-plan/deepseek-v4-flash-0731 --thinking max --tools read,edit -p \"<allocator fixture prompt>\"\ngjc --model alibaba-token-plan/glm-5.2 --thinking xhigh --no-tools -p \"<critic fixture prompt>\"\n```\n\nThe raw authenticated transcripts are intentionally not committed. They may contain local paths and account-scoped runtime metadata. The table above preserves the aggregate timing, tool-call, and test outcomes used for the profile decision.\n\n## Limitations\n\n- One attempt per model and task is not enough to estimate reliability or statistical significance.\n- The allocator and defect-selection probes do not directly measure long-horizon planning or architecture quality.\n- Token Plan credit consumption was not available in GJC telemetry, so this note does not compare per-role credit cost.\n- Preview selectors and provider-side model snapshots can change after publication.\n- The 184-second observations are censored by the benchmark shell and do not reveal eventual completion time.\n\n## Sources\n\n- [Alibaba Cloud Model Studio model list](https://www.alibabacloud.com/help/en/model-studio/models)\n- [Token Plan overview](https://www.alibabacloud.com/help/en/model-studio/token-plan-overview)\n- [Token Plan Personal Edition overview](https://www.alibabacloud.com/help/en/model-studio/token-plan-personal-overview)\n- [DeepSeek V4 Flash 0731 official model card](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731)\n- [GLM 5.2 official model card](https://huggingface.co/zai-org/GLM-5.2)\n- [OpenCode Qwen/DeepSeek comparison](https://opencode.ai/data/compare/alibaba/qwen3-8-max-preview/deepseek/deepseek-v4-flash)\n- [Artificial Analysis DeepSeek/GLM comparison](https://artificialanalysis.ai/models/comparisons/deepseek-v4-flash-vs-glm-5-2-non-reasoning) (different reasoning settings; directional only)\n",
|
|
16
16
|
"analyze-me-with-gjc.md": "# Analyze Me with GJC\n\nUse this prompt for meetup icebreakers where each participant asks GJC to introduce them from their own local GJC usage history.\n\nThe prompt is designed to be repeatable: it tells GJC what local artifacts to inspect, what patterns to extract, how to avoid leaking secrets, and how to turn the analysis into a short spoken self-introduction.\n\n## Full prompt\n\n```text\n~/.gjc 에 있는 내 가재코드 사용내역을 바탕으로, 가재코드 밋업 아이스브레이킹용 “가재코드가 보는 나” 자기소개 글을 작성해줘.\n\n목표:\n- 내 실제 가재코드 사용패턴을 분석해서, 내가 어떤 개발자/빌더/운영자인지 소개하는 글을 써줘.\n- 단순 통계 나열이 아니라, 사용 습관과 관심사에서 드러나는 성향을 해석해줘.\n- 밋업에서 2~4분 정도 읽을 수 있는 분량으로 작성해줘.\n- 너무 딱딱한 리포트 말고, 사람 소개글처럼 재미있고 선명하게 써줘.\n- 과장하거나 없는 사실을 만들지 말고, 실제 ~/.gjc 기록에서 관찰된 패턴만 근거로 삼아줘.\n\n분석 지시:\n1. ~/.gjc 디렉터리 구조를 먼저 확인해줘.\n2. 가능한 경우 아래의 안전한 메타데이터 중심 자료만 분석해줘:\n - ~/.gjc/agent/history.db 의 집계값\n - ~/.gjc/agent/sessions/**/*.jsonl 의 세션 메타데이터(세션 제목, timestamp, cwd, 메시지 수, tool-call 수, 파일 크기, subagent/task 이름)\n - ~/.gjc 내부의 추가 파일은 사용패턴 집계에 꼭 필요하고 민감정보가 없다고 판단되는 경우에만 읽어줘.\n - 기본적으로 ~/.gjc/logs/*, auth/config/credential 파일, env dump, raw tool-result body, raw prompt body, secret-like 값은 읽지 마.\n3. history.db에서는 최소한 다음을 봐줘:\n - 전체 프롬프트 수\n - 기간 범위\n - 작업 디렉터리 / 레포지토리 분포\n - 자주 등장하는 주제어\n - 짧은 명령과 긴 프롬프트의 비율\n - /skill:ultragoal, /skill:ralplan, /skill:deep-interview, /skill:autoresearch 사용 빈도\n - continue, fix, review, merge, PR, CI, test, verify, implement, delegate 같은 실행/검증 관련 단어 빈도\n4. sessions jsonl에서는 가능하면 다음을 봐줘:\n - 메인 세션 수\n - 서브에이전트 / task 세션 수\n - 짧은 세션과 긴 세션의 분포\n - 세션 title 또는 worktree 이름에서 보이는 관심사\n - 장기 실행, 병렬 위임, 검증, 리뷰, 릴리스 운영 흔적\n5. 레포지토리와 주제 다양성을 꼭 반영해줘:\n - 어떤 레포지토리/워크트리에서 많이 일했는지\n - GJC core, 개인 프로젝트, 연구/quant, infra, UI, automation, image/media 등 주제 범위가 보이면 묶어서 설명해줘.\n6. 민감정보는 절대 노출하지 마:\n - API key, 토큰, credential, 개인 연락처, 로컬 secret, private URL, 인증정보는 출력하지 마.\n - 프롬프트 예시는 필요할 때만 짧게 paraphrase해서 써줘.\n - 파일 경로나 레포 이름은 자기소개에 필요한 수준으로만 언급해줘.\n - 분석 중에도 민감정보를 모델 컨텍스트에 올리지 않도록 metadata-first / aggregate-only 방식으로 처리해줘.\n\n출력 형식:\n\n먼저 아주 짧게 “분석한 근거”를 3~6개 bullet로 요약해줘.\n예:\n- 분석 기간:\n- 프롬프트 수:\n- 주요 작업 공간:\n- 세션 패턴:\n- 자주 보인 workflow/skill:\n- 주요 관심사:\n\n그 다음 아래 제목으로 자기소개 글을 써줘:\n\n# 가재코드가 보는 나\n\n글 스타일:\n- 한국어로 작성.\n- 살짝 위트 있게.\n- “당신은 …” 또는 “나는 …” 중 더 자연스러운 쪽을 선택해도 됨.\n- 밋업에서 읽기 좋게 문단을 나눠줘.\n- 너무 아부하지 말고, 사용패턴에서 드러나는 장점과 특이한 습관을 솔직하게 말해줘.\n- 마지막에는 한 문장으로 요약해줘:\n “한 문장으로 말하면, 나는 ___ 하는 사람이다.”\n\n추가로 마지막에 선택사항으로 아래 3개를 붙여줘:\n\n## 10초 버전\n한두 문장짜리 초단기 자기소개.\n\n## 한 줄 별명\n사용패턴 기반 별명 3개.\n\n## 밋업용 오프닝 멘트\n처음 인사할 때 바로 읽을 수 있는 20~30초짜리 멘트.\n\n주의:\n- 분석 없이 일반론으로 쓰지 마.\n- 실제 ~/.gjc 기록을 읽고 나서 작성해.\n- 숫자를 말할 때는 실제로 확인한 숫자만 써.\n- 확인하지 못한 항목은 “확인 불가”라고 하지 말고, 그 항목을 빼고 자연스럽게 작성해.\n```\n\n## Short meetup prompt\n\nUse this when participants need a shorter copy/paste prompt.\n\n```text\n~/.gjc 사용내역을 분석해서 밋업 아이스브레이킹용 “가재코드가 보는 나” 자기소개 글을 써줘.\n\n반드시 실제 ~/.gjc 기록을 읽되, 안전한 메타데이터와 집계값 중심으로 근거 기반 작성해:\n- history.db의 프롬프트 수, 기간, cwd/레포 분포, 자주 쓰는 단어, skill 사용량\n- sessions jsonl의 세션 수, 세션 길이 다양성, subagent/task 사용 흔적\n- 레포지토리/주제 다양성\n- 짧은 명령 vs 긴 지시문 패턴\n- 실행/검증/리뷰/PR/CI/릴리스/위임 습관\n\n민감정보는 읽지도 출력하지도 마. API key, 토큰, private credential, 개인 secret, 긴 원문 프롬프트, raw tool-result body, ~/.gjc/logs/*, auth/config/env dump는 기본적으로 건너뛰고, 필요한 경우에도 안전한 집계값과 짧은 paraphrase만 써.\n\n출력:\n1. 분석 근거 bullet 3~6개\n2. 제목: “가재코드가 보는 나”\n3. 밋업에서 2~4분 읽을 수 있는 한국어 자기소개 글\n4. 마지막에:\n - 10초 버전\n - 사용패턴 기반 별명 3개\n - 20~30초 오프닝 멘트\n\n스타일:\n- 재미있고 선명하게\n- 과장 없이\n- 통계 나열보다 “이 사람이 어떤 식으로 일하는 사람인지” 해석 중심\n- 마지막 문장은 “한 문장으로 말하면, 나는 ___ 하는 사람이다.”\n```\n\n## Optional: GajaeTI prompt\n\nA meetup host can also turn the same analysis into a playful, MBTI-like “GajaeTI” result. This is only an icebreaker taxonomy, not a psychological assessment.\n\n```text\n~/.gjc 사용내역을 안전한 메타데이터와 집계값 중심으로 분석해서, 밋업 아이스브레이킹용 “가재TI”를 만들어줘.\n\n목표:\n- MBTI처럼 4글자 코드와 타입명을 만들되, 실제 성격검사가 아니라 가재코드 사용패턴 기반의 재미있는 작업 스타일 분류로 작성해.\n- 실제 ~/.gjc 기록에서 확인한 사용패턴만 근거로 삼아줘.\n- 민감정보는 읽지도 출력하지도 마. API key, 토큰, private credential, 개인 secret, 긴 원문 프롬프트, raw tool-result body, ~/.gjc/logs/*, auth/config/env dump는 기본적으로 건너뛰고, 안전한 집계값과 짧은 paraphrase만 써.\n\n먼저 아래 4개 축을 기준으로 타입을 판정해줘. 각 축은 한쪽을 고르되, 애매하면 근거와 함께 중간 성향이라고 설명해.\n\n1. E / P — Execute vs Plan\n - E: fix, implement, merge, ship, PR, CI, release처럼 실행/운영 명령이 강함.\n - P: deep-interview, ralplan, spec, architecture, review처럼 계획/정의/합의 흐름이 강함.\n\n2. S / M — Sprint vs Marathon\n - S: 짧은 명령, 빠른 follow-up, `continue`, 작은 세션이 많음.\n - M: 장기 세션, durable goal, 긴 프롬프트, 며칠짜리 작업 흐름이 많음.\n\n3. C / D — Craft vs Delegate\n - C: 직접 구현/수정/탐색 중심.\n - D: subagent, executor, architect, critic, autoresearch, parallel delegation 사용이 강함.\n\n4. X / O — Explore vs Operate\n - X: 새로운 모델/provider/tool/research/실험 주제가 많음.\n - O: PR, CI, release, changelog, version, production 운영/마감 흐름이 많음.\n\n분석할 최소 근거:\n- history.db의 프롬프트 수, 기간, cwd/레포 분포, 자주 쓰는 단어, skill 사용량\n- sessions jsonl의 세션 수, 세션 길이 다양성, subagent/task 흔적\n- 레포지토리/주제 다양성\n- 짧은 명령 vs 긴 지시문 패턴\n- 실행/검증/리뷰/PR/CI/릴리스/위임 습관\n\n출력 형식:\n\n# 나의 가재TI: <4글자 코드> — <타입명>\n\n## 판정 근거\n- E/P: <선택> — <실제 집계 또는 관찰 근거>\n- S/M: <선택> — <실제 집계 또는 관찰 근거>\n- C/D: <선택> — <실제 집계 또는 관찰 근거>\n- X/O: <선택> — <실제 집계 또는 관찰 근거>\n\n## 타입 설명\n밋업에서 1분 정도 읽을 수 있게, 이 사람이 가재코드를 어떻게 쓰는 사람인지 재미있게 설명해줘.\n\n## 강점\n3개 bullet.\n\n## 주의할 점\n놀리는 느낌은 살짝 있어도 되지만, 비하하지 말고 작업 습관상 조심할 점 2~3개.\n\n## 어울리는 밋업 별명\n3개.\n\n주의:\n- 이건 성격검사가 아니라 사용패턴 기반 밋업 놀이야.\n- 숫자는 실제로 확인한 값만 써.\n- 확인하지 못한 축은 억지로 단정하지 말고 “근거 부족” 또는 “혼합형”이라고 써.\n```\n",
|
|
17
|
-
"aside-integration.md": "# Aside sidecar evaluation\n\nThis note records the safe first-step boundary for evaluating [Aside](https://aside.com/) with Gajae-Code (`gjc`). The search/context sidecar path is intentionally docs-only: GJC does not ship an Aside adapter, does not auto-discover Aside, and does not enable browser-control behavior by default.\n\nThe one in-tree CLI ergonomics surface is the explicit `/aside` composer slash command. It probes a user-installed Aside CLI and can run `aside exec` / `aside account` when the operator types it. That command does not restore a GJC browser-tool backend and does not turn Aside on for ordinary browser or search tools.\n\n## Current public surface\n\nOfficial Aside docs currently describe Aside as a browser agent that can run tasks across websites, accounts, browsing history, files, saved credentials, and browser state. The developer surface includes:\n\n- `aside \"...\"` for starting a browser task from the terminal.\n- `aside --session <session-id> \"...\"` for continuing a task.\n- `aside mcp` for exposing Aside to another agent or coding tool as an MCP server.\n- `aside repl` for direct browser automation REPL tasks.\n\nThose are useful evaluation hooks, but they are not a narrow GJC-native search API. The documented Aside product surface is broader than search/context retrieval, including browser actions, login-adjacent flows, files, payments, messages, and internal websites. GJC therefore treats Aside as an external, user-owned sidecar until a separate design approves a smaller protocol contract.\n\n## Supported GJC boundary\n\nUse Aside with GJC only when the user explicitly configures it. The safe default scope is:\n\n- search, source-heavy research, summarization, and context retrieval;\n- read-only inspection prompts where possible;\n- explicit user-provided endpoint, command, and credentials;\n- no raw browser/session/private payloads in logs, PRs, issues, or support bundles.\n\nOut of scope by default:\n\n- browser actions and form submissions;\n- login flows, credential autofill, MFA, account recovery, and password-manager operations;\n- payments, purchases, subscriptions, billing changes, posts, messages, or destructive actions;\n- internal-tool workflows, customer/admin dashboards, or privileged production data;\n- file writes or local computer control through Aside;\n- automatic import of Aside browser history, cookies, task transcripts, screenshots, or local profile data into GJC.\n\nIf a task needs any out-of-scope behavior, stop and require a separate explicit design and approval path. Do not smuggle that behavior through a generic “search” tool name.\n\n## Option A: local Aside MCP command\n\nWhen the Aside CLI is installed and the operator wants to record the Aside MCP command for repo-local inspection, store the definition explicitly:\n\n```sh\ngjc mcp add aside aside mcp --project\n```\n\nUse `--project` for repo-local evaluation records. Omit it only when the operator intentionally wants the stored definition in the user-level GJC MCP config; both scopes are consumed by ordinary standalone GJC sessions at startup (conventional autoload) unless disabled or opted out with `--no-mcp`.\n\nAfter registration, inspect the redacted definition:\n\n```sh\ngjc mcp list --json\n```\n\n`gjc mcp add` makes the stored server definition available to ordinary standalone `gjc`, `gjc --tmux`, and print-mode sessions as runtime tools. Do not paste task transcripts, browser screenshots, cookies, saved credential state, or private Aside profile paths into issues or PRs. If you need to share evidence, summarize the stored definition shape and any benign externally gathered result.\n\n\nRecommended prompt boundary for evaluation:\n\n```text\nUse the Aside sidecar only for read-only search/context retrieval. Do not click, submit, sign in, autofill credentials, use payment or billing flows, post messages, write files, or operate internal tools. Return a short answer with source titles/URLs only.\n```\n\n## Option B: `/aside` composer command\n\nType `/aside` in the GJC composer to probe and use a locally installed Aside CLI. This is operator-initiated and does not enable GJC browser-control by default.\n\n```text\n/aside\n/aside Summarize the current page\n/aside exec --session <id> Continue\n/aside mcp\n/aside account list\n```\n\nBehavior:\n\n- Bare `/aside` prints the resolved CLI path, `aside --version` when available, and usage.\n- `/aside <prompt>` and `/aside exec …` spawn `aside exec` with argv (no shell).\n- `/aside mcp` prints `gjc mcp add aside <resolved-cli> mcp --project`. It does not start `aside mcp` inside GJC, because that command is a stdio server.\n- `/aside repl` is refused inside GJC. Run `aside repl` in a real terminal TTY.\n- If the CLI is missing, GJC prints the searched paths and `curl -fsSL https://releases.aside.com/install.sh | bash`. It never runs the installer.\n- On native Windows, the printed MCP and REPL commands use PowerShell quoting; the documented installer command is for WSL or Git Bash, not PowerShell itself.\n\nProbe order: `~/.local/bin/aside`, then `~/.aside/cli/Aside CLI.app/Contents/MacOS/aside`, then `PATH`.\n\nKeep the same payload boundary as the MCP path: do not paste cookies, screenshots, task transcripts, or private Aside profile paths into issues or PRs.\n\n## Option C: future HTTP/SSE MCP endpoint\n\nIf Aside or a wrapper later exposes a narrow search/context MCP endpoint, keep endpoint and credentials user-owned:\n\n```sh\nexport ASIDE_MCP_URL=\"https://aside.example.invalid/mcp\"\nexport ASIDE_API_KEY=\"...\"\ngjc mcp add aside-search --type http --url \"$ASIDE_MCP_URL\" --header Authorization=\"Bearer $ASIDE_API_KEY\" --project\n```\n\n`gjc mcp list` and `gjc mcp remove` redact header/auth values, but operators are still responsible for not echoing secrets in shell history, CI logs, screenshots, or copied terminal output. Prefer environment indirection over literals whenever possible.\n\nA future Aside search endpoint should be accepted only if it is narrower than browser automation. Minimum shape:\n\n- one or more read-only search/context tools;\n- no browser click/type/navigation tool in the same registered server unless explicitly approved;\n- no direct access to cookies, saved credentials, raw screenshots, raw task transcripts, or browser profile paths;\n- bounded response sizes with source titles/URLs and short snippets by default;\n- clear auth failure vs endpoint/network failure errors without dumping request headers or private response bodies.\n\n## Benign smoke checklist\n\nUse this checklist instead of a live login/payment/internal-site scenario:\n\n1. Register the MCP server definition with `gjc mcp add ... --project`.\n2. Run `gjc mcp list --json` and confirm secrets are redacted.\n3. Confirm the record is project-scoped or user-scoped as intended.\n4. Confirm the registration is consumed by a normal standalone GJC session in that project (tools appear at startup). To verify without a server, use `--no-mcp` or disable the server (`enabled: false` / `disabledServers`) for that session.\n5. If evaluating Aside behavior separately, run one public, non-personal query through the Aside-owned surface, for example: `Find the Aside public help page that describes MCP support and summarize the documented command names.`\n6. Confirm any shared evidence includes only public page titles/URLs or short snippets.\n7. Confirm no API key, Authorization header, cookie, browser profile path, screenshot, raw task transcript, or private session payload appears in terminal output, logs, issue comments, or PR text.\n8. Remove the evaluation server if it is no longer needed:\n\n```sh\ngjc mcp remove aside --project\n# or\ngjc mcp remove aside-search --project\n```\n\n## Troubleshooting\n\n| Symptom | Check |\n| --- | --- |\n| `aside` command not found | Run `/aside` in the GJC composer, or install the Aside CLI from Aside developer settings, then use the concrete CLI path as the MCP `command` if needed. |\n| `/aside` says the CLI was not found | Confirm the installer symlink (`~/.local/bin/aside`) or the `Aside CLI.app` bundle exists and is executable. `/aside` never runs the installer. |\n| `/aside repl` is refused | Expected. GJC cannot attach a TTY to `aside repl`; run that command in a terminal. |\n| MCP server does not appear in `gjc mcp list` | Re-run `gjc mcp list --json`; confirm whether the registration was user-scoped or project-scoped. |\n| Aside tools do not appear in a normal GJC session | Check `gjc mcp list --json`: the server must be `autoload` status (not `enabled: false`, not in `disabledServers`, not `autoload: false`), project scope must not be disabled by an explicit `mcp.enableProjectConfig: false` setting, and the session must not have passed `--no-mcp`. |\n| Auth failure | Rotate or re-enter the Aside-side token/API key. Do not paste it into GJC prompts or issue comments. |\n| Endpoint/network failure | Check the URL, proxy, and TLS path outside GJC with a benign health check; do not dump request headers. |\n| Retrieval misses context | Narrow the query to public sources first. Do not add browser history, cookies, screenshots, or account pages unless a separate approved design covers that data flow. |\n| Stored definition points at browser-action tools | Treat the server as browser automation, not search-only. Keep it as recordkeeping only for default GJC workflows unless a separate approved design covers that broader sidecar for runtime use. |\n\n## Decision\n\nDocs-only remains the smallest safe outcome for the search/context sidecar in issue #1097: existing GJC MCP registration can store a user-provided Aside MCP server definition for redacted inspection, and Aside already documents `aside mcp`. `/aside` is the separate CLI-ergonomics path: an explicit composer command that probes and optionally execs the user-installed binary, without adapter glue and without restoring the reverted Aside browser-tool backend. The future-safe boundary is to keep Aside external and opt-in, document read/search/context-only use, and require a separate design before GJC claims runtime support for browser actions, login, payment, internal-tool, or private browser-session workflows.\n",
|
|
17
|
+
"aside-integration.md": "# Aside sidecar evaluation\n\nThis note records the safe first-step boundary for evaluating [Aside](https://aside.com/) with Gajae-Code (`gjc`). The search/context sidecar path is intentionally docs-only: GJC does not ship an Aside adapter, does not auto-discover Aside, and does not enable browser-control behavior by default.\n\nThe one in-tree CLI ergonomics surface is the explicit `/aside` composer slash command. It probes a user-installed Aside CLI and can run `aside exec` / `aside account` when the operator types it. That command does not restore a GJC browser-tool backend and does not turn Aside on for ordinary browser or search tools.\n\n## Current public surface\n\nOfficial Aside docs currently describe Aside as a browser agent that can run tasks across websites, accounts, browsing history, files, saved credentials, and browser state. The developer surface includes:\n\n- `aside \"...\"` for starting a browser task from the terminal.\n- `aside --session <session-id> \"...\"` for continuing a task.\n- `aside mcp` for exposing Aside to another agent or coding tool as an MCP server.\n- `aside repl` for direct browser automation REPL tasks.\n\nThose are useful evaluation hooks, but they are not a narrow GJC-native search API. The documented Aside product surface is broader than search/context retrieval, including browser actions, login-adjacent flows, files, payments, messages, and internal websites. GJC therefore treats Aside as an external, user-owned sidecar until a separate design approves a smaller protocol contract.\n\n## Supported GJC boundary\n\nUse Aside with GJC only when the user explicitly configures it. The safe default scope is:\n\n- search, source-heavy research, summarization, and context retrieval;\n- read-only inspection prompts where possible;\n- explicit user-provided endpoint, command, and credentials;\n- no raw browser/session/private payloads in logs, PRs, issues, or support bundles.\n\nOut of scope by default:\n\n- browser actions and form submissions;\n- login flows, credential autofill, MFA, account recovery, and password-manager operations;\n- payments, purchases, subscriptions, billing changes, posts, messages, or destructive actions;\n- internal-tool workflows, customer/admin dashboards, or privileged production data;\n- file writes or local computer control through Aside;\n- automatic import of Aside browser history, cookies, task transcripts, screenshots, or local profile data into GJC.\n\nIf a task needs any out-of-scope behavior, stop and require a separate explicit design and approval path. Do not smuggle that behavior through a generic “search” tool name.\n\n## Option A: local Aside MCP command\n\nWhen the Aside CLI is installed and the operator wants to record the Aside MCP command for repo-local inspection, store the definition explicitly:\n\n```sh\ngjc mcp add aside aside mcp --project\n```\n\nUse `--project` for repo-local evaluation records. Omit it only when the operator intentionally wants the stored definition in the user-level GJC MCP config; both scopes are consumed by ordinary standalone GJC sessions at startup (conventional autoload) unless disabled or opted out with `--no-mcp`.\n\nAfter registration, inspect the redacted definition:\n\n```sh\ngjc mcp list --json\n```\n\n`gjc mcp add` makes the stored server definition available to ordinary standalone `gjc`, `gjc --tmux`, and print-mode sessions as runtime tools. Do not paste task transcripts, browser screenshots, cookies, saved credential state, or private Aside profile paths into issues or PRs. If you need to share evidence, summarize the stored definition shape and any benign externally gathered result.\n\n\nRecommended prompt boundary for evaluation:\n\n```text\nUse the Aside sidecar only for read-only search/context retrieval. Do not click, submit, sign in, autofill credentials, use payment or billing flows, post messages, write files, or operate internal tools. Return a short answer with source titles/URLs only.\n```\n\n## Option B: `/aside` composer command\n\nType `/aside` in the GJC composer to probe and use a locally installed Aside CLI. This is operator-initiated and does not enable GJC browser-control by default.\n\n```text\n/aside\n/aside Summarize the current page\n/aside exec --session <id> Continue\n/aside mcp\n/aside account list\n```\n\nBehavior:\n\n- Bare `/aside` prints the resolved CLI path, `aside --version` when available, and usage.\n- `/aside <prompt>` and `/aside exec …` spawn `aside exec` with argv (no shell).\n- `/aside mcp` prints `gjc mcp add aside <resolved-cli> mcp --project`. It does not start `aside mcp` inside GJC, because that command is a stdio server.\n- `/aside repl` is refused inside GJC. Run `aside repl` in a real terminal TTY.\n- If the CLI is missing, GJC prints the searched paths and `curl -fsSL https://releases.aside.com/install.sh | bash`. It never runs the installer.\n- On native Windows, the printed MCP and REPL commands use PowerShell quoting; the documented installer command is for WSL or Git Bash, not PowerShell itself.\n\nProbe order: `~/.local/bin/aside`, then `~/.aside/cli/Aside CLI.app/Contents/MacOS/aside`, then `PATH`.\n\nKeep the same payload boundary as the MCP path: do not paste cookies, screenshots, task transcripts, or private Aside profile paths into issues or PRs.\n\n## Option C: future HTTP/SSE MCP endpoint\n\nIf Aside or a wrapper later exposes a narrow search/context MCP endpoint, keep endpoint and credentials user-owned:\n\n```sh\nexport ASIDE_MCP_URL=\"https://aside.example.invalid/mcp\"\nexport ASIDE_API_KEY=\"...\"\ngjc mcp add aside-search --type http --url \"$ASIDE_MCP_URL\" --header Authorization=\"Bearer $ASIDE_API_KEY\" --project\n```\n\n`gjc mcp list` and `gjc mcp remove` redact header/auth values, but operators are still responsible for not echoing secrets in shell history, CI logs, screenshots, or copied terminal output. Prefer environment indirection over literals whenever possible.\n\nA future Aside search endpoint should be accepted only if it is narrower than browser automation. Minimum shape:\n\n- one or more read-only search/context tools;\n- no browser click/type/navigation tool in the same registered server unless explicitly approved;\n- no direct access to cookies, saved credentials, raw screenshots, raw task transcripts, or browser profile paths;\n- bounded response sizes with source titles/URLs and short snippets by default;\n- clear auth failure vs endpoint/network failure errors without dumping request headers or private response bodies.\n\n## Benign smoke checklist\n\nUse this checklist instead of a live login/payment/internal-site scenario:\n\n1. Register the MCP server definition with `gjc mcp add ... --project`.\n2. Run `gjc mcp list --json` and confirm secrets are redacted.\n3. Confirm the record is project-scoped or user-scoped as intended.\n4. Confirm the registration is consumed by a normal standalone GJC session in that project (tools appear at startup). To verify without a server, use `--no-mcp` or disable the server (`enabled: false` / `disabledServers`) for that session.\n5. If evaluating Aside behavior separately, run one public, non-personal query through the Aside-owned surface, for example: `Find the Aside public help page that describes MCP support and summarize the documented command names.`\n6. Confirm any shared evidence includes only public page titles/URLs or short snippets.\n7. Confirm no API key, Authorization header, cookie, browser profile path, screenshot, raw task transcript, or private session payload appears in terminal output, logs, issue comments, or PR text.\n8. Remove the evaluation server if it is no longer needed:\n\n```sh\ngjc mcp remove aside --project\n# or\ngjc mcp remove aside-search --project\n```\n\n## Troubleshooting\n\n| Symptom | Check |\n| --- | --- |\n| `aside` command not found | Run `/aside` in the GJC composer, or install the Aside CLI from Aside developer settings, then use the concrete CLI path as the MCP `command` if needed. |\n| `/aside` says the CLI was not found | Confirm the installer symlink (`~/.local/bin/aside`) or the `Aside CLI.app` bundle exists and is executable. `/aside` never runs the installer. |\n| `/aside repl` is refused | Expected. GJC cannot attach a TTY to `aside repl`; run that command in a terminal. |\n| MCP server does not appear in `gjc mcp list` | Re-run `gjc mcp list --json`; confirm whether the registration was user-scoped or project-scoped. |\n| Aside tools do not appear in a normal GJC session | Check `gjc mcp list --json`: the server must be `autoload` status (not `enabled: false`, not in `disabledServers`, not `autoload: false`), project scope must not be disabled by an explicit `mcp.enableProjectConfig: false` setting, and the session must not have passed `--no-mcp`. |\n| Auth failure | Rotate or re-enter the Aside-side token/API key. Do not paste it into GJC prompts or issue comments. |\n| Endpoint/network failure | Check the URL, proxy, and TLS path outside GJC with a benign health check; do not dump request headers. |\n| Retrieval misses context | Narrow the query to public sources first. Do not add browser history, cookies, screenshots, or account pages unless a separate approved design covers that data flow. |\n| Stored definition points at browser-action tools | Treat the server as browser automation, not search-only. Keep it as recordkeeping only for default GJC workflows unless a separate approved design covers that broader sidecar for runtime use. |\n\n## Decision\n\nDocs-only remains the smallest safe outcome for the search/context sidecar in issue #1097: existing GJC MCP registration can store a user-provided Aside MCP server definition for redacted inspection, and Aside already documents `aside mcp`. `/aside` is the separate CLI-ergonomics path: an explicit composer command that probes and optionally execs the user-installed binary, without adapter glue and without restoring the reverted Aside browser-tool backend. The future-safe boundary is to keep Aside external and opt-in, document read/search/context-only use, and require a separate design before GJC claims runtime support for browser actions, login, payment, internal-tool, or private browser-session workflows.\n\n## Option D: `browser.backend: aside` (CLI routing)\n\n`browser.backend` selects the browser surface offered to the model. The default `\"native\"` keeps the built-in Puppeteer `browser` tool. Setting it to `\"aside\"` hides the built-in tool (it is neither active nor discoverable) and appends a `<browser-backend>` system-prompt block that requires every browser task to run through the Bash tool as a direct `aside repl` (deterministic Playwright) or `aside exec` (agentic) invocation. It does not register or start an MCP server, does not spawn or supervise any Aside process from GJC, and does not restore the reverted in-process Aside browser driver; the boundary decision above is unchanged because the operator opts in explicitly per setting.\n\nThere is no fallback to the native browser. When the Aside CLI is unavailable, the direct CLI command fails in the Bash tool result and the session has no browser tool.\n\n```sh\ngjc config set browser.backend aside\n```\n\nThe routing contract follows Aside's official developer documentation\n(`https://docs.aside.com/help/developers.md`, indexed by\n`https://docs.aside.com/llms.txt`) and the API references bundled with the\ninstalled Aside version. It covers:\n\n- deterministic Playwright through `aside repl`, including tab attachment,\n snapshots, locators, authenticated fetch/downloads, screenshots, Chrome\n profile APIs, visual CUA fallback, and authorized CAPTCHA helpers;\n- agentic `aside exec` work for runtime judgment, source-heavy research,\n browser history, MFA/autofill, approvals, notifications, monitoring,\n multi-site synthesis, routines, and resumable `--session` follow-ups;\n- account/model/provider/speed/effort targeting without changing user defaults;\n- dedicated Google, document, media, social, password-manager, PDF, and Office\n integrations when the installed Aside build exposes them; and\n- explicit permission, privacy, credential, payment, messaging, posting,\n document-edit, profile-mutation, and destructive-action boundaries.\n\nThe always-injected prompt stays a routing and safety contract rather than\ncopying every versioned method signature. The user-level `aside` skill contains\nthe detailed API inventory and points agents to the installed official builtin\nskills when exact signatures matter.\n",
|
|
18
18
|
"auth-broker-gateway.md": "# Auth Broker and Auth Gateway\n\nThe auth broker and auth gateway are two cooperating HTTP services that move OAuth refresh tokens and provider access tokens off developer laptops and into a single broker host.\n\n- **`gjc auth-broker serve`** holds the canonical SQLite credential vault, performs OAuth refreshes, and exposes a small REST API (`/v1/snapshot`, `/v1/credential/:id/refresh`, `/v1/credential/:id/disable`, `/v1/credential`, `/v1/usage`, `/v1/usage/scoped?provider=<provider>`, `/v1/healthz`).\n- **`gjc auth-gateway serve --provider=<provider>`** is a provider-scoped forward-proxy. It accepts OpenAI Chat Completions, Anthropic Messages, and OpenAI Responses requests, injects the broker-resolved access token, and dispatches only through the selected provider catalog. Clients (containerised gjc, llm-git, the macOS usage widget, …) never see the access token.\n\nTransport security between operator, broker, and gateway is delegated to the operator (Tailscale / Wireguard / reverse proxy + TLS). Every endpoint except `/v1/healthz` (broker) and `/healthz` (gateway) requires a bearer token when bearer authentication is configured. The gateway's `--no-auth` mode disables inbound bearer checks only on a loopback bind; an unauthenticated non-loopback bind is rejected at startup.\n\nSource: `packages/ai/src/auth-broker/`, `packages/ai/src/auth-gateway/`, `packages/coding-agent/src/cli/auth-broker-cli.ts`, `packages/coding-agent/src/cli/auth-gateway-cli.ts`, `packages/coding-agent/src/session/startup-auth-config.ts`.\n\n## Data flow\n\n```\n ┌────────────────────────────────────────────────────────────┐\n │ broker host │\n │ │\n developer ──▶ │ ┌──────────────────────────┐ ┌────────────────────┐ │\n laptop / │ │ gjc auth-broker serve │◀──▶│ SQLite agent.db │ │\n CI │ │ - holds refresh tokens │ │ (canonical writer)│ │\n │ │ - background refresher │ └────────────────────┘ │\n │ │ /v1/{snapshot,refresh,…}│ │\n │ └─────────┬────────────────┘ │\n │ │ bearer ($CONFIG_DIR/auth-broker.token) │\n │ ▼ │\n │ ┌──────────────────────────┐ │\n │ │ gjc auth-gateway serve │ RemoteAuthCredentialStore │\n │ │ /v1/{chat,messages,…} │ pulls /v1/snapshot at boot, │\n │ │ /v1/usage, /v1/models │ refreshes credentials by id │\n │ └─────────┬────────────────┘ via the broker on expiry │\n └────────────┼───────────────────────────────────────────────┘\n │ bearer ($CONFIG_DIR/auth-gateway.token)\n ▼\n unauthenticated clients\n (llm-git, macOS widget, IDE plugins, …)\n │\n ▼ same path is forwarded with Authorization\n api.anthropic.com / api.openai.com / …\n```\n\nThe broker is the only writer of OAuth refresh tokens. Clients (including the gateway itself) load a redacted snapshot in which every `refresh` field has been replaced with `REMOTE_REFRESH_SENTINEL`; when an access token expires the client calls `POST /v1/credential/:id/refresh` and the broker performs the refresh server-side. `RemoteAuthCredentialStore` rejects any local code path that tries to write through it, with an error pointing at `gjc auth-broker login` / `gjc auth-broker logout`.\n\n## auth-broker\n\n### CLI\n\n```\ngjc auth-broker serve [--bind=host:port] # boot the broker\ngjc auth-broker token [--regenerate] [--json] # print or rotate the bearer token\ngjc auth-broker login <provider> [--via=user@host] [--dry-run]\ngjc auth-broker logout <provider>\ngjc auth-broker import <file|dir> [--provider=<id>] [--include-disabled] [--dry-run] [--json]\ngjc auth-broker migrate --from-local [--dry-run] [--json]\ngjc auth-broker status [--json]\n```\n\n- `serve` opens the local SQLite store at `getAgentDbPath()` and binds an HTTP listener (default `127.0.0.1:8765`). On startup a token is ensured at `<config-dir>/auth-broker.token` (mode `0600`, `0700` parent dir). The background refresher refreshes any OAuth credential whose `expires - Date.now() < refreshSkewMs` (default 5 min) every `refreshIntervalMs` (default 60 s).\n- `token` prints the cached bearer or generates a new one. `--regenerate` rotates it.\n- `login <provider>` runs the per-provider OAuth flow locally, or — with `--via=user@host` — `ssh -L <callback-port>:127.0.0.1:<callback-port> user@host gjc auth-broker login <provider>` so the OAuth callback hits the local browser but the credential is written on the broker host. Built-in callback ports: `anthropic:54545`, `openai-code:1455`, `google-gemini-cli:8085`, `google-antigravity:51121`, `gitlab-duo:8080`.\n When no port forward is possible, run the interactive TUI on that host and use `/login anthropic --manual`, which pairs by pasting the code Anthropic renders at `https://platform.claude.com/oauth/code/callback` instead of using a loopback callback at all. `gjc auth-broker login` itself has no manual mode.\n- `logout <provider>` deletes every credential row for `<provider>`.\n- `import <file|dir>` imports CLIProxyAPI-style JSON credentials into the local SQLite store. Maps `type` field → gjc provider (`anthropic-model → anthropic`, `openai-code → openai-code`, `gemini → google-gemini-cli`, `antigravity → google-antigravity`, `gemini-cli → google-gemini-cli`).\n- `migrate --from-local` walks the local SQLite store + env-derived credentials and idempotently uploads them to the configured broker (`POST /v1/credential`).\n- `status` health-pings the configured remote broker.\n\nBroker startup allocates a durable monotonic sequence for its restart epoch. Any\ncustom `AuthCredentialStore` integration must implement atomic\n`allocateMonotonicSequence(key, expiresAtSec)`; stores that cannot provide this\ndurability are rejected before the listener binds.\n\n### Endpoints\n\n| Method | Path | Auth | Purpose |\n| ------ | ---- | ---- | ------- |\n| `GET` | `/v1/healthz` | none | Liveness + version |\n| `GET` | `/v1/snapshot` | bearer | Redacted snapshot (refresh tokens replaced by sentinel) |\n| `POST` | `/v1/credential` | bearer | Upsert one OAuth or API-key credential |\n| `POST` | `/v1/credential/:id/refresh` | bearer | Force-refresh one OAuth credential |\n| `POST` | `/v1/credential/:id/disable` | bearer | Disable one credential with a recorded cause |\n| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` across credentials |\n| `GET` | `/v1/usage/scoped?provider=<provider>` | bearer | Provider-scoped `UsageReport[]`; unavailable on legacy brokers |\n\nRequests use `Authorization: Bearer <token>`. The server compares against an in-memory token allow-list; the gateway’s implementation uses a timing-safe comparison.\n\n### Background refresher\n\n`AuthBrokerRefresher` iterates active OAuth credentials at `refreshIntervalMs` cadence and refreshes any within `refreshSkewMs` of expiry. Refreshes are single-flighted per credential id so a slow refresh cannot be retriggered. The refresher distinguishes:\n\n- **definitive failures** (`invalid_grant`, `invalid_token`, `revoked`, unauthorized refresh-token, 401/403 not from a network blip) — credentials are passed to `AuthStorage.disableCredentialById(id, cause)` so the next snapshot pull surfaces a clean delete on the client;\n- **transient failures** (timeout / ECONNREFUSED / fetch failed) — left in place for the next sweep.\n\n## auth-gateway\n\n### CLI\n\n```\ngjc auth-gateway serve --provider=<provider> [--bind=host:port] [--no-auth]\ngjc auth-gateway token [--regenerate] [--json]\ngjc auth-gateway status [--provider=<provider>] [--json]\ngjc auth-gateway check [--provider=<provider>] [--json]\n```\n\n- `serve` requires an explicit `--provider`. It fetches the broker snapshot before binding and fails closed unless that snapshot contains an enabled credential for the selected provider. The gateway is itself a broker client: it calls `AuthBrokerClient.fetchSnapshot()`, wraps it in `RemoteAuthCredentialStore`, and constructs an `AuthStorage` that resolves access tokens through the broker. Default bind is `127.0.0.1:4000`. The gateway token is stored at `<config-dir>/auth-gateway.token` (`0600`); `--no-auth` disables the inbound bearer check only on a loopback bind for non-browser local clients. Browser-Origin requests and preflight are rejected in no-auth mode. A non-loopback unauthenticated bind is rejected at startup.\n- The selected provider is the complete gateway scope. The bundled catalog is filtered to that provider before startup; model ids from other providers are never admitted, so duplicate ids cannot select a credential by enumeration order. An unscoped `serve` invocation is rejected rather than using first-write routing. Requests require exact membership in the scoped catalog and resolve credentials only for the scoped provider.\n- `status` and `check` accept `--provider` to report or probe one scope. Their JSON and text output includes the scope and redacts credential/token material; an unscoped `status` reports not-ready instead of claiming gateway readiness.\n- `token` manages only the inbound gateway bearer token.\n\n### Endpoints\n\n| Method | Path | Auth | Purpose |\n| ------ | ---- | ---- | ------- |\n| `GET` | `/healthz` | none | Liveness + version |\n| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` for the selected provider scope |\n| `GET` | `/v1/credentials/check` | bearer | Credential health for the selected provider scope |\n| `GET` | `/v1/models` | bearer | Bundled-model catalog for the selected provider scope |\n| `POST` | `/v1/chat/completions` | bearer | OpenAI Chat Completions wire format |\n| `POST` | `/v1/messages` | bearer | Anthropic Messages wire format |\n| `POST` | `/v1/responses` | bearer | OpenAI Responses wire format |\n| `POST` | `/v1/pi/stream` | bearer | Native gjc stream format |\n\nThe model id is read from the top-level `model` field and must be an exact member of the selected provider’s source-backed catalog. The gateway resolves the scoped model and obtains the credential for that model’s provider only; it never falls back to a credential from another provider. `/v1/models` emits the catalog model’s `owned_by` and `api` values. In particular, an `openai-codex` scope exposes Codex-owned rows with `owned_by: \"openai-codex\"` and `api: \"openai-codex-responses\"`; it cannot project the same id through GitHub Copilot or generic OpenAI Responses.\n\nAll provider-format routes parse into gjc’s canonical `Context`, dispatch through `streamSimple()`, and encode back to the inbound wire format. This keeps provider-specific OAuth shaping, headers, and transport selection on the source-backed model.\n\n`idleTimeout` on the underlying `Bun.serve` is set to `255 s` so long thinking-budget calls do not get killed by Bun’s default idle timeout.\n\n## Usage cache: server-side 5-min jitter + client-side 15 s single-flight\n\nTwo layers cache the aggregate provider-usage report. Both are intentional and stacked.\n\n### Server-side cache (broker `AuthStorage`)\n\n`AuthStorage` caches each credential’s `UsageReport` in the broker’s SQLite store at a **5-minute per-credential TTL with ±25 % jitter**. Anthropic and OpenAI rate-limit `/usage` aggressively per source IP, and a synchronized 5-credential fan-out trips 429s every cycle; the jitter decorrelates refresh times within a few cycles. On fetch failure the store keeps the **last-good** report for up to 24 h with a short jittered re-poll window — so a transient upstream blip never blanks out the widget.\n\nConstants: `USAGE_REPORT_TTL_MS = 5 * 60_000`, `USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000` (`packages/ai/src/auth-storage.ts`).\n\n### Client-side single-flight (`RemoteAuthCredentialStore`)\n\nWhen the gateway (or any other broker client) calls `fetchUsageReports()` / `getUsageReport(provider, credential)`, `RemoteAuthCredentialStore` uses the versioned `/v1/usage/scoped?provider=<provider>` route for provider-scoped calls and caches the result for **15 s** in memory. Legacy brokers that do not expose the scoped route fail closed instead of falling back to aggregate usage.\n\n- `USAGE_CACHE_TTL_MS = 15_000` (`packages/ai/src/auth-broker/remote-store.ts`).\n- A single `#usageInflight` promise is shared across all callers; a per-caller `AbortSignal` is **raced** against the shared promise, not threaded into it, so one caller’s abort never cascades into a peer’s in-flight request.\n- On fetch failure the rejected promise is logged and the awaited value is `null` — callers (`AuthStorage.fetchUsageReports`, `#getUsageReport`) treat a `null` report as \"no usage signal for this cycle\" and proceed without it. **This is the 15 s TTL fallback**: the client absorbs transient broker outages by suppressing the error, returning `null` to ranking, and re-attempting after the 15 s window.\n\nThe 15 s client window deliberately sits below the broker’s 5 min server cache, so almost every client poll is served from the broker’s already-cached value; the client cache exists to absorb the parallel fan-out generated by `AuthStorage.#rankOAuthSelections` into a single broker round-trip.\n\n## Operator opt-in\n\nThe broker is **off** unless `GJC_AUTH_BROKER_URL` (or `auth.broker.url` in `config.yml`) is set. When set, `discoverAuthStorage` in `packages/coding-agent/src/sdk/session.ts` swaps the local SQLite credential store for `RemoteAuthCredentialStore` and every API call resolves credentials through the broker.\n\n### Environment variables\n\n| Variable | Purpose | Required when |\n| -------- | ------- | ------------- |\n| `GJC_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`). Selecting this puts the client in broker mode — local SQLite is bypassed. | Any time the gjc client should resolve credentials through a broker (and required by `gjc auth-gateway serve --provider=<provider>`). |\n| `GJC_AUTH_BROKER_TOKEN` | Bearer token used for every broker endpoint except `/v1/healthz`. | When a broker URL is set and no token is available from nested config or `<config-dir>/auth-broker.token`. |\n\n### Startup resolver and configuration\n\nThe startup resolver reads the global agent `config.yml` before the normal settings layer. Broker settings must use the canonical nested YAML shape:\n\n```yaml\nauth:\n broker:\n url: https://broker.example.test:8765\n token: <literal-bearer-token>\n```\n\nResolution is explicit and ordered:\n\n1. `GJC_AUTH_BROKER_URL` takes precedence over the nested `auth.broker.url` value.\n2. If a URL is resolved, `GJC_AUTH_BROKER_TOKEN` takes precedence over nested `auth.broker.token`, which takes precedence over the trimmed contents of `<config-dir>/auth-broker.token`.\n3. A resolved URL without a token is a hard startup error; GJC does not fall back to the local SQLite store.\n\nThe nested URL and token entries may be literal strings or exact `$ENV_NAME` references resolved from the trusted process environment. Missing config is allowed and leaves broker mode disabled. An unreadable file, invalid YAML, non-mapping root, malformed `auth`/`broker`/`gateway` section, unresolved nested URL reference, invalid ranking mode, or invalid credential-pin record fails closed with a typed `StartupAuthConfigError` rather than silently downgrading to local authority. An unresolved nested token reference may still fall through to the owner-only token file. Legacy literal dotted auth keys are rejected with manual nested-YAML rewrite guidance.\n\nThe gateway has no dedicated env vars — it inherits `GJC_AUTH_BROKER_*` because it is itself a broker client.\n\n### `config.yml` keys\n\n| Key | Default | Purpose |\n| --- | ------- | ------- |\n| `auth.broker.url` | unset | Same as `GJC_AUTH_BROKER_URL`; env wins. Hidden from the settings UI. |\n| `auth.broker.token` | unset | Same as `GJC_AUTH_BROKER_TOKEN`; env wins. Accepts a literal bearer token or exact `$ENV_NAME` reference. |\n\n### Token files\n\n| Path | Owner | Mode |\n| ---- | ----- | ---- |\n| `<config-dir>/auth-broker.token` | `gjc auth-broker serve` (created at first start) | `0600` in a `0700` parent dir |\n| `<config-dir>/auth-gateway.token` | `gjc auth-gateway serve` (skipped under `--no-auth`) | `0600` in a `0700` parent dir |\n\n`<config-dir>` resolves to `~/.gjc/` (respecting `GJC_CONFIG_DIR`).\n\n## Interaction with the local API-key resolution order\n\nThe broker only owns OAuth credentials and provider-API-key credentials that were uploaded to it. The standard credential ladder in `models.md` (`Auth and API key resolution order`) is preserved, with one addition committed alongside the gateway:\n\n- `AuthStorage.setConfigApiKey / removeConfigApiKey / clearConfigApiKeys` let a `models.yml` `apiKey` beat a stored OAuth token **without** overriding an explicit `--api-key`. This is what allows a broker-resolved OAuth credential to be reliably shadowed by a per-environment `models.yml` config key when both are present.\n\n## See also\n\n- [`secrets.md`](./secrets.md) — secret obfuscation around tokens that *do* leak through (e.g. `GJC_AUTH_BROKER_TOKEN` in shell output).\n- [`models.md`](./models.md) — provider auth resolution order; the broker plugs in at layers 2–3 (stored credentials).\n- [`environment-variables.md`](./environment-variables.md) — full env reference including `GJC_AUTH_BROKER_URL` / `GJC_AUTH_BROKER_TOKEN`.\n",
|
|
19
19
|
"bash-tool-runtime.md": "# Bash tool runtime\n\nThis document describes the **`bash` tool** runtime path used by agent tool calls, from command normalization to execution, truncation/artifacts, and rendering.\n\nIt also calls out where behavior diverges in interactive TUI, print mode, ACP, and user-initiated bang (`!`) shell execution.\n\n## Scope and runtime surfaces\n\nThere are two different bash execution surfaces in coding-agent:\n\n1. **Tool-call surface** (`toolName: \"bash\"`): used when the model calls the bash tool.\n - Entry point: `BashTool.execute()`.\n - Parameters include `command`, optional `env`, `timeout`, `cwd`, `pty`, and, when `async.enabled` is true, `async`.\n2. **User bang-command surface** (`!cmd` from interactive input): session-level helper path.\n - Entry point: `AgentSession.executeBash()`.\n\nBoth eventually use `executeBash()` in `src/exec/bash-executor.ts` for non-PTY execution, but only the tool-call path runs normalization/interception, optional managed background-job handling, and tool renderer logic.\n\n## End-to-end tool-call pipeline\n\n## 1) Input handling and parameter merge\n\n`BashTool.execute()` currently handles input before execution as follows:\n\n- validates optional `env` names against shell-variable syntax,\n- extracts a leading `cd <path> && ...` into `cwd` when `cwd` was not supplied,\n- rejects `async: true` when `async.enabled` is false,\n- optionally removes harmless trailing `| head ...` / `| tail ...` limiters through `applyBashFixups()` when `bash.stripTrailingHeadTail` is enabled,\n- leaves output-window selection to `OutputSink`: a 1 KiB tail by default, an explicitly configured `tools.artifactTailBytes` tail budget, or head+tail when `tools.artifactHeadBytes` is explicitly configured.\n\n## 2) Optional interception (blocked-command path)\n\nIf `bashInterceptor.enabled` is true, `BashTool` loads rules from settings and runs `checkBashInterception()` against the normalized command.\n\nInterception behavior:\n\n- command is blocked **only** when:\n - regex rule matches, and\n - the suggested tool is present in `ctx.toolNames`.\n- invalid regex rules are silently skipped.\n- on block, `BashTool` throws `ToolError` with message:\n - `Blocked: ...`\n - original command included.\n\nDefault rule patterns (defined in code) target common misuses:\n\n- file readers (`cat`, `head`, `tail`, ...)\n- search tools (`grep`, `rg`, ...)\n- file finders (`find`, `fd`, ...)\n- in-place editors (`sed -i`, `perl -i`, `awk -i inplace`)\n- shell redirection writes (`echo ... > file`, heredoc redirection)\n\n### Caveat\n\n`InterceptionResult` includes `suggestedTool`, but `BashTool` currently surfaces only the message text (no structured suggested-tool field in `details`).\n\n## 3) CWD validation and timeout clamping\n\n`cwd` is resolved relative to session cwd (`resolveToCwd`), then validated via `stat`:\n\n- missing path -> `ToolError(\"Working directory does not exist: ...\")`\n- non-directory -> `ToolError(\"Working directory is not a directory: ...\")`\n\nTimeout is clamped to `[1, 3600]` seconds and converted to milliseconds.\n\n## 4) Artifact allocation\n\nBefore execution, the tool allocates an artifact path/id (best-effort) for truncated output storage.\n\n- artifact allocation failure is non-fatal (execution continues without artifact spill file),\n- artifact id/path are passed into execution path for full-output persistence on truncation.\n\n## 5) PTY vs non-PTY execution selection\n\n`BashTool` chooses PTY execution only when all are true:\n\n- tool input `pty === true`\n- `GJC_NO_PTY !== \"1\"`\n- tool context has UI (`ctx.hasUI === true` and `ctx.ui` set)\n\nOtherwise it uses non-interactive `executeBash()`.\n\nThat means print mode and non-UI tool contexts always use non-PTY.\n\n## Non-interactive execution engine (`executeBash`)\n\n## Shell session reuse model\n\n`executeBash()` caches native `Shell` instances in a process-global map keyed by:\n\n- shell path,\n- configured command prefix,\n- snapshot path,\n- serialized shell env,\n- optional agent session key.\n\nSession-level bang-command executions pass `sessionKey: this.sessionId`.\n\nTool-call executions pass `sessionKey: this.session.getSessionId?.()`, when available. In both surfaces, a session key isolates shell reuse per session; without one, reuse falls back to shell config/snapshot/env.\n\n## Shell config and snapshot behavior\n\nAt each call, executor loads settings shell config (`shell`, `env`, optional `prefix`).\n\nIf selected shell includes `bash`, it attempts `getOrCreateSnapshot()`:\n\n- snapshot captures aliases/functions/options from user rc,\n- snapshot creation is best-effort,\n- failure falls back to no snapshot.\n\nIf `prefix` is configured, command becomes:\n\n```text\n<prefix> <command>\n```\n\n## Streaming and cancellation\n\n`Shell.run()` streams chunks to `OutputSink` and optional `onChunk` callback.\n\nCancellation:\n\n- aborted signal triggers `shellSession.abort(...)`,\n- timeout from native result is mapped to `cancelled: true` + annotation text,\n- explicit cancellation similarly returns `cancelled: true` + annotation.\n\nNo exception is thrown inside executor for timeout/cancel; it returns structured `BashResult` and lets caller map error semantics.\n\n## Interactive PTY path (`runInteractiveBashPty`)\n\nWhen PTY is enabled, tool runs `runInteractiveBashPty()` which opens an overlay console component and drives a native `PtySession`.\n\nBehavior highlights:\n\n- xterm-headless virtual terminal renders viewport in overlay,\n- keyboard input is normalized (including Kitty sequences and application cursor mode handling),\n- `esc` while running kills the PTY session,\n- terminal resize propagates to PTY (`session.resize(cols, rows)`).\n\nEnvironment hardening defaults are injected for unattended runs:\n\n- pagers disabled (`PAGER=cat`, `GIT_PAGER=cat`, etc.),\n- editor prompts disabled (`GIT_EDITOR=true`, `EDITOR=true`, ...),\n- terminal/auth prompts reduced (`GIT_TERMINAL_PROMPT=0`, `SSH_ASKPASS=/usr/bin/false`, `CI=1`),\n- package-manager/tool automation flags for non-interactive behavior.\n\nPTY output is normalized (`CRLF`/`CR` to `LF`, `sanitizeText`) and written into `OutputSink`, including artifact spill support.\n\nOn PTY startup/runtime error, sink receives `PTY error: ...` line and command finalizes with undefined exit code.\n\n## Output handling: streaming, truncation, artifact spill\n\nBoth PTY and non-PTY paths use `OutputSink`.\n\n## OutputSink semantics\n\n- keeps a small in-memory UTF-8-safe tail buffer (1 KiB by default),\n- uses an explicitly configured `tools.artifactTailBytes` value to set the Bash tail budget,\n- retains no head window by default; explicitly configuring `tools.artifactHeadBytes` opts Bash into head+tail middle elision,\n- tracks total bytes/lines seen,\n- if an artifact path exists and output overflows (or the file is already active), writes the stream up to the artifact hard cap; any omitted bytes are counted and disclosed instead of calling the artifact complete,\n- when memory threshold overflows, trims the in-memory buffer to the tail (UTF-8 boundary safe),\n- marks `truncated` when overflow/file spill occurs.\n\n`dump()` returns:\n\n- `output` (possibly annotated prefix),\n- `truncated`,\n- `totalLines/totalBytes`,\n- `outputLines/outputBytes`,\n- `artifactId` if artifact file was active.\n- `artifactTruncatedBytes` when the artifact hard cap omitted bytes.\n\n### Long-output caveat\n\n`BashTool` supplies a 1 KiB byte threshold to `OutputSink` by default, overridden by an explicit `tools.artifactTailBytes` setting. Direct user bang commands continue to use the executor's shared 50 KiB tail plus configured head window. Neither path enforces a hard line-count cap.\n\n## Live tool updates and async jobs\n\nForeground streamed updates, PTY capture, managed async jobs, and monitor jobs all use the Bash retention policy resolved from the active `ToolSession`: a 1 KiB UTF-8-safe tail by default, an explicit `tools.artifactTailBytes` tail budget, and optional `tools.artifactHeadBytes` head retention for final captured output. Foreground, async, and monitor progress callbacks use bounded tail previews; PTY live rendering remains in the custom overlay while its final capture uses the same `OutputSink` budgets.\n\nWhen `async.enabled` is true and the call passes `async: true`, `BashTool` starts a managed Bash job, returns a running job result with a job id, and stores bounded completion output through the session managed-job path. Auto-backgrounding can start the same path after `bash.autoBackground.thresholdMs`.\n\n### ACP client-terminal retention\n\nWhen the connected client owns terminal execution, GJC requests the same bounded Bash output contract through `outputByteLimit`:\n\n- the default request retains the last 1 KiB; ACP truncates from the beginning at a UTF-8 character boundary,\n- an explicit `tools.artifactTailBytes` value sets that requested tail limit,\n- an explicit `tools.artifactHeadBytes` value omits the client-side byte limit so GJC can receive the complete returned stream, apply local head+tail middle elision, and save the full returned output when artifact storage is available,\n- if the client itself reports `truncated: true`, the returned bytes are already incomplete and GJC does not label an artifact made from that partial value as the full capture,\n- poll updates and timeout output use the same local retention policy; a complete oversized timeout capture is saved before the bounded error is surfaced when artifact storage is available.\n\nFor an ACP result where the client reports `truncated: true`, a truncation notice without an `artifact://` link means GJC never received the full stream. Separately, when artifact allocation is unavailable, a complete local capture can remain without a link or diagnostic because SDK allocation wrappers may return an empty value; if an artifact writer/save operation is attempted and fails, it emits a bounded diagnostic without inventing an artifact URI.\n\n## Result shaping, metadata, and error mapping\n\nAfter execution:\n\n1. `cancelled` handling:\n - if abort signal is aborted -> throw `ToolAbortError` (abort semantics),\n - else -> throw `ToolError` (treated as tool failure).\n2. PTY `timedOut` -> throw `ToolError`.\n3. retain only the final 1 KiB output window by default (or use explicit `tools.artifactTailBytes` / `tools.artifactHeadBytes` retention budgets).\n4. empty output becomes `(no output)`.\n5. attach truncation metadata via `toolResult(...).truncationFromSummary(result, { direction: \"tail\" })`.\n6. exit-code mapping:\n - missing exit code -> `ToolError(\"... missing exit status\")`\n - non-zero exit -> `ToolError(\"... Command exited with code N\")`\n - zero exit -> success result.\n\nSuccess payload structure:\n\n- `content`: text output,\n- `details.meta.truncation` when truncated, including:\n - `direction`, `truncatedBy`, total/output line+byte counts,\n - `shownRange`,\n - `artifactId` when available.\n\nBecause built-in tools are wrapped with `wrapToolWithMetaNotice()`, truncation notice text is appended to final text content automatically; when truncation metadata includes an artifact reference, that notice can include an example such as `Full: artifact://<id>`.\n\n## Rendering paths\n\n## Tool-call renderer (`bashToolRenderer`)\n\n`bashToolRenderer` is used for tool-call messages (`toolCall` / `toolResult`):\n\n- collapsed mode shows visual-line-truncated preview,\n- expanded mode shows all currently available output text,\n- warning line includes the truncation reason and, when metadata has one, its `artifact://<id>` reference,\n- timeout value (from args) is shown in footer metadata line.\n\n### Caveat: full artifact expansion\n\n`BashRenderContext` has `isFullOutput`, but current renderer context builder does not set it for bash tool results. Expanded view still uses the text already in result content (tail/truncated output) unless another caller provides full artifact content.\n\n## User bang-command component (`BashExecutionComponent`)\n\n`BashExecutionComponent` is for user `!` commands in interactive mode (not model tool calls):\n\n- streams chunks live,\n- collapsed preview keeps last 20 logical lines,\n- line clamp at 4000 chars per line,\n- shows truncation + artifact warnings when metadata is present,\n- marks cancelled/error/exit state separately.\n\nThis component is wired by `CommandController.handleBashCommand()` and fed from `AgentSession.executeBash()`.\n\n## Mode-specific behavior differences\n\n| Surface | Entry path | PTY eligible | Live output UX | Error surfacing |\n| ------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ |\n| Interactive tool call | `BashTool.execute` | Yes, when `pty=true` and UI exists and `GJC_NO_PTY!=1` | PTY overlay (interactive) or streamed tail updates | Tool errors become `toolResult.isError` |\n| Print mode tool call | `BashTool.execute` | No (no UI context) | No TUI overlay; output appears in event stream/final assistant text flow | Same tool error mapping |\n| ACP tool call (agent tooling) | `BashTool.execute` | Usually no UI -> non-PTY | Structured protocol events/results | Same tool error mapping |\n| Interactive bang command (`!`) | `AgentSession.executeBash` + `BashExecutionComponent` | No (uses executor directly) | Dedicated bash execution component | Controller catches exceptions and shows UI error |\n\n## Operational caveats\n\n- Interceptor only blocks commands when suggested tool is currently available in context.\n- If artifact allocation/storage is unavailable before a writer/save operation is attempted, truncation still occurs without an `artifact://` back-reference and may have no diagnostic because SDK allocation wrappers can return an empty value. If a writer/save operation is attempted and fails, Bash emits a bounded diagnostic; it never fabricates a reference.\n- Shell session cache has no explicit eviction in this module; lifetime is process-scoped.\n- PTY and non-PTY timeout surfaces differ:\n - PTY exposes explicit `timedOut` result field,\n - non-PTY maps timeout into `cancelled + annotation` summary.\n\n## Implementation files\n\n- [`src/tools/bash.ts`](../packages/coding-agent/src/tools/bash.ts) — tool entrypoint, input handling/interception, async and PTY/non-PTY selection, result/error mapping, bash tool renderer.\n- [`src/tools/bash-command-fixup.ts`](../packages/coding-agent/src/tools/bash-command-fixup.ts) — optional removal of harmless trailing `head`/`tail` limiters before execution.\n- [`src/tools/bash-interceptor.ts`](../packages/coding-agent/src/tools/bash-interceptor.ts) — interceptor rule matching and blocked-command messages.\n- [`src/exec/bash-executor.ts`](../packages/coding-agent/src/exec/bash-executor.ts) — non-PTY executor, shell session reuse, cancellation wiring, output sink integration.\n- [`src/tools/bash-interactive.ts`](../packages/coding-agent/src/tools/bash-interactive.ts) — PTY runtime, overlay UI, input normalization, non-interactive env defaults.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink`, `TailBuffer`, truncation/artifact spill, and summary metadata.\n- [`src/tools/output-meta.ts`](../packages/coding-agent/src/tools/output-meta.ts) — truncation metadata shape + notice injection wrapper.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — session-level `executeBash`, message recording, abort lifecycle.\n- [`src/modes/components/bash-execution.ts`](../packages/coding-agent/src/modes/components/bash-execution.ts) — interactive `!` command execution component.\n- [`src/modes/controllers/command-controller.ts`](../packages/coding-agent/src/modes/controllers/command-controller.ts) — wiring for interactive `!` command UI stream/update completion.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://<id>` resolution.\n",
|
|
20
20
|
"blob-artifact-architecture.md": "# Blob and artifact storage architecture\n\nThis document describes how coding-agent stores large/binary payloads outside session JSONL, how truncated tool output is persisted, and how internal URLs (`artifact://`, `agent://`) resolve back to stored data.\n\n## Why two storage systems exist\n\nThe runtime uses two different persistence mechanisms for different data shapes:\n\n- **Content-addressed blobs** (`blob:sha256:<hash>`): global storage used to externalize large image base64 payloads and provider image data URLs 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\nThey are intentionally separate:\n\n- blob storage optimizes deduplication and stable references by content hash,\n- artifact storage optimizes append-only session tooling and human/tool retrieval by local IDs.\n\n## Storage boundaries and on-disk layout\n\n## Blob store boundary (global)\n\n`SessionManager` constructs `BlobStore(getBlobsDir())`, so blob files live in a shared global blob directory (not in a session folder).\n\nBlob file naming:\n\n- file path: `<blobsDir>/<sha256-hex>`\n- no extension\n- reference string stored in entries: `blob:sha256:<sha256-hex>`\n\nImplications:\n\n- same binary content across sessions resolves to the same hash/path,\n- writes are idempotent at the content level,\n- blobs can outlive any individual session file.\n\n## Artifact boundary (session-local)\n\n`ArtifactManager` derives artifact directory from session file path:\n\n- session file: `.../<timestamp>_<sessionId>.jsonl`\n- artifacts directory: `.../<timestamp>_<sessionId>/` (strip `.jsonl`)\n\nArtifact types share this directory:\n\n- truncated tool output files: `<numericId>.<toolType>.log` (for `artifact://`)\n- subagent output files: `<outputId>.md` (for `agent://`)\n\n## Resident-text cache boundary (profile-local, not an artifact)\n\nResident text that is externalized only to keep a live session's memory bounded is not a durable blob and is never part of a session artifact directory, copy manifest, fork, or move.\n\nOn supported POSIX hosts, its private root is derived from the session destination's logical profile agent directory (`getResidentCacheRootDir(profileAgentDir)`). The default profile retains the normal XDG cache routing; SDK/custom profiles receive an isolated `<agentDir>/resident-cache` root. The cache-owned root and all active instance directories are owner-only and verified before use.\n\nEach disk-backed resident-store candidate receives a new `i-<nonce>` directory beneath that root. Before its first blob write, it receives a 0600 `owner.json` lease containing its owning PID, process start time (`startTimeMs` when obtainable), and nonce; the directory is 0700. `SessionManager` owns this directory through the resident-store transition seam: `#prepareResidentTextStoreTransition` creates and populates a candidate without changing the installed session, then `#commitResidentTextStoreTransition` swaps the completed store and disposes the predecessor last.\n\nWindows deliberately takes no disk-backed resident-cache path: it installs `MemoryBlobStore`, increments `residentCacheWin32FallbackCount`, and does not create the profile cache root or an instance directory.\n\nOpening a verified POSIX cache root schedules a fire-and-forget lease sweep. A pass re-verifies the root, examines at most 64 `i-*` siblings for no more than 250 ms, and only reaps a dead PID or a provably PID-reused lease. It re-reads the exact owner token before action, quarantine-renames the stale directory with a fresh nonce, then removes that quarantined tree with an `lstat`/no-follow walk so planted symlinks cannot escape the cache boundary.\n\n## ID and name allocation schemes\n\n## Blob IDs: content hash\n\n`BlobStore.put()` computes SHA-256 over the bytes it is given and returns:\n\n- `hash`: hex digest,\n- `path`: `<blobsDir>/<hash>`,\n- `ref`: `blob:sha256:<hash>`.\n\nNo session-local counter is used.\n\n## Artifact IDs: session-local monotonic integer\n\n`ArtifactManager` scans existing `*.log` artifacts and hidden `.artifact-id-{id}` claims on first use to find the next numeric candidate. Every allocation atomically publishes its claim before exposing the ID; a competing manager or process that loses the no-replace publication retries the next candidate. Claims remain with the artifact root, so abandoned path reservations consume an ID instead of allowing later reuse or ambiguous resolution.\n\nAllocation behavior:\n\n- file format: `{id}.{toolType}.log`\n- claim format: `.artifact-id-{id}`\n- IDs are sequential strings (`\"0\"`, `\"1\"`, ...) when uncontended; collisions can leave safe gaps,\n- resume and same-root multi-manager allocation do not overwrite or create duplicate numeric IDs because claims are scanned and atomically published.\n\nIf the artifact directory is missing, scanning yields empty state and allocation first attempts `0`.\n\n## Agent output IDs (`agent://`)\n\n`AgentOutputManager` allocates IDs for subagent outputs as `<index>-<requestedId>` (optionally nested under parent prefix, e.g. `0-Parent.1-Child`). It scans existing `.md` files on initialization to continue from the next index on resume.\n\nA subagent adopts its parent's `ArtifactManager` (`SessionManager.adoptArtifactManager`), so the whole agent tree — including nested subagents whose own session file lives inside the shared root — writes `<outputId>.md` into one directory and one ID space. The task tool accepts that manager only when the live `ToolSession` proves the exact manager relationship through `isArtifactManagerAuthorized`; `SessionManager` authorizes only its current created, ephemeral, or explicitly adopted manager by object identity. Pathname or session-file containment is never authority, and unrelated or cross-session manager instances are rejected even when their paths are lexically nested.\n\n## Persistence dataflow\n\n## 1) Session entry persistence rewrite path\n\nBefore session entries are written (`#rewriteFile` / incremental persist), `SessionManager` calls `prepareEntryForPersistence()` (via `truncateForPersistence`).\n\nKey behaviors:\n\n1. **Large string truncation**: oversized strings are cut and suffixed with `\"[Session persistence truncated large content]\"`; signature fields (`thinkingSignature`, `thoughtSignature`, `textSignature`) are cleared instead of truncated.\n2. **Transient field stripping**: `partialJson` and `jsonlEvents` are removed from persisted entries.\n3. **Image externalization to blobs**:\n - image blocks in `content` arrays are externalized when `data` is not already a blob ref and base64 length is at least threshold (`BLOB_EXTERNALIZE_THRESHOLD = 1024`),\n - provider-style `image_url` data URLs are externalized when they start with `data:image/` and contain `;base64,`,\n - image block `data` is stored as decoded binary bytes,\n - provider data URLs are stored as the original UTF-8 data URL string,\n - persisted values are replaced with `blob:sha256:<hash>`.\n\nThis keeps session JSONL compact while preserving recoverability.\n\n## 2) Session load rehydration path\n\nWhen opening a session (`setSessionFile`), after migrations, `SessionManager` runs `resolveBlobRefsInEntries()`.\n\nFor message/custom-message image blocks with `blob:sha256:<hash>` and for persisted provider `image_url` fields with blob refs:\n\n- reads blob bytes from blob store,\n- converts image-block bytes back to base64,\n- converts provider `image_url` blobs back to the original string,\n- mutates in-memory entry fields for runtime consumers.\n\nIf blob is missing:\n\n- `resolveImageData()` logs warning,\n- returns original ref string unchanged,\n- load continues (no hard crash).\n\n## 3) Tool output spill/truncation path\n\n`OutputSink` powers streaming output in bash/python/ssh and related executors.\n\nBehavior:\n\n1. Every chunk is sanitized and appended to in-memory tail buffer.\n2. When in-memory bytes exceed spill threshold (`DEFAULT_MAX_BYTES`, 50KB), sink marks output truncated.\n3. If an artifact path is available, sink opens a file writer and writes:\n - existing buffered content once,\n - all subsequent chunks.\n4. In-memory buffer is always trimmed to tail window for display.\n5. `dump()` returns summary including `artifactId` only when file sink was successfully created.\n\nPractical effect:\n\n- UI/tool return shows truncated tail,\n- full output is preserved in artifact file and referenced as `artifact://<id>`.\n\nIf file sink creation fails (I/O error, missing path, etc.), sink silently falls back to in-memory truncation only; full output is not persisted.\n\n## URL access model\n\n## `blob:` references\n\n`blob:sha256:<hash>` is a persistence reference inside session entry payloads, not an internal URL scheme handled by the router. Resolution is done by `SessionManager` during session load.\n\n## `artifact://<id>`\n\nHandled by `ArtifactProtocolHandler`:\n\n- requires active session artifact directory,\n- ID must be numeric,\n- resolves by matching filename prefix `<id>.`,\n- returns raw text (`text/plain`) from the matched `.log` file,\n- when missing, error includes list of available artifact IDs.\n\nMissing directory behavior:\n\n- if artifacts directory does not exist, throws `No artifacts directory found`.\n\n## `agent://<id>`\n\nHandled by `AgentProtocolHandler` over `<artifactsDir>/<id>.md`:\n\n- plain form returns markdown text,\n- `/path` or `?q=` forms perform JSON extraction,\n- path and query extraction cannot be combined,\n- if extraction requested, file content must parse as JSON.\n\nMissing directory behavior:\n\n- throws `No artifacts directory found`.\n\nMissing output behavior:\n\n- throws `Not found: <id>` with available IDs from existing `.md` files.\n\nRead tool integration:\n\n- `read` supports offset/limit pagination for non-extraction internal URL reads,\n- rejects `offset/limit` when `agent://` extraction is used.\n\n## Resume, fork, and move semantics\n\n## Resume\n\n- `ArtifactManager` scans existing `{id}.*.log` files on first allocation and continues numbering.\n- `AgentOutputManager` scans existing `.md` output IDs and continues numbering.\n- `SessionManager` rehydrates blob refs to base64 on load.\n\n## Fork\n\n`SessionManager.fork()` creates a new session file with new session ID and `parentSession` link, then returns old/new file paths. Artifact copying is handled by `AgentSession.fork()`:\n\n- attempts recursive copy of old artifact directory to new artifact directory,\n- missing old directory is tolerated,\n- non-ENOENT copy errors are logged as warnings and fork still completes.\n\nID implications after fork:\n\n- if copy succeeded, artifact counters in new session continue after max copied ID,\n- if copy failed/skipped, new session artifact IDs start from `0`.\n\nBlob implications after fork:\n\n- blobs are global and content-addressed, so no blob directory copy is required.\n\n## Move to new cwd\n\n`SessionManager.moveTo()` renames both session file and artifact directory to the new default session directory, with rollback logic if a later step fails. This preserves artifact identity while relocating session scope.\n\n## Failure handling and fallback paths\n\n| Case | Behavior |\n| -------------------------------------------------------- | --------------------------------------------------------------------- |\n| Blob file missing during rehydration | Warn and keep `blob:sha256:` ref string in-memory |\n| Blob read ENOENT via `BlobStore.get` | Returns `null` |\n| Artifact directory missing (`ArtifactManager.listFiles`) | Returns empty list (allocation can start fresh) |\n| Artifact directory missing (`artifact://` / `agent://`) | Throws explicit `No artifacts directory found` |\n| Artifact ID not found | Throws with available IDs listing |\n| OutputSink artifact writer init fails | Continues with tail-only truncation (no full-output artifact) |\n| No session file (some task paths) | Task tool falls back to temp artifacts directory for subagent outputs |\n| Non-persistent session (`persist=false`) | `saveArtifact` lazily creates a temp artifact directory; content is read back from disk, never retained in memory |\n\n## Binary blob externalization vs text-output artifacts\n\n- **Blob externalization** is for image payloads inside persisted session entry content and provider image data URLs; it replaces inline payload strings in JSONL with stable content refs.\n- **Artifacts** are plain text files for execution output and subagent output; they are addressable by session-local IDs through internal URLs.\n\nThe two systems intersect only indirectly (both reduce session JSONL bloat) but have different identity, lifetime, and retrieval paths.\n\n## Implementation files\n\n- [`src/session/blob-store.ts`](../packages/coding-agent/src/session/blob-store.ts) — blob references, verified resident-cache instance leases, bounded GC, hashing, put/get, and externalize/resolve helpers.\n- [`src/session/artifacts.ts`](../packages/coding-agent/src/session/artifacts.ts) — session artifact directory model and numeric artifact ID/path allocation.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink` truncation/spill-to-file behavior and summary metadata.\n- [`src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts) — persistence transforms, resident-store prepare/commit ownership, blob rehydration on load, and session fork/move interactions.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — artifact directory copy during interactive fork.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://` resolver.\n- [`src/internal-urls/agent-protocol.ts`](../packages/coding-agent/src/internal-urls/agent-protocol.ts) — `agent://` resolver + JSON extraction.\n- [`src/sdk/session.ts`](../packages/coding-agent/src/sdk/session.ts) — internal URL router wiring and artifacts-dir resolver.\n- [`src/task/output-manager.ts`](../packages/coding-agent/src/task/output-manager.ts) — session-scoped agent output ID allocation for `agent://`.\n- [`src/task/executor.ts`](../packages/coding-agent/src/task/executor.ts) — subagent output artifact writes (`<id>.md`) and temp artifact directory fallback.\n",
|
|
@@ -53,7 +53,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
53
53
|
"macos-option-key.md": "# macOS + iTerm2 Option/Alt key setup for GJC\n\nHow to make the macOS Option key reach GJC as Alt/Meta input instead of producing composed characters like `œ` or `ˆ`.\n\n## iTerm2 settings\n\nIn **Settings → Profiles → Keys → General** for the profile you use:\n\n- Left Option Key: `+Esc`\n- Right Option Key: `+Esc`\n\nA verified live configuration sets `Option Key Sends = 2` and `Right Option Key Sends = 2` for both the `Default` and `tmux` profiles. iTerm2's `+Esc` setting (`OPTION_KEY_ESC = 2`) prepends ESC to Option input, delivering `Option+Q` as `ESC q` and `Option+I` as `ESC i`. Value `1` is Meta mode and is not an ESC prefix.\n\nIn **Settings → Profiles → Keys → Key Mappings**, remove any `Send Text`, `Send Escape Sequence`, or other mappings that intercept `Option+Q` or `Option+I`, then open a new session for each profile after changing settings.\n\n## macOS input source\n\nSwitch the active keyboard layout to `ABC` when typing GJC Alt commands. `scripts/verify-option-key.sh` does not merely check that ABC is in the list; it verifies `AppleCurrentKeyboardLayoutInputSourceID = com.apple.keylayout.ABC` together with ABC in the selection list.\n\n## Verification\n\n```sh\n./scripts/verify-option-key.sh\npython3 scripts/capture-option-key.py\n```\n\nThe verify script checks the `Default` and `tmux` profiles, both Option keys, Option+Q/I mapping conflicts in both physical-keycode (`Q=12`, `I=34`) and character-code (`q=0x71`, `i=0x69`) form, the active ABC layout, Bun, and the GJC smoke test.\n\nUse the capture tool in a real TTY. In raw mode, Ctrl-C arrives as `0x03` rather than `KeyboardInterrupt`; the tool detects it, restores the terminal, and exits. Confirm with physical key presses in fresh Default and tmux sessions that `Option+Q` and `Option+I` arrive as `ESC q` and `ESC i` and trigger the GJC commands.\n\nRelated live and fixture verification evidence is recorded in `artifacts/option-key-verification.json`.\n",
|
|
54
54
|
"memory.md": "# Autonomous Memory\n\nWhen enabled, the agent automatically extracts durable knowledge from past sessions and injects a compact summary into each new session. Over time it builds a project-scoped memory store — technical decisions, recurring workflows, pitfalls — that carries forward without manual effort.\n\nDisabled by default. Enable via `/settings` or `config.yml`:\n\n```yaml\nmemories:\n enabled: true\n```\n\n## Usage\n\n### What gets injected\n\nAt session start, if a memory summary exists for the current project, it is injected into the system prompt as a **Memory Guidance** block. The agent is instructed to:\n\n- Treat memory as heuristic context — useful for process and prior decisions, not authoritative on current repo state.\n- Pair memory-influenced decisions with current-repo evidence before acting.\n- Prefer repo state and user instruction when they conflict with memory; treat conflicting memory as stale.\n\n### Memory artifacts\n\nGenerated local-memory artifacts are private runtime state, not a public tool or URI surface. They may be summarized into the system prompt when local memory is enabled, but users and model-facing tool docs should not rely on direct `memory://` reads. The legacy internal `memory://` resolver remains only for compatibility with existing persisted guidance and is not part of the public coding harness contract; remove it after legacy local-memory prompts no longer reference it.\n### `/memory` slash command\n\n| Subcommand | Effect |\n| --------------------- | ---------------------------------------------- |\n| `view` | Show the current memory injection payload |\n| `clear` / `reset` | Delete all memory data and generated artifacts |\n| `enqueue` / `rebuild` | Force consolidation to run at next startup |\n\n## How it works\n\nMemories are built by a background pipeline that runs at startup or when manually triggered via slash command.\n\n**Phase 1 — per-session extraction:** For each past session that has changed since it was last processed, a model reads the session history and extracts durable signal: technical decisions, constraints, resolved failures, recurring workflows. Sessions that are too recent, too old, or currently active are skipped. Each extraction produces a raw memory block and a short synopsis for that session.\n\n**Phase 2 — consolidation:** After extraction, a second model pass reads all per-session extractions and produces three outputs written to disk:\n\n- `MEMORY.md` — a curated long-term memory document\n- `memory_summary.md` — the compact text injected at session start\n- `skills/` — reusable procedural playbooks, each in its own subdirectory\n\nPhase 2 uses a lease to prevent double-running when multiple processes start simultaneously. Stale skill directories from prior runs are pruned automatically.\n\nAll output is scanned for secrets before being written to disk.\n\n### Extraction behavior\n\nMemory extraction and consolidation behavior is driven by static prompt files in `packages/coding-agent/src/prompts/memories/`.\n\n| File | Purpose | Variables |\n| --------------------- | ------------------------------------------- | ------------------------------------------- |\n| `stage_one_system.md` | System prompt for per-session extraction | — |\n| `stage_one_input.md` | User-turn template wrapping session content | `{{thread_id}}`, `{{response_items_json}}` |\n| `consolidation.md` | Prompt for cross-session consolidation | `{{raw_memories}}`, `{{rollout_summaries}}` |\n| `read_path.md` | Memory guidance injected into live sessions | `{{memory_summary}}` |\n\n### Model selection\n\nMemory piggybacks on the model role system.\n\n| Phase | Role | Purpose |\n| ----------------------- | ------------------------------------------------------------------- | -------------------------------- |\n| Phase 1 (extraction) | `default` | Per-session knowledge extraction |\n| Phase 2 (consolidation) | `smol` (falls back to `default`, then current/first registry model) | Cross-session synthesis |\n\nIf the requested memory role is not configured, memory model resolution falls back to the `default` role, then the active session model, then the first model in the registry.\n\n## Configuration\n\n| Setting | Default | Description |\n| ------------------------------------- | ------- | --------------------------------------------------------- |\n| `memories.enabled` | `false` | Master switch |\n| `memories.maxRolloutAgeDays` | `30` | Sessions older than this are not processed |\n| `memories.minRolloutIdleHours` | `12` | Sessions active more recently than this are skipped |\n| `memories.maxRolloutsPerStartup` | `64` | Cap on sessions processed in a single startup |\n| `memories.summaryInjectionTokenLimit` | `5000` | Max tokens of the summary injected into the system prompt |\n\nAdditional tuning knobs (concurrency, lease durations, token budgets) are available in config for advanced use.\n\n## Key files\n\n- `packages/coding-agent/src/memories/index.ts` — pipeline orchestration, injection, slash command handling\n- `packages/coding-agent/src/memories/storage.ts` — SQLite-backed job queue and thread registry\n- `packages/coding-agent/src/prompts/memories/` — memory prompt templates\n- `packages/coding-agent/src/internal-urls/memory-protocol.ts` — legacy non-public `memory://` compatibility handler\n",
|
|
55
55
|
"models.md": "# Model and Provider Configuration (`models.yml`)\n\nThis document describes how the coding-agent currently loads models, applies overrides, resolves credentials, and chooses models at runtime.\n\n## What controls model behavior\n\nPrimary implementation files:\n\n- `src/config/model-registry.ts` — loads embedded + signed-registry + custom models, provider overrides, runtime discovery, auth integration\n- `src/config/model-resolver.ts` — parses model patterns and selects models for the default and agent roles\n- `src/config/settings-schema.ts` — model-related settings (`modelRoles`, provider transport preferences)\n- `src/session/auth-storage.ts` — API key + OAuth resolution order\n- `packages/ai/src/models.ts` and `packages/ai/src/types.ts` — built-in providers/models and `Model`/`compat` types\n\n## Config file location and legacy behavior\n\nDefault config path:\n\n- `~/.gjc/agent/models.yml`\n\nLegacy behavior still present:\n\n- If `models.yml` is missing and `models.json` exists at the same location, it is migrated to `models.yml`.\n- Explicit `.json` / `.jsonc` config paths are still supported when passed programmatically to `ModelRegistry`.\n\n## `models.yml` shape\n\n```yaml\nproviders:\n <provider-id>:\n # provider-level config\nequivalence:\n overrides:\n <provider-id>/<model-id>: <canonical-model-id>\n exclude:\n - <provider-id>/<model-id>\n```\n\n`provider-id` is the canonical provider key used across selection and auth lookup.\n\n`equivalence` is optional and configures canonical model grouping on top of concrete provider models:\n\n- `overrides` maps an exact concrete selector (`provider/modelId`) to an official upstream canonical id\n- `exclude` opts a concrete selector out of canonical grouping\n\n## Provider-level fields\n\n```yaml\nproviders:\n my-provider:\n baseUrl: https://api.example.com/v1\n apiKey: MY_PROVIDER_API_KEY\n api: openai-completions\n headers:\n X-Team: platform\n authHeader: true\n auth: apiKey\n disableStrictTools: false # set true for Anthropic-compatible endpoints that reject the strict field\n cacheRetention: short # none | short | long; model entries and modelOverrides can override this\n discovery:\n type: ollama\n modelOverrides:\n some-model-id:\n name: Renamed model\n cacheRetention: long\n models:\n - id: some-model-id\n name: Some Model\n api: openai-completions\n reasoning: false\n input: [text]\n cost:\n input: 0\n output: 0\n cacheRead: 0\n cacheWrite: 0\n contextWindow: 128000\n maxTokens: 16384\n headers:\n X-Model: value\n cacheRetention: none\n thinking:\n minLevel: low\n maxLevel: xhigh\n mode: effort\n defaultLevel: high\n levels: [low, medium, high, xhigh]\n compat:\n supportsStore: true\n supportsDeveloperRole: true\n supportsReasoningEffort: true\n maxTokensField: max_completion_tokens\n openRouterRouting:\n only: [anthropic]\n vercelGatewayRouting:\n order: [anthropic, openai]\n extraBody:\n gateway: m1-01\n controller: mlx\nmodelBindings:\n modelRoles:\n default: my-provider/some-model-id:high\n agentModelOverrides:\n executor: my-provider/some-model-id\n```\n\n### Allowed provider/model `api` values\n\n- `openai-completions`\n- `openai-responses`\n- `openai-codex-responses`\n- `azure-openai-responses`\n- `bedrock-converse-stream`\n- `anthropic-messages`\n- `google-generative-ai`\n- `google-vertex`\n- `google-gemini-cli`\n- `ollama-chat`\n- `cursor-agent`\n\n\n### First-class DeepInfra, Azure OpenAI, and Amazon Bedrock examples\n\nAzure OpenAI uses canonical OpenAI model IDs in GJC and resolves those IDs to Azure deployment names at request time. Set `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` to avoid assuming model id equals deployment name:\n\n```yaml\nproviders:\n azure-openai:\n baseUrl: https://my-resource.openai.azure.com/openai/v1\n apiKeyEnv: AZURE_OPENAI_API_KEY\n api: azure-openai-responses\n models:\n - id: gpt-4.1\n - id: o3\n```\n\n```sh\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP='gpt-4.1=gpt-41-prod,o3=o3-reasoning-prod'\n```\n\nDeepInfra is available as the first-class `deepinfra` provider. It uses DeepInfra's OpenAI-compatible Chat Completions endpoint and reads `DEEPINFRA_API_KEY` when no explicit config key is provided. Set `serviceTier: priority` in GJC config or use the runtime service-tier controls to send DeepInfra's `service_tier: \"priority\"` request field for supported models:\n\n```yaml\nproviders:\n deepinfra:\n baseUrl: https://api.deepinfra.com/v1/openai\n apiKeyEnv: DEEPINFRA_API_KEY\n api: openai-completions\n models:\n - id: deepseek-ai/DeepSeek-V3.2\n```\n\n#### `/fast` provider support\n\n`/fast on` only shows `⚡` when GJC will put a fast/priority field on the selected provider's wire request:\n\n| Provider ID | Wire request | Notes |\n|---|---|---|\n| `openai` | `service_tier: \"priority\"` | OpenAI renamed Priority processing to [Fast mode](https://developers.openai.com/api/docs/guides/fast-mode); `priority` remains an accepted alias. For API-key requests, the response `service_tier` reports the tier actually used and may be `default` after a ramp-rate downgrade. |\n| `openai-codex` | `service_tier: \"priority\"` | ChatGPT-authenticated Codex handles Fast through server-side routing. A final response value of `service_tier: \"default\"` does not show that Fast was ignored or downgraded. |\n| `anthropic` | `speed: \"fast\"` plus `fast-mode-2026-02-01` beta | Direct Claude API only. Anthropic's [Fast mode](https://platform.claude.com/docs/en/build-with-claude/fast-mode) is model- and account-gated; unsupported or unavailable requests can fall back after a provider rejection. Bedrock, Vertex, and Microsoft Foundry do not support it. |\n| `deepinfra` | `service_tier: \"priority\"` | Sent only for the first-class `deepinfra` provider ID and only with the `priority` tier. |\n| `opencodex` | `service_tier: \"priority\"` | First-class OpenCodex discovery opts in automatically; OpenCodex Fast Mode must remain `Auto` for client passthrough. When OpenCodex uses ChatGPT authentication, a final `service_tier: \"default\"` is not downgrade evidence. |\n\nCustom OpenAI-compatible providers remain fail-closed unless their provider or model configuration explicitly sets `compat.supportsServiceTier: true`. Use that opt-in only when the proxy preserves or intentionally realizes OpenAI's `service_tier` contract:\n\n```yaml\nproviders:\n my-openai-proxy:\n baseUrl: http://proxy.example/v1\n api: openai-responses\n compat:\n supportsServiceTier: true\n```\n\nWithout that capability, `/fast status` shows `off` even when the session retains an unscoped `priority` intent. The `⚡` indicator means that GJC sends the provider's fast request field. API-key providers may report a downgrade in their response; ChatGPT-authenticated Codex and OpenCodex route Fast server-side and cannot be verified from the final `service_tier` value.\n\nAmazon Bedrock uses the native `bedrock-converse-stream` transport and AWS credential chain auth. Do not put AWS access keys in `models.yml`; configure `AWS_REGION` / `AWS_PROFILE` or standard static AWS credential environment variables instead:\n\n```yaml\nproviders:\n amazon-bedrock:\n baseUrl: https://bedrock-runtime.us-east-1.amazonaws.com\n api: bedrock-converse-stream\n models:\n - id: us.anthropic.claude-opus-4-6-v1\n - id: anthropic.claude-3-5-sonnet-20241022-v2:0\n```\n\n### Coding-plan provider presets\n\nFor supported coding-plan providers, prefer presets so the API type, base URL, environment variable, model catalog, discovery behavior, and compatibility flags are written together:\n\n```sh\ngjc setup provider --preset minimax\ngjc setup provider --preset minimax-cn\ngjc setup provider --preset glm\ngjc setup provider --preset alibaba-token-plan\ngjc setup provider --preset cline-pass\ngjc setup provider --preset commandcode-goat\n```\n\nThe same presets are available inside the TUI:\n\n```text\n/provider add --preset minimax\n/provider add --preset glm\n/provider add zai\n/provider add --preset alibaba-token-plan\n/provider add --preset cline-pass\n/provider add --preset commandcode-goat\n```\n\nPresets only write `models.yml` entries that reference documented environment variable names (`MINIMAX_CODE_API_KEY`, `MINIMAX_CODE_CN_API_KEY`, `ZAI_API_KEY`, `ALIBABA_TOKEN_PLAN_API_KEY`, `CLINE_API_KEY`, or `CMD_API_KEY`); they do not store or validate real credentials. The GLM preset aliases (`glm`, `zai`, `z-ai`) write an OpenAI-compatible custom provider named `glm-proxy` and do not replace the first-class `zai` provider. The Alibaba Token Plan preset (aliases: `alibaba`, `token-plan`) writes an OpenAI-compatible custom provider named `alibaba-token-plan` with per-model API routing. The ClinePass preset (aliases: `clinepass`, `cline`) does not hardcode models: Cline's inference API has no working `/models` route, so GJC follows Cline's own catalog-generation source and fetches the live `cline-pass` provider catalog from `https://models.dev/api.json`. The Command Code GOAT preset (aliases: `commandcode`, `command-code`, `goat`) fetches its live `/provider/v1/models` catalog, keeps every current or future model—including Claude-named IDs—on the provider's documented OpenAI-compatible `/chat/completions` transport, and requires a fixed harmless inference entitlement probe before login persistence. Create the corresponding API key in the provider dashboard before inference; plan entitlement is enforced by the provider.\n\n## Signed remote preset registry\n\nGJC ships its embedded model metadata and profiles as an immutable bootstrap fallback, then overlays a separately published signed registry before applying local configuration:\n\n1. embedded model presets and profiles\n2. the last accepted `Yeachan-Heo/gajae-code-presets` registry snapshot\n3. user `~/.gjc/agent/models.yml` entries and overrides\n\nLocal user configuration always wins. Registry refresh never writes `models.yml`, and a failed, partial, oversized, incompatible, downgraded, equivocated, digest-mismatched, or untrusted update never replaces the active snapshot. Startup reads only the verified local cache and does not wait for network I/O; a delayed best-effort refresh runs at a bounded cadence. Offline cold starts use embedded data, while offline warm starts use the last-known-good accepted snapshot.\n\nThe registry manifest is canonical JSON signed with a compiled Ed25519 trust root. Signatures are 64-byte Ed25519 values encoded as canonical RFC 4648 Base64; unused padding bits must be zero, so two encodings of the same signature cannot produce distinct anti-equivocation digests. The signed payload binds the monotonic revision, consumer-contract compatibility, immutable revision paths, exact byte counts, SHA-256 digests, source commit provenance, snapshot, profile data, and credential-free model metadata. GJC sends no cookies, authorization headers, API keys, or provider credentials when fetching it. Registry schemas do not permit endpoints, request headers, credentials, environment references, commands, scripts, or arbitrary executable content. Selectors, preset identifiers, display text, and context-promotion targets reject Unicode format controls. Registry provider IDs are slash-free (`[a-z0-9][a-z0-9._-]*`), matching profile `requiredProviders` and the first-slash selector boundary used at runtime.\n\nAdministrative commands:\n\n```sh\ngjc models presets status [--json]\ngjc models presets refresh [--json]\ngjc models presets rollback <accepted-revision> [--json]\ngjc models presets pin <accepted-revision> [--json]\ngjc models presets unpin [--json]\ngjc models presets disable [--json]\ngjc models presets enable [--json]\n```\n\n`status` reports deterministic, credential-free provenance: active and highest-seen revisions, manifest/snapshot/profile/preset digests, signature key id, source GJC commit, accepted/published/check timestamps, retained removed entries, cache health, history, and pin/disable state. Rollback and pin can select only previously verified retained revisions; neither lowers the highest-seen anti-rollback floor, and selected generations are protected from bounded-history eviction. A rollback remains selected across background refreshes until another rollback/pin is chosen or `unpin` returns selection to the highest accepted revision. When a registry revision removes a profile, GJC retains that profile plus only the removed model metadata and dynamic-provider declarations it references so an existing default/current selection remains usable without unboundedly copying the whole prior catalog.\n\nThe cache and control files live under `~/.gjc/agent/model-presets/` (respecting `GJC_CODING_AGENT_DIR`). Writes use an interprocess lock, file fsync, and atomic rename. POSIX filesystems also receive a parent-directory durability barrier; Windows does not expose an equivalent directory fsync through Bun/Node, so it retains the file-fsync + atomic-rename guarantee with weaker crash durability for the renamed directory entry. `GJC_MODEL_PRESET_REGISTRY_URL` may override the manifest URL only with credential-free HTTPS; the trust root cannot be replaced at runtime. `GJC_MODEL_PRESET_REGISTRY_DISABLED=1` provides a non-destructive environment disable.\n\n## Model profiles (`--mpreset`)\n\nModel profiles are optional top-level `profiles:` entries in `~/.gjc/agent/models.yml`. A profile can require provider credentials before activation and can map one or more model roles; omitted roles inherit from the active defaults.\n\n> See also: [Cross-vendor role-based profiles](./multi-vendor-profiles.md) — a curated multi-vendor `profiles:` recipe and verified selector notes that build on the mechanism described here.\n\n```yaml\nprofiles:\n team-standard:\n required_providers: [openai, anthropic]\n model_mapping:\n default: openai/gpt-5.2\n executor: anthropic/claude-sonnet-5:medium\n architect: openai/o3:high\n planner: openai/o3:high\n critic: openai/o3:high\n```\n\n`model_mapping` keys are role names (`default`, `executor`, `architect`, `planner`, `critic`). Every role accepts either one selector or a non-empty ordered array of selectors; the first entry is primary and later entries are fallback candidates. A selector may be a provider-agnostic bare alias such as `glm-5.2[:effort]` or an explicit `provider/modelId[:effort]` pin, including nested model IDs such as `openrouter/anthropic/claude-sonnet-5`. `required_providers` lists explicit provider prerequisites and may be empty when availability is resolved from bare aliases.\n\n### Fallback chains\n\nPreset `model_mapping` roles, top-level `modelRoles`, and `task.agentModelOverrides` all accept `string | string[]`. Keep one selector per line when a chain needs to be readable:\n\n```yaml\nprofiles:\n reliable:\n required_providers: [anthropic, openai]\n model_mapping:\n default: [anthropic/claude-sonnet-4-5, openai/gpt-4o-mini]\nmodelBindings:\n modelRoles:\n default: [anthropic/claude-sonnet-4-5, openai/gpt-4o-mini]\n agentModelOverrides:\n executor: [anthropic/claude-sonnet-4-5, openai/gpt-4o-mini]\n```\n\nResolution-time skips for unavailable, unauthenticated, or unknown entries cost zero attempts and advance immediately. Only request-time retryable failures (such as 429, quota, authentication, or 5xx failures) consume an entry's `fallback.maxAttempts` total attempts (default: `3`). The active default fallback remains sticky for the session; role-override fallback state is fresh for each subagent call. The active model is shown consistently in status and `/model`.\n\nManaged fallback attempts buffer provisional streamed output until an attempt is accepted, so output can appear later than it does for a one-model stream. Current Cursor-agent transports are fail-closed unavailable in retryable fallback chains: resolution rejects them with `Cursor model <selector> requires provider-side tool execution and cannot be used in a retryable fallback chain` because they do not provide a client-side tool-call mode.\n\nCancellation discards provisional output and emits exactly one cancelled `agent_end`; RPC, ACP, and the TUI therefore settle once. On load, the source-aware one-shot migration reads legacy `retry.fallbackChains`, prepends the effective role chain, and writes the ordered, deduplicated result to the corresponding role array; the legacy key is then ignored.\n\nBuilt-in profiles are grouped by provider mix and tier:\n\n- `codex-{eco,medium,pro}` — GPT-5.6 Sol/Terra/Luna role mixes tuned by tier and reasoning effort; `lunamaxxing` — OpenAI Codex Luna-only profile with maximum reasoning on delegated roles\n- `opencodego` — single OpenCode Go preset (Kimi K3 default and planner, DeepSeek executor/architect, MiMo critic)\n- `commandcode-goat` — Command Code GOAT preset (GLM-5.3 default, DeepSeek V4 Flash executor, Kimi K3 planner, GLM-5.2 critic, and DeepSeek V4 Pro architect)\n- Provider-agnostic open-model profiles are named by the model families they require. Single-family choices are `open-weights-{glm,deepseek,kimi,luna}`; two-family choices are `open-weights-glm-deepseek`, `open-weights-kimi-deepseek`, and `open-weights-kimi-glm`; `open-weights-kimi-glm-deepseek` uses all three open-weight families; `open-weights-all` adds GPT-5.6 Luna. Choose the smallest combination covered by the models available through your configured providers. Every selector is a bare final-segment alias with `required_providers: []`, so each family may come from any authenticated bundled or custom provider under Provider Priority. GPT-5.6 Luna is proprietary despite its inclusion in this group.\n- `macos-omlx-{fast,balanced,quality}` — oMLX presets for local Apple Silicon inference, tuned by measured same-machine throughput. `fast` pins the 4-bit and `balanced` the 8-bit quant of Qwen 3.6 35B A3B; `quality` keeps the 8-bit MoE for the default, executor, planner, and architect roles and routes the critic to the official dense `Qwen3.8-27B-8bit` checkpoint; `macos-omlx-abliterated-{fast,balanced}` both pin `Qwen3.8-27B-Uncensored-MLX-4bit`, the faster of the measured uncensored quants. Every preset uses one role-effort ladder — critic and architect `high`, planner `medium`, executor and default `low` — and `fast`, `balanced`, and the abliterated presets serve a single model, so sub-agents never trigger an oMLX model unload/reload; `quality` swaps models only for the critic role. Selectors use the ids the local server returns from `/v1/models`; activate after starting oMLX on its default loopback endpoint (see [Implicit oMLX discovery](#implicit-omlx-discovery)) — the provider is keyless, so no `/login` is needed.\n- `claude-opus` — Anthropic OAuth preset that prefers `claude-opus-5` and deterministically falls back to `claude-opus-4-6` when Opus 5 is absent from the active catalog\n- `claude-fable` — Anthropic OAuth preset on `claude-fable-5-1` (default/architect `xhigh`, critic `high`, planner `low`) with `claude-sonnet-5` on the executor role. Fable 5.1 is served only by Anthropic's first-party API; it requires a Claude Code compatibility version of at least `2.1.251` and answers older fingerprints with an HTTP 400 `claude_code_version_too_old`.\n- Single-provider tiers: `glm-{eco,medium,pro}`, `kimi-coding-plan-{eco,medium,pro}`, `mimo-{eco,medium,pro}`, `grok-{eco,medium,pro}`, `grok-45-{eco,medium,pro}`, `grok-46-{eco,medium,pro}`, `cursor-{eco,medium,pro}`, `minimax-{eco,medium,pro}`. The versioned Grok profiles use the existing xAI OAuth/subscription provider: `/login xai` authenticates both versions. Direct `/model` assignment requires an explicit effort for `xai/grok-4.5` (`low`, `medium`, or `high`) and `xai/grok-4.6` (`low`, `medium`, `high`, or `xhigh`) instead of leaving the role at `(inherit)`.\n- Alibaba Token Plan: `alibaba-token-plan-balanced` preserves the established Qwen/DeepSeek V4 Pro/GLM mix; `alibaba-token-plan-pro` raises execution and independent criticism with DeepSeek V4 Flash 0731 max and GLM xhigh; `alibaba-token-plan-qwenmaxxing` stays Qwen-only; `alibaba-token-plan-qwen-deepseek` keeps Qwen 3.8 Max (`qwen3.8-max`) on the expensive default (high)/architect (xhigh)/critic (xhigh) roles and spends DeepSeek V4 Flash 0731 on the cheap planner (max) and executor (high) roles; `alibaba-token-plan-glm-deepseek` does the same with GLM 5.2 (`glm-5.2`) as the expensive model\n- Combos: `opus-codex`, `codex-opencodego`, and `fable-opus-codex`\n\nGLM-5.3 always enables thinking and accepts only `low`, `high`, and `max`; `max` is the provider default and is recommended for coding. The GLM tiers preserve the former role ordering by collapsing `minimal`/`low` to `low`, `medium`/`high` to `high`, and `xhigh` to `max`. GLM-5.3-Flash keeps the same text contract (per z.ai docs, its text parameters are consistent with GLM-5.3 with a 1M-token context window) and now backs the high-volume lanes of the GLM tiers: `glm-eco` runs default, executor, and planner on `glm-5.3-flash:low` while critic and architect stay on `glm-5.3:high`; `glm-medium` and `glm-pro` keep every role on `glm-5.3` except the executor, which uses `glm-5.3-flash:low` (Medium) and `glm-5.3-flash:high` (Pro).\n\nGemini 3.7 Flash is bundled wherever Gemini 3.6 Flash already was (`google/gemini-3.7-flash`, `google-gemini-cli/gemini-3.7-flash`, Copilot, Antigravity effort variants, OpenCode Zen, OpenRouter, Vercel AI Gateway, Cursor, and the other 3.6 Flash gateways). First-class Google transports use `google-level` thinking and accept only `low`, `medium`, and `high`; `minimal` is rejected because the official Gemini API returns an error. Provider defaults stay on the existing Pro-class models.\n\nThe `eco`, `medium`, and `pro` Codex profile mappings are current product judgments: Eco assigns Terra low/Luna low/Luna high/Terra xhigh/Terra high to default/executor/planner/critic/architect; Medium assigns Sol low/Terra low/Terra high/Sol xhigh/Sol high; Pro assigns Sol medium/Terra medium/Sol high/Sol max/Sol xhigh; and LunaMaxxing assigns Luna medium/Luna xhigh/Luna max/Luna max/Luna max. `opus-codex` retains the Medium Codex executor, critic, and architect roles but uses `anthropic/claude-sonnet-5` for planner; `codex-opencodego` retains the Medium Codex default and architect roles; and `fable-opus-codex` uses the Pro Codex executor and architect roles with `anthropic/claude-opus-5:medium` for planner. The descriptive repeated local exact-edit evidence informs only selected executor-style TypeScript tasks; it does not evaluate or prove default, planner, architect, or critic performance. See [GPT-5.6 Codex preset benchmark](./gpt-5.6-codex-preset-benchmark.md). The Alibaba Pro role evidence and its limits are recorded separately in [Alibaba Token Plan Pro profile benchmark](./alibaba-token-plan-pro-profile-benchmark.md). Cursor Eco uses Composer 2.5 for every role; Medium keeps standard Composer for default/planning and spends the Fast premium on execution, criticism, and architecture; Pro uses Composer 2.5 Fast throughout. Composer does not expose a strength value through the current Cursor RPC, so these profiles use exact model IDs without inert generic effort suffixes. See [Cursor Composer profile tiers](./cursor-composer-profile-tiers.md). Effort suffixes are clamped to each model's supported thinking range at preview and activation time. Single-provider tiers pin each provider's current flagship (`zai/glm-5.2`, `kimi-code/kimi-k2.7-code`, `xiaomi/mimo-v2.5-pro`, `xai/grok-4.3`, `cursor/composer-2.5`, `minimax-code/MiniMax-M3`). User-defined profiles override built-ins by exact profile name.\n\n\nUse `gjc --mpreset <name>` to activate a profile for the current session only. Activation hard-blocks when any provider listed in `required_providers` lacks credentials. Add `--default` to persist the selected profile as `modelProfile.default` in `config.yml`, so it applies at startup:\n\n```sh\ngjc --mpreset codex-medium\ngjc --mpreset opencodego --default\n```\n\n### Routing built-in presets through a proxy (`modelProfile.proxyProvider`)\n\nBuilt-in preset selectors pin a direct provider endpoint (`xai/grok-4.3`, `xiaomi/mimo-v2.5-pro`, …). To serve those models through your own OpenAI-compatible gateway (LiteLLM, OpenRouter, or a custom proxy) instead of each vendor's endpoint, configure the proxy provider id and routing mode in `config.yml`:\n\n```yaml\nmodelProfile:\n proxyProvider: litellm\n proxyMode: always # use fallback to keep directly authenticated providers direct\n```\n\nThe proxy provider is a normal `providers:` entry. Add it with `gjc setup provider --preset litellm --base-url <url>` or the generic `gjc setup provider --preset openai-compatible-proxy --base-url <url>` (both presets require `--base-url` and use live model discovery). The configured proxy must be authenticated and expose every routed model. Activation rewrites each selected built-in preset selector from `<direct-provider>/<model>` to `<proxy>/<direct-provider>/<model>` (for example `xai/grok-4.3` → `litellm/xai/grok-4.3`), matching the proxy's catalog entry for the model. The rules:\n\n- Routing applies to **built-in presets only**. User-defined `profiles:` entries always keep their exact selectors — set them explicitly if you want them proxied.\n- `proxyMode: fallback` (the default) routes only selectors whose direct provider is unauthenticated. `proxyMode: always` routes every proxy-routable built-in selector through the configured proxy, including selectors with direct credentials.\n- The proxy id must name a configured provider. `proxyMode: always` requires `proxyProvider` and a usable proxy credential; activation fails closed when a required proxy is unset or unauthenticated. `auth: none` proxies count as authenticated.\n- Only providers the bundled preset catalog treats as routable are rewritten; providers outside that set (for example a custom `acme-private`) keep the direct credential error.\n- A routed selector must have exactly one matching proxy catalog model. Exact `<direct-provider>/<model>` proxy ids win over suffix matches; missing or ambiguous matches fail activation before any role can run.\n\nThe `/model` command opens to a preset landing view: presets are grouped by provider with live auth marks (✓/✗), highlighting a group expands its tiers, and selecting a tier shows the full role→model preview before applying for the session or as default. Typing jumps straight to model search, and `Browse all models` opens the classic tabbed model selector. In `/login`, `Add custom provider` is the first option for configuring credentials needed by custom or profile-required providers; after a successful provider login, the matching preset is recommended automatically. Custom providers participate in provider-agnostic alias resolution but require manual preset selection.\n\nExternal SDK/ACP clients (e.g. the Paseo TUI) can select profiles like ordinary models: the SDK `models.list/current` (Q10) catalog exposes every usable profile as a synthetic `gajae-code/<profile>` entry (e.g. `gajae-code/codex-eco`), and selecting one through `model.set` (or the ACP Model picker) activates the profile for the live session only. Persisting a profile remains an explicit TUI choice, mirroring `gjc --mpreset <name> --default`. See [SDK model profiles](./sdk.md#model-profiles-as-synthetic-models-gajae-codeprofile).\n\nMiniMax's OpenAI-compatible endpoint rejects multiple system messages and emits thinking in `reasoning_content`, so pin the public-safe compatibility fields when hand-authoring a custom provider:\n\n```yaml\nproviders:\n minimax-custom:\n baseUrl: https://api.minimax.io/v1\n apiKeyEnv: MINIMAX_API_KEY\n api: openai-completions\n compat:\n supportsStore: false\n supportsDeveloperRole: false\n supportsReasoningEffort: false\n reasoningContentField: reasoning_content\n models:\n - id: MiniMax-M2.5\n```\n\nGLM via z.ai is available as the first-class `zai` provider. For a private GLM-compatible proxy, keep secrets in an env var and disable OpenAI-only request fields as needed:\n\n```yaml\nproviders:\n glm-proxy:\n baseUrl: https://api.z.ai/api/paas/v4\n apiKeyEnv: ZAI_API_KEY\n api: openai-completions\n compat:\n supportsDeveloperRole: false\n supportsReasoningEffort: false\n models:\n - id: glm-4.6\n```\n\n### JetBrains AI (Junie)\n\n`jetbrains-junie` is a first-class provider serving JetBrains-hosted models through the documented\nIngrazzio gateway (`https://ingrazzio-cloud-prod.labs.jb.gg`).\n\nAuthenticate with an access token generated at [junie.jetbrains.com/cli](https://junie.jetbrains.com/cli):\n\n```sh\nexport JUNIE_API_KEY=...\n```\n\nThe token is sent as `Authorization: Bearer` — JetBrains AI rejects requests that also carry `x-api-key`, so\nthis provider never lets the Anthropic SDK attach one. Usage is billed against your JetBrains AI\nsubscription, so bundled per-token costs are zero. There is no OAuth login flow; the environment variable is\nthe only supported credential source.\n\nThe gateway multiplexes transports by model family:\n\n| Family | Models | Transport | Prompt limit |\n| --- | --- | --- | --- |\n| Claude | `claude-sonnet-4-6` (default), `claude-sonnet-5`, `claude-opus-4-6`, `claude-opus-4-7`, `claude-opus-4-8`, `claude-opus-5`, `claude-fable-5` | `anthropic-messages` | 1M |\n| GPT | `gpt-5-2025-08-07`, `gpt-5.2-2025-12-11`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra` | `openai-completions` | 922K |\n| GPT (Responses-only) | `gpt-5.3-codex` | `openai-responses` | 272K |\n\nAll models cap output at 128K. Junie also exposes Gemini and Grok, but those ride a proprietary Grazie\ntranslation protocol that GJC does not implement, so they are deliberately not bundled. The bare\n`opus`/`sonnet`/`gpt`/`grok` aliases are Junie CLI shorthands the gateway itself rejects.\n\n### Allowed auth/discovery values\n\n- `auth`: `apiKey` (default), `none`, or `oauth`; for `models.yml` custom models, `oauth` is accepted by schema but does not waive the `apiKey` requirement\n- `models.yml` is strict: unknown provider/model keys fail validation before provider dispatch, so stale keys such as `requestTransform` or `wireModelId` only work where this document lists them.\n- `discovery.type`: `ollama`, `llama.cpp`, `lm-studio`, `omlx`, `vllm`, `sglang`, `openai-models-list`, or `models-dev`; `models-dev` may select a different catalog entry with `modelsDevProvider`\n- `cacheRetention`: `none`, `short`, or `long`; request-time options win over model/modelOverride values, then provider values, then `GJC_CACHE_RETENTION`, then the runtime default. The runtime default is `short` for most providers, but the Anthropic provider defaults to `long` because the ~5m cache is fragile for long-running subagent workflows. Canonical Anthropic models use top-level automatic caching and emit `ttl: \"1h\"` when long retention is supported. Claude-family models on non-canonical Anthropic-compatible endpoints default to explicit block markers because compatible proxies commonly inject, rewrite, or reject top-level cache controls; they omit `ttl` unless `compat.supportsLongCacheRetention: true` opts the endpoint into 1-hour retention. For OpenAI Responses, this controls `prompt_cache_retention` only; it does not disable `prompt_cache_key` when a stable session id exists.\n\n## OpenAI-compatible proxy configuration\n\nOpenAI-compatible proxy providers should use schema-supported provider keys first:\nThe first-class way to add a proxy provider is `gjc setup provider --preset litellm --base-url <url>` (LiteLLM) or `gjc setup provider --preset openai-compatible-proxy --base-url <url>` (any OpenAI-compatible gateway); both presets require `--base-url` and configure live model discovery. Proxy providers can also be used to route built-in model-preset selectors — see [Routing built-in presets through a proxy](#routing-built-in-presets-through-a-proxy-modelprofileproxyprovider). The YAML below shows the equivalent hand-written provider config:\n\n```yaml\nproviders:\n proxy-provider:\n baseUrl: https://api.proxy.example/v1\n apiKeyEnv: PROXY_API_KEY\n api: openai-completions\n auth: apiKey\n headers:\n User-Agent: curl/8.7.1\n models:\n - id: local-gpt\n name: Local GPT\n reasoning: true\n thinking:\n minLevel: low\n maxLevel: high\n mode: effort\n compat:\n supportsReasoningEffort: true\n input: [text]\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }\n contextWindow: 400000\n maxTokens: 128000\n```\n\nUse provider-level `headers` for proxy-required headers. Keep the provider `api` set to `openai-completions` when the proxy exposes Chat Completions-compatible `/v1/chat/completions` semantics. `auth: apiKey` sends the resolved token as bearer auth; use `auth: none` only for trusted local/no-auth endpoints.\n\nFor an unknown custom endpoint, `reasoning: true` declares model capability but does not prove the proxy accepts a control parameter. A familiar provider id or model-family name is not transport evidence: configurable LiteLLM/vLLM/local endpoints still fail closed. Add `thinking` and `compat.supportsReasoningEffort: true` only when the endpoint documents OpenAI-style `reasoning_effort`; set `compat.thinkingFormat` as well when it uses a different documented request shape. Otherwise GJC keeps reasoning-level controls unavailable and omits the parameter.\n\n`auth` selects the transport scheme only; it never supplies a credential. A provider that declares `models:` must therefore also declare where its key comes from, and `models.yml` validation rejects the config before model discovery otherwise:\n\n| Intent | Required keys |\n| --- | --- |\n| Authenticated proxy (recommended) | `auth: apiKey` (default) + `apiKeyEnv: MY_TOKEN` |\n| Authenticated proxy, key inline | `auth: apiKey` (default) + `apiKey: sk-…` (less safe; stored in plaintext) |\n| Genuinely unauthenticated endpoint | `auth: none`, no key |\n\nOmitting both `apiKey` and `apiKeyEnv` while leaving `auth` at its `apiKey` default fails with `Provider <name>: custom models need a credential source, but none is configured.` — the fix is to add one of the rows above, not to change `api` or `baseUrl`.\n\n`input` is the model modality list GJC uses to decide whether image content is forwarded. When a custom model omits `input`, GJC defaults to `[text]` (unless a bundled model with the same id contributes a reference). Vision-capable upstream models therefore need an explicit `input: [text, image]`; otherwise `read`/tool images are stripped before the request and replaced with `[image omitted: model does not support vision]`, even if the remote model can see images.\n\n```yaml\nproviders:\n ali:\n baseUrl: https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\n apiKeyEnv: ALI_API_KEY\n api: openai-completions\n auth: apiKey\n models:\n # id-only → text-only; images will be omitted\n - id: some-text-model\n # vision-capable hosted model must declare image input\n - id: qwen3.8-max-preview\n name: Qwen3.8 Max Preview\n reasoning: true\n input: [text, image]\n```\n\n`requestTransform` and `wireModelId` remain supported for request-body shaping, but they are not needed for ordinary OpenAI-compatible proxies whose local model id is already the upstream wire id. Unknown config keys fail validation before a provider request is sent.\n\nWhen request shaping is needed:\n\n- `requestTransform.profile: openai-proxy` strips OpenAI SDK/Stainless telemetry and beta headers at final fetch time and sets a generic GJC user agent.\n- `stripHeaders` replaces the preset strip list when provided.\n- `setHeaders` is applied after stripping; use `null` to remove a header.\n- `extraBody` is shallow-merged into the JSON request body after provider compatibility fields; core transport keys such as `model`, `messages`/`input`, `stream`, `tools`, and `tool_choice` are protected and ignored.\n- Model-level `requestTransform` overrides provider-level fields and shallow-merges `setHeaders`/`extraBody`.\n- `wireModelId` changes only the upstream request body model id; local selection still uses `provider/id`.\n\n### Layofflabs-style proxy example\n\n```yaml\nproviders:\n layofflabs:\n baseUrl: https://api.layofflabs.com/v1\n apiKeyEnv: OPENAI_API_KEY\n api: openai-completions\n auth: apiKey\n headers:\n User-Agent: curl/8.7.1\n models:\n - id: gpt-5.5\n name: GPT 5.5 via Layofflabs\n reasoning: true\n thinking:\n minLevel: low\n maxLevel: xhigh\n mode: effort\n defaultLevel: high\n levels: [low, medium, high, xhigh]\n compat:\n supportsReasoningEffort: true\n input: [text]\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }\n contextWindow: 400000\n maxTokens: 128000\n\nmodelBindings:\n modelRoles:\n default: layofflabs/gpt-5.5:high\n agentModelOverrides:\n executor: layofflabs/gpt-5.5:high\n```\n\n## Validation rules (current)\n\n### Full custom provider (`models` is non-empty)\n\nRequired:\n\n- `baseUrl`\n- A credential source: `apiKeyEnv` or `apiKey`. `auth` selects the scheme, not the credential, so `auth: apiKey` (the default) still needs one of them. Exempt: `auth: none`, and `api: bedrock-converse-stream`, which resolves AWS credentials from its own chain.\n- `api` at provider level or each model\n\n### Override-only provider (`models` missing or empty)\n\nMust define at least one of:\n\n- `baseUrl`\n- `headers`\n- `compat`\n- `requestTransform`\n- `disableStrictTools`\n- `modelOverrides`\n- `discovery`\n\n### Discovery\n\n- `discovery` requires provider-level `api`.\n\n### Model value checks\n\n- `id` required\n- `contextWindow` and `maxTokens` must be positive if provided\n- unknown provider, model, override, and request-transform keys fail schema validation; remove stale keys instead of relying on them being ignored.\n\n`maxTokens` on an explicit `models.yml` model (or `modelOverrides` entry) is the\nauthoritative default output budget for that configured model. A positive\nper-request `maxTokens` option still wins. Built-in catalog values and runtime\ndiscovery metadata retain the transport's conservative 32,000-token default\nunless the caller supplies a positive request override; their displayed\n`maxTokens`/`max-out` value is model metadata, not an unconditional request\npromise. A request value of `0` is treated as unspecified and does not bypass\nthe safe default. Provider hard limits and reasoning-budget rules remain\nenforced by the selected transport.\nValid budget values — configured or per-request — are positive safe integers\n(1 … 9007199254740991). Fractional values and anything above\n`Number.MAX_SAFE_INTEGER` fail schema validation in `models.yml`/overrides and\nare treated as unspecified when passed programmatically, so integer-only\nprovider fields (`max_tokens`, `max_completion_tokens`, `max_output_tokens`)\nnever receive an unsatisfiable budget. The low-level `stream()`/`complete()`\nboundary applies the same rule to provider options.\n\n## Merge and override order\n\nModelRegistry pipeline (on refresh):\n\n1. Load built-in providers/models from `@gajae-code/ai`.\n2. Load `models.yml` custom config.\n3. Apply provider overrides (`baseUrl`, `headers`, `requestTransform`, `disableStrictTools`, `cacheRetention`) to built-in models.\n4. Apply `modelOverrides` (per provider + model id).\n5. Merge custom `models`:\n - same `provider + id` replaces existing\n - otherwise append\n6. Load cached/runtime-discovered models (Ollama, llama.cpp, LM Studio, plus built-in provider managers), then re-apply model overrides.\n\n### Provider-model cache and static fingerprint\n\nCached per-provider model lists are persisted in the model-cache SQLite\ndatabase (schema v3) with a `static_fingerprint` column that hashes the\nstatic catalog slice merged into the row. When `resolveProviderModels`\nskips the network fetch and the fingerprint of the in-memory static\ncatalog matches the cached one, the cached rows are returned verbatim —\nthe static + dynamic merge is bypassed entirely. The fingerprint is\nmemoized per process via a WeakMap keyed by the static-models array\nreference, so repeated cold-start calls do not re-hash.\n\n## Canonical model equivalence and coalescing\n\nThe registry keeps every concrete provider model and then builds a canonical layer above them.\n\nCanonical ids are official upstream ids only, for example:\n\n- `anthropic-model-opus-4-6`\n- `anthropic-model-haiku-4-5`\n- `gpt-5.3-openai-code`\n\n### `models.yml` equivalence config\n\nExample:\n\n```yaml\nproviders:\n zenmux:\n baseUrl: https://api.zenmux.example/v1\n apiKey: ZENMUX_API_KEY\n api: openai-codex-responses\n models:\n - id: openai-code\n name: Zenmux OpenAI code\n reasoning: true\n input: [text]\n cost:\n input: 0\n output: 0\n cacheRead: 0\n cacheWrite: 0\n contextWindow: 200000\n maxTokens: 32768\n\nequivalence:\n overrides:\n zenmux/openai-code: gpt-5.3-openai-code\n p-openai-code/openai-code: gpt-5.3-openai-code\n exclude:\n - demo/openai-code-preview\n```\n\nBuild order for canonical grouping:\n\n1. exact user override from `equivalence.overrides`\n2. bundled official-id matches from built-in model metadata\n3. conservative heuristic normalization for gateway/provider variants\n4. fallback to the concrete model's own id\n\nCurrent heuristics are intentionally narrow:\n\n- embedded upstream prefixes can be stripped when present, for example `anthropic/...` or `openai/...`\n- dotted and dashed version variants can normalize only when they map to an existing official id, for example `4.6 -> 4-6`\n- ambiguous families or versions are not merged without a bundled match or explicit override\n\n### Canonical and preset-equivalent resolution behavior\n\nWhen multiple concrete variants are eligible for automatic resolution, the global provider policy uses this order:\n\n1. explicit `config.yml` `modelProviderOrder` entries, in their saved order\n2. omitted providers whose effective credential came from OAuth\n3. omitted providers using a manual API key, unknown credential provenance, or keyless access\n4. vision capability, exact canonical identity, canonical source quality, lowest `cost.input + cost.cacheRead`, stable registry model order, then concrete selector order\n\nThe explicit provider list may be partial. A listed API-key provider beats every omitted OAuth provider. Resetting Provider Priority clears the explicit list and restores OAuth-first plus deterministic fallback. Saved providers that are not currently available remain visible and persisted, but runtime resolution skips them.\n\nModel-profile and preset assignments support a lookup-only alias when the assignment does not explicitly name a provider. For example, a bare preset assignment `gpt-5` can select any available concrete variant whose final model-id segment is `gpt-5`, ranked by the same global policy. An assignment such as `openai/gpt-5` is an explicit provider pin: if that exact model is unavailable, activation reports it unavailable instead of switching providers. Alias lookup never rewrites the selected model's concrete provider, full model id, or `wireModelId`; a known bare alias with no eligible variant is unavailable and does not fall through to a different fuzzy match. Direct model selection remains unchanged.\nRuntime custom providers participate in the same lookup automatically. A custom provider model such as `hosted/glm-5.2` contributes the alias `glm-5.2` when the provider is registered, authenticated, and available; Provider Priority can rank that custom provider ahead of bundled providers without changing the preset. Custom IDs whose final segment differs (for example `glm-5.2-special`) do not join the alias.\n\nA session that resolves a canonical or alias selector keeps its concrete variant across provider-priority edits and discovery refreshes. Updated priority applies to new or unpinned resolutions. The session re-ranks only after explicit reselection or when the sticky variant becomes unavailable. Session state and transcripts continue to record the concrete provider/model that executed the turn.\n\nProvider defaults vs per-model overrides:\n\n- Provider `headers` are baseline.\n- Model `headers` override provider header keys.\n- `modelOverrides` can override model metadata (`name`, `reasoning`, `input`, `cost`, `contextWindow`, `maxTokens`, `headers`, `compat`, `contextPromotionTarget`).\n- `compat` is deep-merged for nested routing blocks (`openRouterRouting`, `vercelGatewayRouting`, `extraBody`).\n\n## Runtime discovery integration\n\n### Implicit Ollama discovery\n\nIf `ollama` is not explicitly configured, registry adds an implicit discoverable provider:\n\n- provider: `ollama`\n- api: `openai-responses`\n- base URL: `OLLAMA_BASE_URL` or `http://127.0.0.1:11434`\n- auth mode: keyless (`auth: none` behavior)\n\nRuntime discovery calls Ollama endpoints and normalizes discovered OpenAI-compatible models to `openai-responses`.\n\n### Implicit llama.cpp discovery\n\nIf `llama.cpp` is not explicitly configured, registry adds an implicit discoverable provider:\n\n- provider: `llama.cpp`\n- api: `openai-responses`\n- base URL: `LLAMA_CPP_BASE_URL` or `http://127.0.0.1:8080`\n- auth mode: keyless (`auth: none` behavior)\n\nRuntime discovery calls llama.cpp model endpoints and synthesizes model entries with local defaults.\n\n### Implicit LM Studio discovery\n\nIf `lm-studio` is not explicitly configured, registry adds an implicit discoverable provider:\n\n- provider: `lm-studio`\n- api: `openai-completions`\n- base URL: `LM_STUDIO_BASE_URL` or `http://127.0.0.1:1234/v1`\n- auth mode: keyless (`auth: none` behavior)\n\nRuntime discovery fetches models (`GET /models`) and synthesizes model entries with local defaults.\n\n### Implicit oMLX discovery\n\nIf `omlx` is not explicitly configured, registry adds an implicit discoverable provider:\n\n- provider: `omlx`\n- api: `openai-completions`\n- base URL: `OMLX_BASE_URL` or `http://127.0.0.1:8080/v1`\n- auth mode: keyless (`auth: none` behavior)\n\nRuntime discovery fetches models (`GET /v1/models`) and synthesizes model entries with local defaults and `max_model_len` support.\n\n### Implicit vLLM discovery\n\nIf `vllm` is not explicitly configured, its bundled provider descriptor discovers the local server implicitly:\n\n- provider: `vllm`\n- api: `openai-completions`\n- base URL: trusted `VLLM_BASE_URL` or `http://127.0.0.1:8000/v1` (a project `.env` cannot redirect authenticated traffic)\n- auth mode: keyless (`auth: none` behavior), `VLLM_API_KEY` attaches when present\n\nRuntime discovery fetches models (`GET /v1/models`) and synthesizes model entries with local defaults and `max_model_len` support. Credentialless implicit discovery is limited to loopback. For a remote vLLM server (for example, a LAN GPU box), set `VLLM_BASE_URL` and `VLLM_API_KEY` in the launching shell or a user-owned GJC environment file, or configure it explicitly under `providers` as shown below.\n\n### Implicit SGLang discovery\n\nIf `sglang` is not explicitly configured, its bundled provider descriptor discovers the local server implicitly:\n\n- provider: `sglang`\n- api: `openai-completions`\n- base URL: trusted `SGLANG_BASE_URL` or `http://127.0.0.1:30000/v1` (a project `.env` cannot redirect authenticated traffic)\n- auth mode: keyless (`auth: none` behavior), `SGLANG_API_KEY` attaches when present\n\nRuntime discovery fetches models (`GET /v1/models`) and synthesizes model entries with local defaults and `max_model_len` support. Credentialless implicit discovery is limited to loopback and needs no `/login`; `/login sglang` stores only an actual API key. For a remote SGLang server (for example, a LAN GPU box), set `SGLANG_BASE_URL` and `SGLANG_API_KEY` in the launching shell or a user-owned GJC environment file, or configure it explicitly under `providers` as shown below. Standard proxy environment variables remain explicit transport configuration, so include local SGLang hosts in `NO_PROXY` when local traffic must connect directly.\n\n### Explicit provider discovery\n\nYou can configure discovery yourself:\n\n```yaml\nproviders:\n ollama:\n baseUrl: http://127.0.0.1:11434\n api: openai-responses\n auth: none\n discovery:\n type: ollama\n\n llama.cpp:\n baseUrl: http://127.0.0.1:8080\n api: openai-responses\n auth: none\n discovery:\n type: llama.cpp\n```\n\n### Extension provider registration\n\nExtensions can register providers at runtime (`pi.registerProvider(...)`), including:\n\n- model replacement/append for a provider\n- custom stream handler registration for new API IDs\n- custom OAuth provider registration\n\n## Auth and API key resolution order\n\nWhen requesting a key for a provider, effective order is:\n\n1. Runtime override (CLI `--api-key`)\n2. `models.yml` `providers.<name>.apiKey` literal pin\n3. Stored API key credential in `agent.db` (written by `auth login`)\n4. `models.yml` `providers.<name>.apiKeyEnv` indirection — a pointer to a key,\n not a pinned value, so a stored login credential outranks it; it still\n outranks stored OAuth credentials\n5. Stored OAuth credential in `agent.db` (with refresh)\n6. Environment variable mapping (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.)\n7. ModelRegistry fallback resolver (provider `apiKey` from `models.yml`, env-name-or-literal semantics)\n\n`models.yml` `apiKey` behavior:\n\n- Value is first treated as an environment variable name.\n- If no env var exists, the literal string is used as the token.\n\nIf `authHeader: true` and provider `apiKey` is set, models get:\n\n- `Authorization: Bearer <resolved-key>` header injected.\n\nKeyless providers:\n\n- Providers marked `auth: none` are treated as available without credentials.\n- `getApiKey*` returns `kNoAuth` for them.\n\n### Broker mode\n\nWhen `GJC_AUTH_BROKER_URL` (or `auth.broker.url`) is set, the local SQLite credential store is replaced by `RemoteAuthCredentialStore`. Layers 2 and 3 above (stored API key / OAuth in `agent.db`) are served from a broker-supplied snapshot whose `refresh` tokens are redacted; expiry triggers `POST /v1/credential/:id/refresh` on the broker rather than a local refresh.\n\n`AuthStorage.setConfigApiKey` lets a `models.yml` `apiKey` win over a broker-resolved OAuth token without overriding a runtime `--api-key`. See [`auth-broker-gateway.md`](./auth-broker-gateway.md) for the full broker / gateway design and env surface (`GJC_AUTH_BROKER_URL`, `GJC_AUTH_BROKER_TOKEN`, `auth.broker.url`, `auth.broker.token`).\n\n## Model availability vs all models\n\n- `getAll()` returns the loaded model registry (built-in + merged custom + discovered).\n- `getAvailable()` filters to models that are keyless or have resolvable auth.\n\nSo a model can exist in registry but not be selectable until auth is available.\n\n## Runtime model resolution\n\n### CLI and pattern parsing\n\n`model-resolver.ts` supports:\n\n- exact `provider/modelId`\n- exact canonical model id\n- exact model id (provider inferred)\n- fuzzy/substring matching\n- glob scope patterns in `--models` (e.g. `openai/*`, `*sonnet*`)\n- optional `:thinkingLevel` suffix (`off|minimal|low|medium|high|xhigh`)\n\n`--provider` is legacy; `--model` is preferred.\n\nResolution precedence for exact selectors:\n\n1. exact `provider/modelId` bypasses coalescing\n2. exact canonical id resolves through the canonical index\n3. exact bare concrete id still works\n4. fuzzy and glob matching run after the exact paths\n\nThinking suffixes are split once from the final `:` only after the complete selector does not resolve. This preserves concrete OpenRouter route IDs such as `openrouter/z-ai/glm-4.7:nitro`; `:high` can follow that route suffix. Multiple suffixes are not recursively consumed. A complete `provider/modelId` selector is exact-only: it never falls back to fuzzy, substring, glob, or another provider when that concrete selector is absent. Exact-case provider/model entries resolve deterministically for custom replacement semantics; a case-insensitive selector that remains ambiguous does not guess.\n\nPreset/profile activation may use an unqualified assignment as a final-segment lookup alias after exact resolution fails. Provider-qualified assignments remain exact pins, and this opt-in does not apply to CLI or direct concrete model selection.\n\n### Initial model selection priority\n\n`findInitialModel(...)` uses this order:\n\n1. explicit CLI provider+model\n2. first scoped model (if not resuming)\n3. saved default provider/model\n4. known provider defaults (e.g. OpenAI/Anthropic/etc.) among available models\n5. first available model\n\n### Role aliases and settings\n\nSupported model roles:\n\n- `default` plus the agent assignment targets `executor`, `architect`, `planner`, `critic`\n\nRole aliases like `pi/default` expand through `settings.modelRoles`. Each role value can also append a thinking selector such as `:minimal`, `:low`, `:medium`, or `:high`.\n\nIf a role points at another role, the target model still inherits normally and any explicit suffix on the referring role wins for that role-specific use.\n\nRelated settings:\n\n- `modelRoles` (record)\n- `enabledModels` (scoped pattern list)\n- `modelProviderOrder` (global automatic provider precedence; editable under Settings → Providers with add/remove, move up/down, unavailable-entry retention, and Reset)\n- `providers.kimiApiFormat` (`openai` or `anthropic` request format)\n- `providers.openaiWebsockets` (`auto|off|on` websocket preference for OpenAI code provider transport)\n\n`modelRoles` may store either:\n\n- `provider/modelId` to pin a concrete provider variant\n- a canonical id such as `gpt-5.3-openai-code` to allow provider coalescing\n\nFor `enabledModels` and CLI `--models`:\n\n- exact canonical ids expand to all concrete variants in that canonical group\n- explicit `provider/modelId` entries stay exact\n- globs and fuzzy matches still operate on concrete models\n\nGlobal `enabledModels` and `disabledProviders` entries may also be scoped to a path prefix:\n\n```yaml\nenabledModels:\n - anthropic-model-sonnet-4-5\n - path: ~/work\n models:\n - anthropic/anthropic-model-opus-4-5\ndisabledProviders:\n - ollama\n - path: ~/private\n providers:\n - anthropic\n```\n\nString entries apply everywhere. Scoped entries apply when the current working directory is the configured path or one of its subdirectories. Use `path`, `paths`, `pathPrefix`, or `pathPrefixes`; use `models` for `enabledModels`, `providers` for `disabledProviders`, or `values` for either.\n\n## `/model` and `--list-models`\n\nBoth surfaces keep provider-prefixed models visible and selectable.\n\nThey now also expose canonical/coalesced models:\n\n- `/model` includes a canonical view alongside provider tabs\n- `--list-models` prints a canonical section plus the concrete provider rows\n\nSelecting a canonical entry stores the canonical selector. Selecting a provider row stores the explicit `provider/modelId`.\n\n## Context promotion (model-level fallback chains)\n\nContext promotion is an overflow recovery mechanism for small-context variants (for example `*-spark`) that automatically promotes to a larger-context sibling when the API rejects a request with a context length error. It is **off by default** (`contextPromotion.enabled` is `false`); opt in to enable it.\n\n### Trigger and order\n\nWhen a turn fails with a context overflow error (e.g. `context_length_exceeded`), `AgentSession` attempts promotion **before** falling back to compaction:\n\n1. If `contextPromotion.enabled` is true, resolve a promotion target (see below).\n2. If a target is found, switch to it and retry the request — no compaction needed.\n3. If no target is available, fall through to auto-compaction on the current model.\n\n### Target selection\n\nSelection is model-driven, not role-driven:\n\n1. `currentModel.contextPromotionTarget` (if configured)\n2. smallest larger-context model on the same provider + API\n\nCandidates are ignored unless credentials resolve (`ModelRegistry.getApiKey(...)`).\n\n### OpenAI code provider websocket handoff\n\nIf switching from/to `openai-codex-responses`, session provider state key `openai-codex-responses` is closed before model switch. This drops websocket transport state so the next turn starts clean on the promoted model.\n\n### Persistence behavior\n\nPromotion uses temporary switching (`setModelTemporary`):\n\n- recorded as a temporary `model_change` in session history\n- does not rewrite saved role mapping\n\n### Configuring explicit fallback chains\n\nConfigure fallback directly in model metadata via `contextPromotionTarget`.\n\n`contextPromotionTarget` accepts either:\n\n- `provider/model-id` (explicit)\n- `model-id` (resolved within current provider)\n\nExample (`models.yml`) for Spark -> non-Spark on the same provider:\n\n```yaml\nproviders:\n openai-code:\n modelOverrides:\n gpt-5.3-openai-code-spark:\n contextPromotionTarget: openai-code/gpt-5.3-openai-code\n```\n\nThe built-in model generator also assigns this automatically for `*-spark` models when a same-provider base model exists.\n\n## Compatibility and routing fields\n\nThe `compat` block on a provider or model overrides the URL-based auto-detection in `packages/ai/src/providers/openai-completions-compat.ts`. It is validated by `OpenAICompatSchema` in `packages/coding-agent/src/config/model-registry.ts` and consumed by every `openai-completions` transport (`packages/ai/src/providers/openai-completions.ts`). The canonical type is `OpenAICompat` in `packages/ai/src/types.ts`.\n\n`models.yml` accepts the following keys (all optional; unset falls back to URL detection):\n\nRequest shaping:\n\n- `supportsStore` — emit `store: false` on requests. Default: auto (off for non-standard endpoints).\n- `supportsDeveloperRole` — use the `developer` system role for reasoning models instead of `system`. Default: auto.\n- `sendSessionHeaders` — forward the agent session id as `session_id` and `x-session-id` request headers so OpenAI-compatible relays/proxies can do session-affinity routing and reuse a server-side prompt cache. Default: `false`. Caller-set `headers`/`requestTransform` values are never overwritten.\n- `supportsResponsesSessionAffinity` — for `openai-responses`, opt in to forwarding `session_id` and `x-client-request-id` affinity headers to a custom OpenAI-compatible relay. Canonical OpenAI routing remains automatic; known non-OpenAI provider IDs are rejected. Default: `false`.\n- `supportsUsageInStreaming` — send `stream_options: { include_usage: true }` to receive token usage on streaming responses. Default: `true`.\n- `maxTokensField` — `\"max_completion_tokens\"` or `\"max_tokens\"`. Default: auto.\n- `supportsToolChoice` — emit the `tool_choice` parameter when the caller forces a specific tool. Default: `true`. Set `false` for endpoints that 400 on `tool_choice` (e.g. DeepSeek when reasoning is on).\n- `disableReasoningOnForcedToolChoice` — drop `reasoning_effort` / OpenRouter `reasoning` whenever `tool_choice` forces a call. Default: auto (Kimi/Anthropic-fronted endpoints).\n- `extraBody` — extra top-level fields merged into every request body (gateway hints, controller selectors, etc.).\n\nReasoning / thinking:\n\n- `supportsReasoningEffort` — accept OpenAI-style `reasoning_effort`. Default: auto for bundled/audited providers and recognized first-party endpoints; `false` for unknown custom endpoints. Set `true` only from provider documentation or probe evidence, and pair it with explicit `reasoning: true` plus `thinking` metadata.\n- `reasoningEffortMap` — partial map from internal effort levels (`minimal|low|medium|high|xhigh`) to provider-specific strings (e.g. DeepSeek maps `xhigh -> \"max\"`).\n- `thinkingFormat` — request shape for thinking: `\"openai\"` (`reasoning_effort`), `\"openrouter\"` (`reasoning: { effort }`), `\"zai\"` (`thinking: { type: \"enabled\" }`), `\"qwen\"` (top-level `enable_thinking`), or `\"qwen-chat-template\"` (`chat_template_kwargs.enable_thinking`). Default: `\"openai\"`.\n- `reasoningContentField` — assistant field carrying chain-of-thought: `\"reasoning_content\"`, `\"reasoning\"`, or `\"reasoning_text\"`. Default: auto.\n- `requiresReasoningContentForToolCalls` — assistant tool-call turns must round-trip the reasoning field (DeepSeek-R1, Kimi, OpenRouter when reasoning is on). Default: `false`.\n- `requiresAssistantContentForToolCalls` — assistant tool-call turns must include non-empty text content (Kimi). Default: `false`.\n\nTool / message normalization:\n\n- `requiresToolResultName` — tool-result messages need a `name` field (Mistral). Default: auto.\n- `requiresAssistantAfterToolResult` — a user message after a tool result needs an assistant turn in between. Default: auto.\n- `requiresThinkingAsText` — convert thinking blocks to text wrapped in `<thinking>` delimiters (Mistral). Default: auto.\n- `requiresMistralToolIds` — normalize tool-call ids to exactly 9 alphanumeric chars. Default: auto.\n- `supportsStrictMode` — accept the per-tool `strict` field on tool schemas. Default: conservative auto-detect per provider/baseUrl.\n- `toolStrictMode` — `\"all_strict\"` forces strict on every tool, `\"none\"` forces it off; unset keeps the existing per-tool mixed behavior.\n\nGateway routing (only applied when `baseUrl` matches the gateway):\n\n- `openRouterRouting.only` / `openRouterRouting.order` — provider routing on `openrouter.ai` (see <https://openrouter.ai/docs/provider-routing>).\n- `vercelGatewayRouting.only` / `vercelGatewayRouting.order` — provider routing on `ai-gateway.vercel.sh` (see <https://vercel.com/docs/ai-gateway/models-and-providers/provider-options>).\n\nProvider-level `compat` is the baseline; per-model `compat` is deep-merged on top, with `openRouterRouting`, `vercelGatewayRouting`, and `extraBody` merged as nested objects.\n\n### Anthropic compatibility (`anthropic-messages`)\n\nFor `anthropic-messages` models, `compat.promptCacheMode` and `compat.supportsLongCacheRetention` are configurable at provider, model, and `modelOverrides` levels. Provider-level `compat` is the baseline; model and override values merge on top.\n\nPrompt-cache modes:\n\n- `automatic` — emit one top-level `cache_control` marker and let the Anthropic-compatible endpoint advance the breakpoint as the conversation grows.\n- `explicit` — emit block-level breakpoints instead. Use this for endpoints that reject top-level `cache_control` but support Anthropic's explicit content-block markers.\n- `none` — emit no generated Anthropic cache controls. Per-request or configured `cacheRetention: none` also disables generated caching.\n\nWithout an explicit mode, canonical Anthropic endpoints default to `automatic`, Claude-family model ids on non-canonical compatible endpoints default to `explicit`, and unknown non-Claude compatible endpoints default to `none`. Non-canonical endpoints get the default ~5m lifetime unless they opt into `supportsLongCacheRetention: true`. Set `promptCacheMode: automatic` only when a gateway is known to pass through Anthropic's top-level cache control without adding conflicting block markers.\n\nIf a gateway attaches enough cache markers of its own that ours push the request past Anthropic's four-breakpoint limit, Anthropic rejects it with `A maximum of 4 blocks with cache_control may be provided.` Those extra markers are not visible in the request GJC builds, so the limit is handled at runtime rather than predicted. Because the rejection means \"too many\" rather than \"none allowed\", recovery reduces the generated breakpoints one step at a time: `explicit` mode normally emits two markers (a conversation-prefix anchor and a current-turn refresh point), so the first retry keeps only the prefix anchor, and generated caching is disabled entirely only if that is rejected too. The reduced setting persists for the rest of the provider session, so an endpoint with one free slot keeps caching its conversation prefix instead of losing caching altogether. Set `promptCacheMode: none` on a gateway that never has a free slot to skip the wasted attempts.\n\n```yaml\nproviders:\n corp-anthropic:\n baseUrl: https://proxy.example.com/anthropic\n apiKeyEnv: CORP_ANTHROPIC_API_KEY\n api: anthropic-messages\n compat:\n promptCacheMode: explicit\n supportsLongCacheRetention: false\n models:\n - id: claude-sonnet-4-5\n contextWindow: 200000\n maxTokens: 8192\n```\n\nOther Anthropic-side compatibility knobs such as `disableAdaptiveThinking` and `supportsEagerToolInputStreaming` remain built-in catalog metadata rather than `models.yml` fields. `disableStrictTools` stays a provider-level setting (below).\n\n### Strict tool schemas (`disableStrictTools`)\n\nAnthropic's API supports a `strict` field on tool definitions that forces the model to always follow the provided schema exactly. This is enabled by default for all `anthropic-messages` providers because it guarantees schema conformance in agentic systems.\n\nThird-party providers that front the Anthropic API (AWS Bedrock, Azure, self-hosted proxies) do not always implement this field and will reject requests that include it. Set `disableStrictTools: true` at the provider level to opt out:\n\n```yaml\nproviders:\n bedrock-anthropic:\n baseUrl: https://bedrock-runtime.us-east-1.amazonaws.com/anthropic\n apiKey: AWS_BEARER_TOKEN\n api: anthropic-messages\n disableStrictTools: true\n models:\n - id: anthropic-model-sonnet-4-20250514\n name: Anthropic model Sonnet 4 (Bedrock)\n input: [text, image]\n contextWindow: 200000\n maxTokens: 16384\n cost:\n input: 3.00\n output: 15.00\n cacheRead: 0.30\n cacheWrite: 3.75\n```\n\n`disableStrictTools` is a provider-level flag that applies to all models in the provider.\n\nTool schemas going on the wire are normalized by the unified flow in\n`packages/ai/src/utils/schema/normalize.ts` (Google/CCA/MCP dispatchers\nplus the OpenAI strict-mode sanitize+enforce pipeline). See\n[`ai-schema-normalize.md`](./ai-schema-normalize.md) for the strict-mode\nedge cases (local `$ref` inlining, single-item `allOf` collapse,\n`anyOf`-wrapper description hoist, enum/const primitive-type inference)\nand the per-provider dispatcher mapping.\n## Practical examples\n\n### Local OpenAI-compatible endpoint (no auth)\n\n```yaml\nproviders:\n local-openai:\n baseUrl: http://127.0.0.1:8000/v1\n auth: none\n api: openai-completions\n models:\n - id: Qwen/Qwen2.5-Coder-32B-Instruct\n name: Qwen 2.5 Coder 32B (local)\n```\n\n### Hosted proxy with env-based key\n\n```yaml\nproviders:\n anthropic-proxy:\n baseUrl: https://proxy.example.com/anthropic\n apiKey: ANTHROPIC_PROXY_API_KEY\n api: anthropic-messages\n authHeader: true\n disableStrictTools: true # if the proxy doesn't support strict tool schemas\n models:\n - id: anthropic-model-sonnet-4-20250514\n name: Anthropic model Sonnet 4 (Proxy)\n reasoning: true\n input: [text, image]\n```\n\n### Override built-in provider route + model metadata\n\n```yaml\nproviders:\n openrouter:\n baseUrl: https://my-proxy.example.com/v1\n headers:\n X-Team: platform\n modelOverrides:\n anthropic/anthropic-model-sonnet-4:\n name: Sonnet 4 (Corp)\n compat:\n openRouterRouting:\n only: [anthropic]\n```\n\n## Legacy consumer caveat\n\nMost model configuration now flows through `models.yml` via `ModelRegistry`. Explicit `.json` / `.jsonc` paths remain supported only when passed programmatically to `ModelRegistry`; the default user config is `~/.gjc/agent/models.yml`.\n\n## Failure mode\n\nIf `models.yml` fails schema or validation checks:\n\n- registry keeps operating with built-in models\n- error is exposed via `ModelRegistry.getError()` and surfaced in UI/notifications\n",
|
|
56
|
-
"multi-vendor-profiles.md": "# Choosing models in GJC: role-based profiles\n\nA practical guide to picking models for GJC's roles, for every subscription situation — one vendor, two vendors, or the full multi-vendor set. It adds curated cross-vendor `profiles:` for `~/.gjc/agent/models.yml` and verified selector notes on top of the mechanism in [Model profiles](./models.md#model-profiles---mpreset). Everything here is **user config**; it complements the built-in `--mpreset` presets and overrides a built-in only when it shares its exact name.\n\n> Selectors, prices, and \"axis leaders\" are catalog- and time-sensitive (selectors and prices observed 2026-07 on the current bundled catalog; the measured latency and single-message-limit notes below were observed 2026-06 on `claude-opus-4-8` and have not been re-measured on `claude-opus-5`). Re-verify any selector with `gjc -p --no-session --no-tools --model <selector> \"Reply OK\"`.\n\n## The five roles\n\n`default` runs the main loop and most turns; `executor` / `architect` / `planner` / `critic` are the four bundled task agents, delegated only when the work calls for it.\n\n| Role | What it optimizes for |\n| --- | --- |\n| `default` | tool-calling reliability + honesty (it routes — its quality bounds the whole system) |\n| `executor` | real coding (SWE-bench Verified) |\n| `planner` | reasoning + sequencing (GPQA / ARC-AGI-2) |\n| `architect` | large-context + multimodal review |\n| `critic` | independent adversarial review (different family from what it reviews) |\n\n## Pick by what you subscribe to\n\n| You have | Use |\n| --- | --- |\n| **One vendor** | the built-in preset for that vendor — `claude-opus` (Anthropic), `codex-{eco,medium,pro}` (OpenAI/Codex), `opencodego` (OpenCode Go), or a single-vendor flagship tier (`zai/glm-5.2`, `kimi-code/...`, `xiaomi/...`, `xai/grok-4.3`, `minimax-code/...`). These already map all five roles inside one vendor. |\n| **Claude + Codex** | the built-in `opus-codex` (Claude main loop + Codex support roles). |\n| **Three or more / all five** | the cross-vendor profiles below — each role on its axis leader, `critic` kept cross-family. |\n\nThe single guiding rule across all of these: **keep `default` on the strongest router you have** (Anthropic Opus when available). A weak `default` caps quality regardless of the delegated models.\n\n## Cross-vendor profiles (3+ vendors)\n\nNo single vendor leads every axis, so these put each role on its axis leader and keep `critic` on a different family from the `executor` it reviews.\n\n```yaml\nprofiles:\n\n daily: # everyday balance\n required_providers: [anthropic, openai-codex, google-antigravity, xai]\n model_mapping:\n default: anthropic/claude-opus-5:medium\n executor: openai-codex/gpt-5.4:high\n planner: google-antigravity/gemini-3.1-pro-low:high\n architect: google-antigravity/gemini-3.1-pro-low:high\n critic: xai/grok-4.3:medium\n\n ultimate: # cost-no-object, best per role\n required_providers: [anthropic, openai-codex, google-antigravity, xai]\n model_mapping:\n default: anthropic/claude-opus-5:high\n executor: anthropic/claude-opus-5:max\n planner: openai-codex/gpt-5.5:xhigh\n architect: google-antigravity/gemini-3.1-pro-low:high\n critic: xai/grok-4.3:high\n\n eco: # cheapest delegated work; main loop stays on Opus\n required_providers: [anthropic, opencode-go, google-antigravity, xai]\n model_mapping:\n default: anthropic/claude-opus-5:low\n executor: opencode-go/deepseek-v4-flash\n planner: xai/grok-4-1-fast:high\n architect: google-antigravity/gemini-3.1-pro-low\n critic: google-antigravity/gemini-3.5-flash\n\n monorepo: # huge codebases (openai-codex excluded: 372k context cap)\n required_providers: [anthropic, google-antigravity, opencode-go]\n model_mapping:\n default: anthropic/claude-opus-5:medium\n executor: anthropic/claude-opus-5:high\n planner: google-antigravity/gemini-3.1-pro-low:high\n architect: anthropic/claude-opus-5:high\n critic: opencode-go/glm-5.2\n\n reviewer: # review/audit stance — the author-mode role split, inverted\n required_providers: [anthropic, openai-codex, google-antigravity]\n model_mapping:\n default: anthropic/claude-opus-5:high # aggregator restraint: preserve raw reviewer verdicts\n executor: openai-codex/gpt-5.5:high # support — repro PoCs, failing tests, harnesses\n planner: google-antigravity/gemini-3.1-pro-low:high # review checklists / audit scoping\n architect: anthropic/claude-opus-5:high # lead 1 — primary code-review judge (effective long-context)\n critic: openai-codex/gpt-5.5:high # lead 2 — merge gate, cross-family vs Claude-authored code\n```\n\n## Reviewer stance and the external review gate\n\nThe profiles above assume an **authoring** stance: `executor` is the lead and `architect`/`critic` verify its work. In a session whose primary job is reviewing or auditing (not writing) code, the roles invert — `architect`/`critic` become the leads and `executor` is support (reproduction PoCs, failing tests). The `reviewer` profile encodes that inversion, with one generalized provenance rule: **the reviewing model family must differ from the family that authored the code under review**, not merely from the session's own executor.\n\nA verified use is the cross-session final review gate: the authoring session launches a fresh, stateless reviewer sub-session so the finished diff is judged without the authoring context:\n\n```sh\n# the one-shot gate needs only a cross-family --model; add --mpreset reviewer as an\n# optional enhancement AFTER installing this profile in ~/.gjc/agent/models.yml:\ngjc -p --no-session --model openai-codex/gpt-5.5:xhigh --tools read,search,find \"<review prompt: diff + spec paths, severity findings, final line VERDICT: APPROVE|REQUEST_CHANGES>\"\n```\n\nThe `--tools` allowlist is part of the contract: it enforces the reviewer's read-only boundary for the built-in tool surface instead of trusting the prompt (the runtime still injects the session `goal` tool unless `goal.enabled` is off — disabling it for the reviewer invocation is **mandatory**, via a dedicated gate directory outside the repo so the reviewed checkout stays clean, see the template — plus `generate_image` when an image credential exists). In this one-shot form the session's `default` model authors the verdict — a tool-restricted print session cannot delegate to the profile's `critic`/`architect` roles — so the explicit cross-family `--model` carries provenance, and the `reviewer` profile itself serves the interactive review-session case (activate it with `--mpreset reviewer` only after copying it into `models.yml`; otherwise activation fails with an unknown-profile error). Profile names in this document live in the user namespace — a user profile overrides a builtin preset only on an exact name match, and a future builtin with the same name would be silently shadowed by your copy.\n\nSee [Extragoal local skill template](./extragoal-skill-template.md) for the full gate workflow (verdict contract, findings triage, bounded re-sign loop, secret-scan and injection guards) built on this recipe.\n\n## Model cheatsheet (by need)\n\nCurrent axis leaders and the cheaper second option, with metered price ($/1M in/out; Gemini via Antigravity runs on the Google AI subscription):\n\n| Need | First pick | Cheaper option |\n| --- | --- | --- |\n| Router / tool-calling (`default`) | `anthropic/claude-opus-5` (5/25) | `anthropic/claude-sonnet-5` (3/15) |\n| Coding (`executor`) | `anthropic/claude-opus-5` (5/25) — the prior `claude-opus-4-8` scored SWE-bench Verified ~88.6; no Opus 5 measurement yet | `openai-codex/gpt-5.4` (2.5/15) · `opencode-go/deepseek-v4-flash` (0.14/0.28) |\n| Reasoning (`planner`) | `openai-codex/gpt-5.5` (ARC-AGI-2) / `google-antigravity/gemini-3.1-pro-low:high` (GPQA) | `xai/grok-4-1-fast` (0.2/0.5) |\n| Large context (`architect`) | `anthropic/claude-opus-5` (effective long-context) | `xai/grok-4-fast` (2M nominal, 0.2/0.5) |\n| Multimodal review (`architect`) | `google-antigravity/gemini-3.1-pro-low:high` | `google-antigravity/gemini-3.5-flash` |\n| Independent critic | `xai/grok-4.3` (1.25/2.5) | `opencode-go/glm-5.2` · `google-antigravity/gemini-3.5-flash` |\n\nOn standard tasks, all current frontier models in the catalog are accurate; **pick by cost, latency, and role fit, not by raw accuracy on easy prompts.** As an indicative GJC-routed latency reference (`gjc -p`, identical coding + reasoning prompts, all correct): `grok-4.3` and `glm-5.2` ≈ 2–3s, `deepseek-v4-pro` ≈ 3–4s, `claude-opus-4-8` / `gpt-5.5` ≈ 4–7s, `gemini-3.1-pro-low:high` ≈ 7s. `claude-opus-5` shares Opus 4.8's published context/output envelope but has not been latency-measured here.\n\n## Verified selector notes (current catalog)\n\nObserved via live `gjc -p` calls; useful when wiring the profiles above:\n\n- **Antigravity Gemini, high reasoning** → use `google-antigravity/gemini-3.1-pro-low:high`. The id `gemini-3.1-pro-high` returns HTTP 400 (no matching backend model); `thinkingLevel` is a per-request parameter, so raising it on `gemini-3.1-pro-low` invokes the model's native high-reasoning mode rather than a degraded one.\n- **openai-codex on a ChatGPT account** serves base GPT only (`gpt-5.5`, `gpt-5.4`). Standalone `-codex` variants (`gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.1-codex-max` / `-mini`) return `not supported when using Codex with a ChatGPT account`.\n- **Single-message input limit is separate from the context window.** Measured on `claude-opus-4-8` (not yet re-measured on `claude-opus-5`, which publishes the same 1M window): the model runs with a 1M window via multi-turn accumulation, but a single `@file` message above ~400k tokens returns 400 on `anthropic` / `google-antigravity`; `xai` / `opencode-go` accept larger single messages. Chunk very large inputs across turns instead of pasting one block.\n- **Some selectors come from a provider's live catalog, not the bundled snapshot.** `opencode-go/glm-5.2` and `google-antigravity/gemini-3.5-flash` resolved in `gjc -p` tests but are **not** in `packages/ai/src/models.json`; they appear only after the provider's online model discovery has populated the registry. `required_providers` verifies credentials at activation — it does **not** guarantee fresh, non-stale discovery — so activation can still fail with `selector did not resolve` until discovery runs (re-login or retry to refresh). If you hit that, substitute a bundled id: `opencode-go/deepseek-v4-pro` for the critic, or `zai/glm-5.2` (add `zai` to `required_providers`) for GLM 5.2.\n\n## Activation\n\n```bash\ngjc --mpreset daily # this session only\ngjc --mpreset ultimate --default # persist as the startup default (config.yml)\n```\n\n### Delegation is what makes vendor separation pay off\n\nWorker roles only consume the other vendor's quota when the main agent actually delegates, and the strong \"delegate by default\" directive is gated behind `task.eager`. When `executor` or `planner` is pinned to a provider other than the `default` role's provider, GJC therefore turns eager delegation on for that session unless `task.eager` is set explicitly in config. Setting `task.eager false` by hand stays authoritative and keeps the separated workers idle — `gjc config doctor` reports that combination as an advisory.\n\nActivation hard-blocks when any provider in `required_providers` lacks credentials, so log in first: `/login anthropic`, `/login openai-codex`, `/login google-antigravity`, `/login xai` (and `opencode-go` via `OPENCODE_API_KEY`).\n\n### Serving cross-vendor profiles through one OpenAI-compatible proxy\n\nWhen a single gateway (LiteLLM, OpenRouter, or a custom proxy) fronts several vendors, you do not need to configure every `required_providers` entry directly. Add the gateway as a provider — `gjc setup provider --preset litellm --base-url <url>` or `gjc setup provider --preset openai-compatible-proxy --base-url <url>` — and point `modelProfile.proxyProvider` at it in `config.yml`:\n\n```yaml\nmodelProfile:\n proxyProvider: litellm\n proxyMode: always # route all supported built-in preset selectors through the gateway\n```\n\n`proxyMode: fallback` is the default and uses the gateway only when the direct provider is unauthenticated. Set `proxyMode: always` when the gateway must be the single audit, quota, or spend-control surface: it routes all proxy-routable **built-in** preset selectors through the proxy even when direct credentials exist. The configured proxy must be authenticated and expose every routed model; activation fails closed for missing or ambiguous models. User-defined profiles are never rewritten — set their selectors to `litellm/…` explicitly if you want them proxied. Routing and fail-closed behavior are documented in [Routing built-in presets through a proxy](./models.md#routing-built-in-presets-through-a-proxy-modelprofileproxyprovider).\n",
|
|
56
|
+
"multi-vendor-profiles.md": "# Choosing models in GJC: role-based profiles\n\nA practical guide to picking models for GJC's roles, for every subscription situation — one vendor, two vendors, or the full multi-vendor set. It adds curated cross-vendor `profiles:` for `~/.gjc/agent/models.yml` and verified selector notes on top of the mechanism in [Model profiles](./models.md#model-profiles---mpreset). Everything here is **user config**; it complements the built-in `--mpreset` presets and overrides a built-in only when it shares its exact name.\n\n> Selectors, prices, and \"axis leaders\" are catalog- and time-sensitive (selectors and prices observed 2026-07 on the current bundled catalog; the measured latency and single-message-limit notes below were observed 2026-06 on `claude-opus-4-8` and have not been re-measured on `claude-opus-5`). Re-verify any selector with `gjc -p --no-session --no-tools --model <selector> \"Reply OK\"`.\n\n## The five roles\n\n`default` runs the main loop and most turns; `executor` / `architect` / `planner` / `critic` are the four bundled task agents, delegated only when the work calls for it.\n\n| Role | What it optimizes for |\n| --- | --- |\n| `default` | tool-calling reliability + honesty (it routes — its quality bounds the whole system) |\n| `executor` | real coding (SWE-bench Verified) |\n| `planner` | reasoning + sequencing (GPQA / ARC-AGI-2) |\n| `architect` | large-context + multimodal review |\n| `critic` | independent adversarial review (different family from what it reviews) |\n\n## Pick by what you subscribe to\n\n| You have | Use |\n| --- | --- |\n| **One vendor** | the built-in preset for that vendor — `claude-opus` (Anthropic), `codex-{eco,medium,pro}` (OpenAI/Codex), `opencodego` (OpenCode Go), or a single-vendor flagship tier (`zai/glm-5.2`, `kimi-code/...`, `xiaomi/...`, `xai/grok-4.3`, `minimax-code/...`). These already map all five roles inside one vendor. |\n| **Claude + Codex** | the built-in `opus-codex` (Claude main loop + Codex support roles). |\n| **Three or more / all five** | the cross-vendor profiles below — each role on its axis leader, `critic` kept cross-family. |\n\nThe single guiding rule across all of these: **keep `default` on the strongest router you have** (Anthropic Opus when available). A weak `default` caps quality regardless of the delegated models.\n\n## Cross-vendor profiles (3+ vendors)\n\nNo single vendor leads every axis, so these put each role on its axis leader and keep `critic` on a different family from the `executor` it reviews.\n\n```yaml\nprofiles:\n\n daily: # everyday balance\n required_providers: [anthropic, openai-codex, google-antigravity, xai]\n model_mapping:\n default: anthropic/claude-opus-5:medium\n executor: openai-codex/gpt-5.4:high\n planner: google-antigravity/gemini-3.1-pro-low:high\n architect: google-antigravity/gemini-3.1-pro-low:high\n critic: xai/grok-4.3:medium\n\n ultimate: # cost-no-object, best per role\n required_providers: [anthropic, openai-codex, google-antigravity, xai]\n model_mapping:\n default: anthropic/claude-opus-5:high\n executor: anthropic/claude-opus-5:max\n planner: openai-codex/gpt-5.5:xhigh\n architect: google-antigravity/gemini-3.1-pro-low:high\n critic: xai/grok-4.3:high\n\n eco: # cheapest delegated work; main loop stays on Opus\n required_providers: [anthropic, opencode-go, google-antigravity, xai]\n model_mapping:\n default: anthropic/claude-opus-5:low\n executor: opencode-go/deepseek-v4-flash\n planner: xai/grok-4-1-fast:high\n architect: google-antigravity/gemini-3.1-pro-low\n critic: google-antigravity/gemini-3.5-flash\n\n monorepo: # huge codebases (openai-codex excluded: 372k context cap)\n required_providers: [anthropic, google-antigravity, opencode-go]\n model_mapping:\n default: anthropic/claude-opus-5:medium\n executor: anthropic/claude-opus-5:high\n planner: google-antigravity/gemini-3.1-pro-low:high\n architect: anthropic/claude-opus-5:high\n critic: opencode-go/glm-5.2\n\n reviewer: # review/audit stance — the author-mode role split, inverted\n required_providers: [anthropic, openai-codex, google-antigravity]\n model_mapping:\n default: anthropic/claude-opus-5:high # aggregator restraint: preserve raw reviewer verdicts\n executor: openai-codex/gpt-5.5:high # support — repro PoCs, failing tests, harnesses\n planner: google-antigravity/gemini-3.1-pro-low:high # review checklists / audit scoping\n architect: anthropic/claude-opus-5:high # lead 1 — primary code-review judge (effective long-context)\n critic: openai-codex/gpt-5.5:high # lead 2 — merge gate, cross-family vs Claude-authored code\n```\n\n## Reviewer stance and the external review gate\n\nThe profiles above assume an **authoring** stance: `executor` is the lead and `architect`/`critic` verify its work. In a session whose primary job is reviewing or auditing (not writing) code, the roles invert — `architect`/`critic` become the leads and `executor` is support (reproduction PoCs, failing tests). The `reviewer` profile encodes that inversion, with one generalized provenance rule: **the reviewing model family must differ from the family that authored the code under review**, not merely from the session's own executor.\n\nA verified use is the cross-session final review gate: the authoring session launches a fresh, stateless reviewer sub-session so the finished diff is judged without the authoring context:\n\n```sh\n# the one-shot gate needs only a cross-family --model; add --mpreset reviewer as an\n# optional enhancement AFTER installing this profile in ~/.gjc/agent/models.yml:\ngjc -p --no-session --model openai-codex/gpt-5.5:xhigh --tools read,search,find \"<review prompt: diff + spec paths, severity findings, final line VERDICT: APPROVE|REQUEST_CHANGES>\"\n```\n\nThe `--tools` allowlist is part of the contract: it enforces the reviewer's read-only boundary for the built-in tool surface instead of trusting the prompt (the runtime still injects the session `goal` tool unless `goal.enabled` is off — disabling it for the reviewer invocation is **mandatory**, via a dedicated gate directory outside the repo so the reviewed checkout stays clean, see the template — plus `generate_image` when an image credential exists). In this one-shot form the session's `default` model authors the verdict — a tool-restricted print session cannot delegate to the profile's `critic`/`architect` roles — so the explicit cross-family `--model` carries provenance, and the `reviewer` profile itself serves the interactive review-session case (activate it with `--mpreset reviewer` only after copying it into `models.yml`; otherwise activation fails with an unknown-profile error). Profile names in this document live in the user namespace — a user profile overrides a builtin preset only on an exact name match, and a future builtin with the same name would be silently shadowed by your copy.\n\nSee [Extragoal local skill template](./extragoal-skill-template.md) for the full gate workflow (verdict contract, findings triage, bounded re-sign loop, secret-scan and injection guards) built on this recipe.\n\n## Model cheatsheet (by need)\n\nCurrent axis leaders and the cheaper second option, with metered price ($/1M in/out; Gemini via Antigravity runs on the Google AI subscription):\n\n| Need | First pick | Cheaper option |\n| --- | --- | --- |\n| Router / tool-calling (`default`) | `anthropic/claude-opus-5` (5/25) | `anthropic/claude-sonnet-5` (3/15) |\n| Coding (`executor`) | `anthropic/claude-opus-5` (5/25) — the prior `claude-opus-4-8` scored SWE-bench Verified ~88.6; no Opus 5 measurement yet | `openai-codex/gpt-5.4` (2.5/15) · `opencode-go/deepseek-v4-flash` (0.14/0.28) |\n| Reasoning (`planner`) | `openai-codex/gpt-5.5` (ARC-AGI-2) / `google-antigravity/gemini-3.1-pro-low:high` (GPQA) | `xai/grok-4-1-fast` (0.2/0.5) |\n| Large context (`architect`) | `anthropic/claude-opus-5` (effective long-context) | `xai/grok-4-fast` (2M nominal, 0.2/0.5) |\n| Multimodal review (`architect`) | `google-antigravity/gemini-3.1-pro-low:high` | `google-antigravity/gemini-3.5-flash` |\n| Independent critic | `xai/grok-4.3` (1.25/2.5) | `opencode-go/glm-5.2` · `google-antigravity/gemini-3.5-flash` |\n\nOn standard tasks, all current frontier models in the catalog are accurate; **pick by cost, latency, and role fit, not by raw accuracy on easy prompts.** As an indicative GJC-routed latency reference (`gjc -p`, identical coding + reasoning prompts, all correct): `grok-4.3` and `glm-5.2` ≈ 2–3s, `deepseek-v4-pro` ≈ 3–4s, `claude-opus-4-8` / `gpt-5.5` ≈ 4–7s, `gemini-3.1-pro-low:high` ≈ 7s. `claude-opus-5` shares Opus 4.8's published context/output envelope but has not been latency-measured here.\n\n## Verified selector notes (current catalog)\n\nObserved via live `gjc -p` calls; useful when wiring the profiles above:\n\n- **Antigravity Gemini, high reasoning** → use `google-antigravity/gemini-3.1-pro-low:high`. The id `gemini-3.1-pro-high` returns HTTP 400 (no matching backend model); `thinkingLevel` is a per-request parameter, so raising it on `gemini-3.1-pro-low` invokes the model's native high-reasoning mode rather than a degraded one.\n- **openai-codex on a ChatGPT account** has entitlement-dependent model access: base GPT includes `gpt-5.5` and `gpt-5.4`, while GPT-5.6 Sol requires a Pro-tier account. Standalone `-codex` variants (`gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.1-codex-max` / `-mini`) return `not supported when using Codex with a ChatGPT account`, and a known non-Pro account is rejected locally before a GPT-5.6 Sol turn.\n- **Single-message input limit is separate from the context window.** Measured on `claude-opus-4-8` (not yet re-measured on `claude-opus-5`, which publishes the same 1M window): the model runs with a 1M window via multi-turn accumulation, but a single `@file` message above ~400k tokens returns 400 on `anthropic` / `google-antigravity`; `xai` / `opencode-go` accept larger single messages. Chunk very large inputs across turns instead of pasting one block.\n- **Some selectors come from a provider's live catalog, not the bundled snapshot.** `opencode-go/glm-5.2` and `google-antigravity/gemini-3.5-flash` resolved in `gjc -p` tests but are **not** in `packages/ai/src/models.json`; they appear only after the provider's online model discovery has populated the registry. `required_providers` verifies credentials at activation — it does **not** guarantee fresh, non-stale discovery — so activation can still fail with `selector did not resolve` until discovery runs (re-login or retry to refresh). If you hit that, substitute a bundled id: `opencode-go/deepseek-v4-pro` for the critic, or `zai/glm-5.2` (add `zai` to `required_providers`) for GLM 5.2.\n\n## Activation\n\n```bash\ngjc --mpreset daily # this session only\ngjc --mpreset ultimate --default # persist as the startup default (config.yml)\n```\n\n### Delegation is what makes vendor separation pay off\n\nWorker roles only consume the other vendor's quota when the main agent actually delegates, and the strong \"delegate by default\" directive is gated behind `task.eager`. When `executor` or `planner` is pinned to a provider other than the `default` role's provider, GJC therefore turns eager delegation on for that session unless `task.eager` is set explicitly in config. Setting `task.eager false` by hand stays authoritative and keeps the separated workers idle — `gjc config doctor` reports that combination as an advisory.\n\nActivation hard-blocks when any provider in `required_providers` lacks credentials, so log in first: `/login anthropic`, `/login openai-codex`, `/login google-antigravity`, `/login xai` (and `opencode-go` via `OPENCODE_API_KEY`).\n\n### Serving cross-vendor profiles through one OpenAI-compatible proxy\n\nWhen a single gateway (LiteLLM, OpenRouter, or a custom proxy) fronts several vendors, you do not need to configure every `required_providers` entry directly. Add the gateway as a provider — `gjc setup provider --preset litellm --base-url <url>` or `gjc setup provider --preset openai-compatible-proxy --base-url <url>` — and point `modelProfile.proxyProvider` at it in `config.yml`:\n\n```yaml\nmodelProfile:\n proxyProvider: litellm\n proxyMode: always # route all supported built-in preset selectors through the gateway\n```\n\n`proxyMode: fallback` is the default and uses the gateway only when the direct provider is unauthenticated. Set `proxyMode: always` when the gateway must be the single audit, quota, or spend-control surface: it routes all proxy-routable **built-in** preset selectors through the proxy even when direct credentials exist. The configured proxy must be authenticated and expose every routed model; activation fails closed for missing or ambiguous models. User-defined profiles are never rewritten — set their selectors to `litellm/…` explicitly if you want them proxied. Routing and fail-closed behavior are documented in [Routing built-in presets through a proxy](./models.md#routing-built-in-presets-through-a-proxy-modelprofileproxyprovider).\n",
|
|
57
57
|
"native-ffi-optimization-policy.md": "# ADR: Native FFI Optimization Policy\n\n- Status: Accepted\n- Scope: `crates/pi-natives` algorithmic ports proposed for performance reasons\n- Related: [`porting-to-natives.md`](./porting-to-natives.md), [`natives-architecture.md`](./natives-architecture.md), [`natives-binding-contract.md`](./natives-binding-contract.md), [`cpu-hotspot-map.json`](./cpu-hotspot-map.json), [`hotspot-map-successor.md`](./hotspot-map-successor.md)\n\n## Decision\n\nA new native (Rust N-API / FFI) port proposed **to optimize a leftover hot path** does not land unless **all** of the following gates pass:\n\n1. **Corpus evidence** — a profiling-corpus trace shows the path has user-visible latency or RSS impact on a representative workload (not just a static complexity argument).\n2. **Self-time attribution** — a `profilerSelfTime` artifact identifies the proposed hotspot, **or** fallback-toggle evidence proves an end-to-end benefit without byte changes. Wall-clock proxy timing alone is never sufficient.\n3. **Measured FFI overhead** — the N-API call/marshalling overhead is measured against the JS/TS baseline, not assumed away.\n4. **Representative win** — a representative p50/p95 win exists on realistic inputs, not only microbenchmark seed results.\n5. **Byte parity** — a byte-identical corpus covers rendered, persisted, and provider-visible bytes for the changed path.\n6. **Operational cost** — fallback, packaging, and rollback costs are documented.\n\nThis policy governs **speculative algorithmic ports**. It does **not** re-litigate already-native platform/system surfaces (see [Scope boundary](#scope-boundary)).\n\n## Context\n\nThe CPU/memory hotspot program (Optimization Suites v1–v3, tracked in [`cpu-hotspot-map.json`](./cpu-hotspot-map.json)) is closed out. Its prioritization was a **static structural ranking** (algorithmic complexity × trigger frequency), and the map's own `method` field records that real CPU self-time was \"to be measured by the agreed profiling corpus during optimization.\" That corpus is being built separately; until its evidence exists, new native ports for leftover hotspots would repeat the same evidence gap.\n\nThe suites already produced concrete decisions that this policy codifies so they are not re-discovered:\n\n- **v2 (#530)** measured and **rejected the five remaining Rust port candidates** per the FFI cost gates after shipping only `diffLines` (H03) natively. Native overhead did not beat the JS/TS baseline for those candidates on realistic inputs.\n- **v3 (#558) rejected a native word-diff (H04)** \"without a fresh FFI gate\" — the TS fast paths were retained instead; a native port would need to re-clear gates 1–6 above.\n- **Hunt-Szymanski LCS (H05)** was implemented as a native/algorithmic replacement, then **reverted** because it produced byte-different rendered diffs (reproduced by red-team). Byte parity is the gate, not raw speed.\n- **The custom JSON length counter (H08)** was implemented, made exact, then **deleted** — an exact JS reimplementation was not faster than native `JSON.stringify`. \"More native\" is not automatically \"faster.\"\n\nThese four precedents share a root cause: a plausible algorithmic/native win that failed a real gate (cost, byte parity, or end-to-end benefit). The policy makes those gates a precondition rather than a post-hoc discovery.\n\n## Evidence taxonomy\n\nNative-port claims must classify their evidence using the same separated classes as the profiling corpus. These classes must never be conflated:\n\n- **`wallClockPhase`** — elapsed timing around a phase or operation. Useful for perceived-latency and regression detection; **insufficient** to confirm CPU self-time or to justify a port on its own.\n- **`processCpuUsage`** — `process.cpuUsage()` user/system deltas, optionally normalized by elapsed time. Indicates process-level CPU pressure; **cannot** attribute self-time to a specific hotspot.\n- **`profilerSelfTime`** — profiler (or equivalent sampled/trace) attribution of self-time to a function, module, or native symbol. **Required** before a hotspot may be called \"CPU-self-time confirmed.\"\n\nA native-optimization proposal that cites only `wallClockPhase` or `processCpuUsage` is **not** CPU-self-time confirmed and does not clear gate 2.\n\n## Approval checklist\n\nBefore opening a native-optimization PR, confirm and attach evidence for each:\n\n- [ ] Corpus trace shows user-visible latency or RSS impact for the path (gate 1).\n- [ ] `profilerSelfTime` artifact identifies the hotspot, **or** fallback-toggle before/after evidence proves end-to-end benefit without byte changes (gate 2).\n- [ ] FFI/marshalling overhead measured vs the JS/TS baseline in the same benchmark run (gate 3).\n- [ ] Representative p50/p95 win on realistic inputs, not only seeded microbench results (gate 4).\n- [ ] Byte-identical corpus covers rendered, persisted, and provider-visible bytes (gate 5).\n- [ ] Fallback, packaging (platform variants / embedded addon), and rollback costs documented (gate 6).\n\nIf any box is unchecked, keep the work in TypeScript or hold it as a tracked candidate; do not switch callsites. This mirrors the existing **Rule of thumb** in [`porting-to-natives.md`](./porting-to-natives.md): if native is not faster *and* behavior-compatible, do not switch callsites.\n\n## Scope boundary\n\nThis policy targets **speculative algorithmic ports**, not the established native surface. The following are **already native** by design and are explicitly out of scope (see `alreadyNativeExcluded` in [`cpu-hotspot-map.json`](./cpu-hotspot-map.json)):\n\n`grep`, `fd`/`glob`, text width/wrap/truncate/slice, syntax highlighting, HTML→Markdown, AST, summary, process/PTY/shell, SIXEL, clipboard, `Bun.hash.xxHash32/64`, and `JSON.parse`/`JSON.stringify`.\n\nThese are native because they are I/O, OS/process integration, or platform primitives — the criteria in [`porting-to-natives.md`](./porting-to-natives.md#when-to-port). Distinguishing them from algorithmic ports matters: a leftover algorithmic hotspot must clear gates 1–6, whereas adding a new OS/process/native-primitive binding follows the standard porting guide.\n\n## Consequences\n\n- New native algorithmic ports require profiling-corpus evidence and a measured cost gate before review; this slows speculative optimization but prevents byte-parity regressions and dead native code.\n- The default answer for a leftover hotspot is \"keep it in TypeScript\" until the corpus proves it matters.\n- Already-native platform/system primitives and new OS/process bindings are unaffected; they follow [`porting-to-natives.md`](./porting-to-natives.md) as before.\n- Reviewers can reject a native-optimization PR purely on a missing gate, citing this ADR, without re-deriving the rationale.\n\n## Follow-ups\n\n- Held native candidates (H04 word-diff, H05 LCS, and other v2-rejected candidates) stay held unless a future PR clears gates 1–6 with fresh corpus evidence.\n- When the profiling corpus lands, link its threshold/evidence ledger here so native-port proposals can cite concrete corpus artifacts.\n",
|
|
58
58
|
"natives-addon-loader-runtime.md": "# Natives Addon Loader Runtime\n\nThis document covers the runtime loader shipped by `@gajae-code/natives`: how `native/index.js` decides which `.node` file to require, how compiled-binary embedded payloads are extracted, and what startup failures report.\n\n## Implementation files\n\n- `packages/natives/native/index.js`\n- `packages/natives/native/loader-state.js`\n- `packages/natives/native/embedded-addon.js`\n- `packages/natives/scripts/embed-native.ts`\n- `packages/natives/package.json`\n\n## Scope and responsibility\n\nThe loader is intentionally narrow:\n\n- Build a platform/CPU-aware candidate list for addon filenames and directories.\n- Treat an embedded-addon manifest as the authoritative compiled-binary signal when present.\n- Optionally materialize an embedded addon into a versioned per-user cache directory.\n- Attempt candidates in deterministic order and return the first addon that `require(...)` loads.\n\nThe current loader does **not** run a separate `validateNative(...)` export-presence gate. API shape is provided by the generated N-API binding file (`native/index.d.ts`) and the loaded addon itself. A stale binary therefore normally fails as a missing property or native load error rather than as a custom \"missing exports\" validation error.\n\n## Runtime inputs and derived state\n\nAt module initialization, `native/index.js` computes:\n\n- **Platform tag**: `${process.platform}-${process.arch}` (for example `darwin-arm64`).\n- **Package version**: from `packages/natives/package.json`.\n- **Core directories**:\n - `nativeDir`: package-local `packages/natives/native`.\n - `execDir`: directory containing `process.execPath`.\n - `versionedDir`: `<getNativesDir()>/<packageVersion>`.\n - `userDataDir` fallback:\n - Windows: `%LOCALAPPDATA%/gjc` or `%USERPROFILE%/AppData/Local/gjc`.\n - Non-Windows: `~/.local/bin`.\n- **Natives cache root** (`getNativesDir()`):\n - if `$XDG_DATA_HOME/gjc` exists, `$XDG_DATA_HOME/gjc/natives`;\n - otherwise `~/.gjc/natives`.\n- **Compiled-binary mode** (`detectCompiledBinary`): true if any of:\n - embedded-addon manifest is non-null,\n - `GJC_COMPILED` env var is set,\n - `import.meta.url` contains Bun embedded markers (`$bunfs`, `~BUN`, `%7EBUN`).\n- **Variant override**: `GJC_NATIVE_VARIANT` (`modern`/`baseline` only; invalid values ignored).\n- **Selected variant**: explicit override, otherwise runtime AVX2 detection on x64 (`modern` if AVX2, else `baseline`).\n\n## Platform support and tag resolution\n\n`SUPPORTED_PLATFORMS` is fixed to:\n\n- `linux-x64`\n- `linux-arm64`\n- `darwin-arm64`\n- `win32-x64`\n\nUnsupported platforms are not rejected before probing. The loader first tries the computed candidate paths. If all fail and `platformTag` is unsupported, it throws an unsupported-platform error listing supported tags.\n\n## Variant selection (`modern` / `baseline` / default)\n\n### x64 behavior\n\n1. `GJC_NATIVE_VARIANT=modern|baseline` wins when valid.\n2. Otherwise AVX2 support is detected:\n - Linux: scan `/proc/cpuinfo` for `avx2`.\n - macOS: `sysctl -n machdep.cpu.leaf7_features`, then `machdep.cpu.features`.\n - Windows: PowerShell `[System.Runtime.Intrinsics.X86.Avx2]::IsSupported`.\n3. AVX2 selects `modern`; unavailable or undetectable AVX2 selects `baseline`.\n\n### Non-x64 behavior\n\nNo variant suffix is used; the filename is `pi_natives.<platform>-<arch>.node`.\n\n### Filename construction\n\n`loader-state.js#getAddonFilenames` returns:\n\n- Non-x64 or no variant: `pi_natives.<tag>.node`\n- x64 + `modern`:\n 1. `pi_natives.<tag>-modern.node`\n 2. `pi_natives.<tag>-baseline.node`\n 3. `pi_natives.<tag>.node`\n- x64 + `baseline`:\n 1. `pi_natives.<tag>-baseline.node`\n 2. `pi_natives.<tag>.node`\n\nThe default unsuffixed fallback remains part of the x64 candidate list.\n\n## Candidate path construction and fallback ordering\n\n`resolveLoaderCandidates(...)` expands every filename across directories, then de-duplicates while preserving first occurrence order.\n\n### Non-compiled runtime\n\nFor each filename, candidates are:\n\n1. `<nativeDir>/<filename>`\n2. `<execDir>/<filename>`\n\n### Compiled runtime\n\nFor each filename, candidates are:\n\n1. `<versionedDir>/<filename>`\n2. `<userDataDir>/<filename>`\n3. `<nativeDir>/<filename>`\n4. `<execDir>/<filename>`\n\nAt load time, an extracted embedded candidate, when produced, is prepended ahead of these de-duplicated candidates.\n\n## Embedded addon extraction lifecycle\n\n`embedded-addon.js` is generated by `scripts/embed-native.ts`. The reset stub exports `embeddedAddon = null`. A populated manifest has:\n\n- `platformTag`\n- `version`\n- `files[]` entries with `variant`, `filename`, and `filePath`\n\nExtraction (`maybeExtractEmbeddedAddon`) runs only when:\n\n1. compiled-binary mode is true,\n2. `embeddedAddon` is non-null,\n3. manifest `platformTag` equals the runtime platform tag,\n4. manifest `version` equals the package version,\n5. a variant-appropriate embedded file exists.\n\nVariant file selection:\n\n- Non-x64: prefer `default`, then first available file.\n- x64 + `modern`: prefer `modern`, fallback to `baseline`.\n- x64 + `baseline`: require `baseline`.\n\nMaterialization:\n\n1. Ensure `<versionedDir>` exists.\n2. Reuse `<versionedDir>/<selected filename>` if it already exists.\n3. Otherwise read `selectedEmbeddedFile.filePath` and write the target path.\n4. Return the target path as the first candidate.\n\nDirectory creation or write failures are appended to the loader error list; probing continues through normal candidates.\n\n## Lifecycle and state transitions\n\n```text\nInit\n -> Load package metadata and embedded-addon manifest\n -> Compute platform/version/variant/filenames/candidate paths\n -> (compiled + embedded manifest matches?)\n yes -> try extract to versionedDir (record errors, continue)\n no -> skip extraction\n -> For each runtime candidate in order:\n require(candidate)\n -> success: return addon exports (READY)\n -> failure: record error, continue\n -> none loaded:\n if unsupported platform tag -> throw Unsupported platform\n else -> throw Failed to load (tried-path diagnostics + hints)\n```\n\n## Failure behavior and diagnostics\n\n### Unsupported platform\n\nIf all candidates fail and `platformTag` is not supported, the loader throws:\n\n- `Unsupported platform: <tag>`\n- supported platform list\n- issue-reporting guidance\n\n### No loadable candidate\n\nIf the platform is supported but no candidate can be loaded, the final error includes:\n\n- `Failed to load pi_natives native addon for <platformTag>` or `<platformTag> (<variant>)`\n- every attempted path with the corresponding `require(...)` error\n- mode-specific remediation hints\n\n### Compiled-binary startup failures\n\nCompiled mode diagnostics include:\n\n- expected versioned cache target paths (`<versionedDir>/<filename>`),\n- remediation to delete the versioned cache and rerun,\n- direct release download `curl` commands for each expected filename.\n\n### Non-compiled startup failures\n\nNormal package/runtime diagnostics include:\n\n- reinstall hint (`bun install @gajae-code/natives`),\n- local rebuild command (`bun --cwd=packages/natives run build`),\n- optional x64 variant build hint (`TARGET_VARIANT=baseline|modern bun --cwd=packages/natives run build`).\n",
|
|
59
59
|
"natives-architecture.md": "# Natives Architecture\n\n`@gajae-code/natives` is now a two-layer package around a loader:\n\n1. **CommonJS loader/package entrypoint** resolves and loads the correct `.node` addon and patches generated enum objects onto the export object.\n2. **Rust N-API module layer** implements the exported functions/classes and emits the generated TypeScript declarations.\n\nThis document is the foundation for deeper module-level docs. Performance-motivated native ports of leftover algorithmic hot paths are additionally gated by [`native-ffi-optimization-policy.md`](./native-ffi-optimization-policy.md).\n\n## Implementation files\n\n- `packages/natives/native/index.js`\n- `packages/natives/native/index.d.ts`\n- `packages/natives/native/loader-state.js`\n- `packages/natives/native/embedded-addon.js`\n- `packages/natives/scripts/build-native.ts`\n- `packages/natives/scripts/embed-native.ts`\n- `packages/natives/scripts/gen-enums.ts`\n- `packages/natives/package.json`\n- `crates/pi-natives/src/lib.rs`\n\n## Package entrypoint and public surface\n\n`packages/natives/package.json` points directly at generated native bindings:\n\n- `main`: `./native/index.js`\n- `types`: `./native/index.d.ts`\n- `exports[\".\"].types`: `./native/index.d.ts`\n- `exports[\".\"].import`: `./native/index.js`\n\nThere is no current `packages/natives/src` TypeScript wrapper layer. Consumers import functions/classes/enums directly from `@gajae-code/natives`; the type contract is the generated `native/index.d.ts` plus enum exports appended by `scripts/gen-enums.ts`.\n\nCurrent capability groups in the generated API include:\n\n- **Search/text/code primitives**: `grep`, `search`, `hasMatch`, `fuzzyFind`, `glob`, `astGrep`, `astEdit`, text width/slicing/wrapping/sanitization, syntax highlighting.\n- **Execution/process/terminal primitives**: `executeShell`, `Shell`, `PtySession`, process-tree helpers, key parsing.\n- **System/media/conversion primitives**: clipboard, image resize/encode/SIXEL, HTML-to-Markdown, macOS appearance/power helpers, work profiling, Windows ProjFS overlay helpers.\n\n## Loader layer\n\n`packages/natives/native/index.js` owns runtime addon selection and optional embedded extraction.\n\n### Candidate resolution model\n\n- Platform tag is `${process.platform}-${process.arch}`.\n- Supported tags are currently:\n - `linux-x64`\n - `linux-arm64`\n - `darwin-arm64`\n - `win32-x64`\n- x64 can use CPU variants:\n - `modern` (AVX2-capable)\n - `baseline` (fallback)\n- Non-x64 uses the default filename without a variant suffix.\n\nFilename strategy:\n\n- Default: `pi_natives.<platform>-<arch>.node`\n- x64 variant: `pi_natives.<platform>-<arch>-modern.node` or `...-baseline.node`\n- x64 runtime fallback includes the unsuffixed default filename after variant candidates.\n\n### Platform-specific variant detection\n\nFor x64, variant selection uses:\n\n- Linux: `/proc/cpuinfo`\n- macOS: `sysctl -n machdep.cpu.leaf7_features`, then `machdep.cpu.features`\n- Windows: PowerShell check for `System.Runtime.Intrinsics.X86.Avx2`\n\n`GJC_NATIVE_VARIANT` can force `modern` or `baseline`; invalid values are ignored.\n\n### Binary distribution and extraction model\n\n`packages/natives/package.json` publishes `native/`, which contains the loader, generated declarations, generated enum patch, embedded-addon manifest stub, and prebuilt `.node` artifacts.\n\nFor compiled binaries, loader behavior is:\n\n1. Check versioned user cache path: `<getNativesDir()>/<packageVersion>/...`.\n2. Check legacy compiled-binary location:\n - Windows: `%LOCALAPPDATA%/gjc` (fallback `%USERPROFILE%/AppData/Local/gjc`)\n - non-Windows: `~/.local/bin`\n3. Fall back to packaged `native/` and executable directory candidates.\n\n`getNativesDir()` uses `$XDG_DATA_HOME/gjc/natives` when `$XDG_DATA_HOME/gjc` exists; otherwise it uses `~/.gjc/natives`.\n\nIf a populated embedded addon manifest is present, it is also treated as a compiled-binary signal. The loader can extract the matching embedded `.node` into the versioned cache directory before candidate probing.\n\n### Failure modes\n\nLoader failures are explicit:\n\n- **Unsupported platform tag**: after failed probing, throws with supported platform list.\n- **No loadable candidate**: throws with all attempted paths and remediation hints.\n- **Embedded extraction errors**: directory/write failures are recorded and included in final load diagnostics if no candidate loads.\n\nThe current loader does not perform a separate post-`require` export validation pass.\n\n## Rust N-API module layer\n\n`crates/pi-natives/src/lib.rs` declares exported module ownership:\n\n- `appearance`\n- `ast`\n- `clipboard`\n- `fd`\n- `fs_cache`\n- `glob`\n- `glob_util`\n- `grep`\n- `highlight`\n- `html`\n- `image`\n- `keys`\n- `language`\n- `power`\n- `prof`\n- `projfs_overlay`\n- `ps`\n- `pty`\n- `shell`\n- `task`\n- `text`\n- `tokens`\n- `utils` (crate-private helpers)\n\nN-API exports are generated from Rust `#[napi]` functions/classes/objects/enums. Snake_case Rust names are exposed as camelCase JavaScript names unless explicitly configured by napi-rs.\n\n## Ownership boundaries\n\n- **Loader/package ownership (`packages/natives/native`, `packages/natives/scripts`)**\n - runtime binary selection\n - CPU variant selection and override handling\n - compiled-binary embedded extraction\n - generated TypeScript declarations and enum export patching\n- **Rust ownership (`crates/pi-natives/src`)**\n - algorithmic and system-level implementation\n - platform-native behavior and performance-sensitive logic\n - N-API symbol implementation consumed directly by package callers\n- **Consumer ownership (`packages/coding-agent`, `packages/tui`)**\n - user-facing policy and fallbacks that are not built into the native API\n - higher-level rendering, artifact, shell-session, and command behavior\n\n## Runtime flow (high level)\n\n1. Consumer imports from `@gajae-code/natives`.\n2. `native/index.js` computes platform/arch/variant and candidate paths.\n3. Optional embedded binary extraction occurs for compiled distributions.\n4. The first `require(candidate)` that succeeds becomes the exported addon object.\n5. Generated enum objects are appended to `module.exports`.\n6. Caller invokes generated N-API functions/classes directly.\n\n## Glossary\n\n- **Native addon**: A `.node` binary loaded via Node-API (N-API).\n- **Platform tag**: Runtime tuple `platform-arch` (for example `darwin-arm64`).\n- **Variant**: x64 CPU-specific build flavor (`modern` AVX2, `baseline` fallback).\n- **Generated binding declaration**: `native/index.d.ts` emitted by napi-rs during `build-native.ts`.\n- **Compiled binary mode**: Runtime mode where the CLI is bundled and native addons are resolved from embedded/cache paths before package-local paths.\n- **Embedded addon**: Build artifact metadata and file references generated into `native/embedded-addon.js` so compiled binaries can extract matching `.node` payloads.\n",
|
|
@@ -90,17 +90,17 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
90
90
|
"sdk-app-guide.md": "# Building Applications on the Gajae-Code SDK\n\nA beginner-friendly guide to using Gajae-Code as the **agent runtime for your own\napplication** — mobile apps, desktop apps, custom web frontends, chat bots, and\nvertical AI products.\n\n> Proof that this works in production: the bundled **Telegram, Discord, and Slack\n> integrations are SDK-core managed adapters**. They use opaque Router attachments\n> and provider-neutral presentation contracts rather than private endpoint credentials.\n\nRelated references:\n\n- [SDK wire protocol & machine interfaces](./sdk.md) — internal wire semantics and supported managed adapters\n- [Embedding SDK](./sdk-embedding.md) — the in-process TypeScript API\n- [External control readiness](./external-control-readiness.md) — supported surfaces\n\n## Why build on Gajae-Code?\n\nEvery vertical AI app ends up needing the same backend pieces: an agentic loop,\ntool execution, session persistence, model/auth management, streaming, retries,\nand compaction. Some also need a configured remote-notification integration.\nTeams keep rebuilding these from scratch.\n\nGajae-Code packages the runtime as a reusable component:\n\n- **Drop the agentic loop from your codebase.** `createAgentSession()` gives you\n a production agent loop (tools, retries, compaction, session files, model\n fallback chains) in one call.\n- **A managed machine interface is available.** Top-level sessions host an internal\n loopback endpoint, while SDK-core `SessionRouter` retains its discovery record,\n credentials, replay cursor, and exact attachment authority. Applications use\n Coordinator MCP, the SDK session CLI, or a configured managed adapter rather\n than opening that endpoint directly.\n- **Many subscribers, one session.** In-process subscribers and configured managed\n adapters can observe the same session without sharing endpoint credentials.\n- **Not just for coding.** Tools, skills, rules, and the system prompt are all\n injectable, so the same runtime powers legal assistants, research agents,\n data-analysis products — any vertical.\n\n\n## The two supported integration surfaces\n\n| | Embedding SDK (in-process) | Managed external control |\n| --- | --- | --- |\n| What it is | Import `@gajae-code/coding-agent` as a library | Use Coordinator MCP, SDK session CLI, or a configured Telegram/Discord/Slack adapter |\n| Language | TypeScript / Bun (Node-compatible) | Any client capable of the selected MCP/CLI/provider interface |\n| Telemetry | Full token deltas, tool events, and session events | Curated provider-neutral frames and queries |\n| Trust model | Your process hosts the runtime | SDK core retains endpoint credentials and issues exact opaque attachments |\n| Typical consumer | Your app UI and business logic | Bots, dashboards, and orchestrators |\n\nA common production shape combines an in-process UI with one or more configured\nmanaged adapters. Applications never discover endpoint records or open raw session\nWebSockets.\n\n\n## Quick start: embed the runtime\n\n```bash\nbun add @gajae-code/coding-agent\n```\n\n```ts\nimport { createAgentSession } from \"@gajae-code/coding-agent\";\n\nconst { session } = await createAgentSession();\n\nsession.subscribe((event) => {\n if (\n event.type === \"message_update\" &&\n event.assistantMessageEvent.type === \"text_delta\"\n ) {\n process.stdout.write(event.assistantMessageEvent.delta); // token-level stream\n }\n});\n\nawait session.prompt(\"Summarize this repository in 3 bullets.\");\nawait session.dispose();\n```\n\n`createAgentSession()` follows *provide to override, omit to discover*: with no\noptions it auto-discovers auth, models, settings, tools, context files, and a\nfile-backed session store. Everything is overridable.\n\n## Customizing the runtime for your vertical\n\nThis is the part that turns Gajae-Code from \"a coding agent\" into a general\nexecution runtime. All of the following are `createAgentSession()` options; see\nthe [Embedding SDK](./sdk-embedding.md) for the public API.\n\n### Restrict or drop tools\n\n```ts\nconst { session } = await createAgentSession({\n // Allowlist of built-ins — everything else is dropped.\n toolNames: [\"read\", \"grep\", \"find\"],\n // Optionally restrict bash to specific command prefixes.\n bashAllowedPrefixes: [\"git status\", \"git log\"],\n});\n```\n\nRuntime changes are also supported: `session.getActiveToolNames()`,\n`session.getAllToolNames()`, `session.setActiveToolsByName(names)` — the system\nprompt is rebuilt automatically.\n\n### Add custom tools\n\n```ts\nconst { session } = await createAgentSession({\n toolNames: [\"read\"],\n customTools: [myDomainTool], // CustomTool | ToolDefinition\n // Or bring tools from an MCP server you own:\n mcpConfigPath: \"/abs/path/to/mcp-config.json\",\n});\n```\n\n### Inject skills, rules, and identity\n\n```ts\nconst { session } = await createAgentSession({\n skills: myVerticalSkills, // replaces bundled skill discovery\n rules: myRules,\n contextFiles: [{ path: \"DOMAIN.md\", content: domainKnowledge }],\n systemPrompt: (defaults) => [...defaults, myVerticalPromptBlock],\n promptTemplates: myTemplates,\n});\n```\n\n### Isolate state for request-scoped agents\n\n```ts\nimport { SessionManager, Settings } from \"@gajae-code/coding-agent\";\n\nconst { session } = await createAgentSession({\n sessionManager: SessionManager.inMemory(), // no filesystem persistence\n settings: Settings.isolated({ \"compaction.enabled\": true }),\n});\n```\n\n### Structured-output subagents\n\n`outputSchema`, `requireYieldTool`, `taskDepth`, and `parentTaskPrefix` support\norchestrator patterns where a session must return machine-readable results.\n\n### Observability\n\nPass `telemetry: {}` to enable OpenTelemetry GenAI-semantic-convention spans\n(no-op unless an OTEL SDK is registered in your host).\n\n## Attach from another process\n\nProcess-isolated integrations use an SDK-core adapter backed by `SessionRouter`. Endpoint discovery records and bearer tokens are internal implementation details; applications must not read them or open raw session WebSockets.\n\nManaged adapters receive opaque `SessionAttachment` capabilities for live controls and submit lifecycle mutations through `SessionLifecycleService`. See [sdk.md](./sdk.md) for the ownership and adapter contract.\n\nThe `models.list/current` (Q10) catalog also lists model profiles as synthetic\n`gajae-code/<profile>` entries (e.g. `gajae-code/codex-eco`). Treat them as\nlogical selections, not API providers: sending the id back through `model.set`\nactivates the profile for the live session only. Persisting remains an explicit\nTUI choice. Request Q27 (`models.profiles.list`) when you need the\nfull profile catalog including unavailable profiles and their `available`\nstatus. See [Model profiles as synthetic models](./sdk.md#model-profiles-as-synthetic-models-gajae-codeprofile).\n\n## Creating and supervising sessions\n\nEmbedding creates a session directly with `createAgentSession()`. For an\nexternal controller that needs lifecycle operations, use Coordinator MCP or the\nSDK session CLI. A lifecycle CLI request names the `global` action,\nprovides its operation and JSON input, and supplies a caller-chosen idempotency\nkey:\n\n```bash\ngjc sdk session raw global --op session.create \\\n --idempotency-key <unique-key> \\\n --json-input '{\"cwd\":\"/absolute/path/to/repo\"}'\n```\n\nThe CLI connects to the broker as needed; broker bootstrap is not an embedder\nAPI. See the [external controller integration guide](./bot-integration.md#integration-surfaces)\nfor the supported controller surfaces and lifecycle constraints.\n\n\n## Application recipes\n\n- **Vertical AI app.** Embed with `toolNames`, `customTools`, `skills`, and a domain\n `systemPrompt`. Subscribe in-process for full-fidelity streaming.\n- **Custom web app or dashboard.** Keep the agent runtime in your backend process;\n expose your own authenticated product API, or use Coordinator MCP for supported\n external orchestration. Do not relay the internal session endpoint.\n- **Mobile or desktop companion.** Pair through a configured managed notification\n adapter for actions and approvals, or call an application backend that embeds GJC.\n- **Fleet orchestrator.** Use Coordinator MCP or daemon-session lifecycle operations\n to create and supervise worktree-scoped sessions.\n\n## External interface constraints\n\n- Endpoint records, bearer tokens, and raw session WebSockets are private SDK-core\n implementation details.\n- `config.patch` rejects secret fields, and `session.get_endpoint` is prohibited on\n public adapters.\n- Full-fidelity token deltas remain an in-process embedding capability.\n- Action identity is fail-closed: stale IDs never regain authority.\n\nDestructive operations (`session.delete`, `context.clear`) require\n`confirm: true`.\n\n## FAQ\n\n**Is embedding a subprocess?** No — it is a library import; the agent loop runs\nin your process. Process-isolated control uses Coordinator MCP, SDK session CLI,\nor a configured managed adapter.\n\n**Can multiple consumers watch one session?** Yes. In-process subscribers and\nmanaged adapters are additive; replies to asks remain first-valid-wins.\n\n**Can the TUI and my code share a session?** Use in-process embedding for code that\nhosts the runtime, or Coordinator MCP/managed adapters for process-isolated control.\nSession files remain resumable through canonical lifecycle operations.\n\n**I need another language.** Use Coordinator MCP or your own authenticated backend\naround the TypeScript embedding SDK. Dedicated embedding-like Rust/Python SDKs are\nroadmap work, not raw endpoint contracts.\n",
|
|
91
91
|
"sdk-embedding.md": "# SDK\n\nFor managed control and notification attachment policy, see [the Gajae-Code SDK](./sdk.md).\n\nThe SDK is the in-process integration surface for `@gajae-code/coding-agent`.\nUse it when you want direct access to agent state, event streaming, tool wiring, and session control from your own Bun/Node process.\n\nFor process-isolated control, use a managed SDK-core adapter backed by `SessionRouter`; raw endpoint discovery and direct WebSocket clients are not public integration surfaces.\n\n## Installation\n\n```bash\nbun add @gajae-code/coding-agent\n```\n\n\n## Entry points\n\n`@gajae-code/coding-agent/sdk` is the canonical entry point for embedders. The package root exports the same SDK APIs for convenience.\n\nCore exports for embedders:\n\n- `createAgentSession`\n- `SessionManager`\n- `Settings`\n- `AuthStorage`\n- `ModelRegistry`\n- `discoverAuthStorage`\n- Discovery helpers for retained context/prompt surfaces (`discoverContextFiles`, `discoverPromptTemplates`)\n- Tool factory surface (`createTools`, `BUILTIN_TOOLS`, tool classes)\n\n## Quick start (auto-discovery defaults)\n\n```ts\nimport { createAgentSession } from \"@gajae-code/coding-agent\";\n\nconst { session, modelFallbackMessage } = await createAgentSession();\n\nif (modelFallbackMessage) {\n process.stderr.write(`${modelFallbackMessage}\\n`);\n}\n\nconst unsubscribe = session.subscribe((event) => {\n if (\n event.type === \"message_update\" &&\n event.assistantMessageEvent.type === \"text_delta\"\n ) {\n process.stdout.write(event.assistantMessageEvent.delta);\n }\n});\n\nawait session.prompt(\"Summarize this repository in 3 bullets.\");\nunsubscribe();\nawait session.dispose();\n```\n\n## What `createAgentSession()` discovers by default\n\n`createAgentSession()` follows “provide to override, omit to discover”.\n\nIf omitted, it resolves:\n\n- `cwd`: `getProjectDir()`\n- `agentDir`: `~/.gjc/agent` (via `getAgentDir()`)\n- `authStorage`: `discoverAuthStorage(agentDir)`\n- `modelRegistry`: `new ModelRegistry(authStorage)` + background `refreshInBackground()` when the registry is not provided\n- `settings`: `await Settings.init({ cwd, agentDir })`\n- `sessionManager`: `SessionManager.create(cwd)` (file-backed)\n- context files and prompt templates\n- built-in tools via `createTools(...)`\n- LSP integration (enabled by default)\n- `eventBus`: new `EventBus()` unless supplied\n\n### Required vs optional inputs\n\nTypically you must provide only what you want to control:\n\n- **Must provide**: nothing for a minimal session\n- **Usually provide explicitly** in embedders:\n - `sessionManager` (if you need in-memory or custom location)\n - `authStorage` + `modelRegistry` (if you own credential/model lifecycle)\n - `model` or `modelPattern` (if deterministic model selection matters)\n - `settings` (if you need isolated/test config)\n\n## Session manager behavior (persistent vs in-memory)\n\n`AgentSession` always uses a `SessionManager`; behavior depends on which factory you use.\n\n### File-backed (default)\n\n```ts\nimport { createAgentSession, SessionManager } from \"@gajae-code/coding-agent\";\n\nconst { session } = await createAgentSession({\n sessionManager: SessionManager.create(process.cwd()),\n});\n\nconsole.log(session.sessionFile); // absolute .jsonl path\n```\n\n- Persists conversation/messages/state deltas to session files.\n- Supports resume/open/list/fork workflows.\n- `session.sessionFile` is defined.\n\n### In-memory\n\n```ts\nimport { createAgentSession, SessionManager } from \"@gajae-code/coding-agent\";\n\nconst { session } = await createAgentSession({\n sessionManager: SessionManager.inMemory(),\n});\n\nconsole.log(session.sessionFile); // undefined\n```\n\n- No filesystem persistence.\n- Useful for tests, ephemeral workers, request-scoped agents.\n- Session methods still work, but persistence-specific behaviors (file resume/fork paths) are naturally limited.\n\n### Resume/open/list helpers\n\n```ts\nimport { SessionManager } from \"@gajae-code/coding-agent\";\n\nconst recent = await SessionManager.continueRecent(process.cwd());\nconst listed = await SessionManager.list(process.cwd());\nconst opened = listed[0] ? await SessionManager.open(listed[0].path) : null;\n```\n\n## Model and auth wiring\n\n`createAgentSession()` uses `ModelRegistry` + `AuthStorage` for model selection and API key resolution.\n\n### Explicit wiring\n\n```ts\nimport {\n createAgentSession,\n discoverAuthStorage,\n ModelRegistry,\n SessionManager,\n} from \"@gajae-code/coding-agent\";\n\nconst authStorage = await discoverAuthStorage();\nconst modelRegistry = new ModelRegistry(authStorage);\nawait modelRegistry.refresh();\n\nconst available = modelRegistry.getAvailable();\nif (available.length === 0)\n throw new Error(\"No authenticated models available\");\n\nconst { session } = await createAgentSession({\n authStorage,\n modelRegistry,\n model: available[0],\n thinkingLevel: \"medium\",\n sessionManager: SessionManager.inMemory(),\n});\n```\n\n### Selection order when `model` is omitted\n\nWhen no explicit `model`/`modelPattern` is provided:\n\n1. restore model from existing session (if restorable + key available)\n2. settings default model role (`default`)\n3. first available model with valid auth\n\nIf restore fails, `modelFallbackMessage` explains fallback.\n\n### Auth priority\n\n`AuthStorage.getApiKey(...)` resolves in this order:\n\n1. runtime override (`setRuntimeApiKey`)\n2. stored credentials in `agent.db`\n3. provider environment variables\n4. custom-provider resolver fallback (if configured)\n\n## Event subscription model\n\nSubscribe with `session.subscribe(listener)`; it returns an unsubscribe function.\n\n```ts\nconst unsubscribe = session.subscribe((event) => {\n switch (event.type) {\n case \"agent_start\":\n case \"turn_start\":\n case \"tool_execution_start\":\n break;\n case \"message_update\":\n if (event.assistantMessageEvent.type === \"text_delta\") {\n process.stdout.write(event.assistantMessageEvent.delta);\n }\n break;\n }\n});\n```\n\n`AgentSessionEvent` includes core `AgentEvent` plus session-level events:\n\n- `auto_compaction_start` / `auto_compaction_end`\n- `auto_retry_start` / `auto_retry_end`\n- `agent_failed` (diagnostic failure for the current attempt; the correlated\n terminal `agent_end` still follows and remains the lifecycle boundary)\n- `retry_fallback_applied` / `retry_fallback_succeeded`\n- `ttsr_triggered`\n- `todo_reminder` / `todo_auto_clear`\n- `irc_message`\n\n## Prompt lifecycle\n\n`session.prompt(text, options?)` is the primary entry point.\n\nFailure consumers should handle `agent_failed` as an additive diagnostic event,\nnot as terminal completion. It carries only the sanitized `{ code, message }`\ndiagnostic for the correlated attempt — never the raw provider/local error —\nso consumers must not depend on provider-specific detail or raw error objects;\ncontinue waiting for `agent_end` before releasing prompt ownership, deadlines,\nor transport resources. This ordering also applies when the run is canceled or\nenters a maintenance continuation.\n\nBehavior:\n\n1. optional command/template expansion (`/` commands, custom commands, file slash commands, prompt templates)\n2. if currently streaming:\n - requires `streamingBehavior: \"steer\" | \"followUp\"`\n - queues instead of throwing work away\n3. if idle:\n - validates model + API key\n - appends user message\n - starts agent turn\n\nRelated APIs:\n\n- `sendUserMessage(content, { deliverAs? })`\n- `steer(text, images?)`\n- `followUp(text, images?)`\n- `sendCustomMessage({ customType, content, ... }, { deliverAs?, triggerTurn? })`\n- `abort()`\n\n## Tools integration\n\n### Built-ins and filtering\n\n- Built-ins come from `createTools(...)` and `BUILTIN_TOOLS`.\n- `toolNames` acts as an allowlist for built-ins.\n- Hidden tools (for example `yield`) are opt-in unless required by options.\n\n```ts\nconst { session } = await createAgentSession({\n toolNames: [\"read\", \"search\", \"find\", \"write\"],\n requireYieldTool: true,\n});\n```\n\n### Runtime tool set changes\n\n`AgentSession` supports runtime activation updates:\n\n- `getActiveToolNames()`\n- `getAllToolNames()`\n- `setActiveToolsByName(names)`\n\nSystem prompt is rebuilt to reflect active tool changes.\n\n## Discovery helpers\n\nUse these when you want partial control without recreating internal discovery logic:\n\n- `discoverAuthStorage(agentDir?)`\n- `discoverContextFiles(cwd?, _agentDir?)`\n- `discoverPromptTemplates(cwd?, agentDir?)`\n- `buildSystemPrompt(options?)`\n\n## Subagent-oriented options\n\nFor SDK consumers building orchestrators (similar to task executor flow):\n\n- `outputSchema`: passes structured output expectation into tool context\n- `requireYieldTool`: forces `yield` tool inclusion\n- `taskDepth`: recursion-depth context for nested task sessions\n- `parentTaskPrefix`: artifact naming prefix for nested task outputs\n\nThese are optional for normal single-agent embedding.\n\n## `createAgentSession()` return value\n\n```ts\ntype CreateAgentSessionResult = {\n session: AgentSession;\n setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void;\n modelFallbackMessage?: string;\n lspServers?: Array<{\n name: string;\n status: \"ready\" | \"error\";\n fileTypes: string[];\n error?: string;\n }>;\n eventBus: EventBus;\n};\n```\n\nUse `setToolUIContext(...)` only if your embedder provides UI capabilities that tools should call into.\n\n## Startup performance\n\n`createAgentSession()` runs two background optimizations to overlap I/O with the rest of session setup:\n\n- **Model-host preconnect.** As soon as the model is resolved, the SDK fires a best-effort `fetch.preconnect(model.baseUrl)` so DNS + TCP + TLS + HTTP/2 to the provider's host happens in parallel with tool registry build, and system-prompt assembly. The first real `fetch(...)` then reuses the warm connection, saving 100–300 ms on transcontinental hops (e.g. residential IP → `api.anthropic.com`). Implementation lives in `preconnectModelHost()` in `packages/coding-agent/src/sdk/session.ts`. If `fetch.preconnect` is unavailable (non-Bun runtime) or the call throws, the optimization is silently skipped — never a hard dependency. Applies to interactive, print, and ACP modes.\n- **Conditional LSP warmup.** Startup LSP servers (those returned by `discoverStartupLspServers(cwd)`) are only warmed when **all** of these hold:\n - `enableLsp !== false` on the session options, **and**\n - `options.hasUI === true` (interactive TUI), **and**\n - the `lsp.diagnosticsOnWrite` setting is enabled.\n\n Print, script, and ACP invocations (`hasUI=false`) skip the warmup entirely: they don't render the warmup status indicator and typically finish before the language servers would stabilize, so warming them just spends CPU parsing big `initialize` responses concurrently with the LLM stream consumer and jitters perceived latency. Tools that actually need an LSP server still spin one up on demand through `getOrCreateClient()` — only the *startup* warmup is skipped. The returned `lspServers` field in `CreateAgentSessionResult` is therefore `undefined` (not an empty array) whenever the warmup branch was bypassed.\n\n## Minimal controlled embed example\n\n```ts\nimport {\n createAgentSession,\n discoverAuthStorage,\n ModelRegistry,\n SessionManager,\n Settings,\n} from \"@gajae-code/coding-agent\";\n\nconst authStorage = await discoverAuthStorage();\nconst modelRegistry = new ModelRegistry(authStorage);\nawait modelRegistry.refresh();\n\nconst settings = Settings.isolated({\n \"compaction.enabled\": true,\n \"retry.enabled\": true,\n});\n\nconst { session } = await createAgentSession({\n authStorage,\n modelRegistry,\n settings,\n sessionManager: SessionManager.inMemory(),\n toolNames: [\"read\", \"search\", \"find\", \"edit\", \"write\"],\n enableLsp: true,\n});\n\nsession.subscribe((event) => {\n if (\n event.type === \"message_update\" &&\n event.assistantMessageEvent.type === \"text_delta\"\n ) {\n process.stdout.write(event.assistantMessageEvent.delta);\n }\n});\n\nawait session.prompt(\"Find all TODO comments in this repo and propose fixes.\");\nawait session.dispose();\n```\n",
|
|
92
92
|
"sdk-owned-session-lifecycle-handoff.md": "# SDK-owned session lifecycle handoff\n\n## Status\n\n- Branch: `refactor/sdk-owned-session-lifecycle`\n- Worktree: `../gajae-code.gajae-code-worktrees/refactor-sdk-owned-session-lifecycle`\n- Base: `origin/dev` at `31c00c2d5bb462464046208a94a4597c27260a4c`\n- Review target: `dev`\n- Verdict: sound direction, with a required ownership qualification\n\n## Decision\n\nGJC SDK core must be the sole authority for coding-agent session lifecycle and external session attachments. Provider daemons must not create, resume, close, delete, signal, recover, or allocate identities for GJC sessions.\n\n> SDK core owns session lifecycle and attachment authority. Provider supervisors own provider transport authority.\n\nTelegram `getUpdates`, Slack Socket Mode, Discord Gateway, provider cursors, rate limits, and provider topic/thread/message state remain provider-owned because they are external transport and presentation resources, not AgentSession lifecycle resources.\n\n## Problem\n\nThe Broker and per-session SDK runtime already provide lifecycle idempotency, target serialization, a durable lifecycle ledger, startup admission, process-incarnation fencing, effect markers, readiness proof, endpoint-generation checks, exact cleanup evidence, and honest `terminal_uncertain` outcomes.\n\nThe chat-daemon path nevertheless has a second set of session authorities:\n\n- daemon-owned lifecycle control server, credential, pending table, and sequence;\n- chat lifecycle ledger and audit path;\n- tmux/session spawn, close, and resume effects;\n- intended session-ID allocation before Broker creation;\n- notification-root registration as the provider discovery universe;\n- direct endpoint discovery, credentials, reconnect, and action routing.\n\nA provider crash can therefore occur between provider-ledger persistence, Broker-ledger persistence, process effects, endpoint publication, and topic binding. Authority is divided by ingress process instead of by the mutated resource.\n\n## Non-goals\n\n- Do not move provider transport ownership into the Broker.\n- Do not weaken incarnation, generation, effect-marker, readiness, or exact-cleanup checks.\n- Do not add a compatibility lifecycle executor or dual writer.\n- Do not infer completion from terminal output, tmux, or provider delivery state.\n- Do not make topic/thread cleanup part of session lifecycle terminality.\n\n## Current ownership\n\n### Broker and lifecycle executor\n\nPrimary files:\n\n- `packages/coding-agent/src/sdk/broker/broker.ts`\n- `packages/coding-agent/src/sdk/broker/lifecycle.ts`\n\nRetain and strengthen:\n\n- Broker root lock/discovery/heartbeat;\n- `SessionIndex` and `LifecycleLedger`;\n- lifecycle idempotency and target serialization;\n- create/fork/resume/close/delete execution;\n- worktree, transcript, and managed-session validation;\n- effect marker and process-incarnation evidence;\n- readiness and endpoint-generation proof;\n- close escalation and exact cleanup reconciliation.\n\nThe Broker lifecycle executor becomes the only executor and durable terminal authority for session lifecycle mutations.\n\n### Per-session SDK runtime\n\nPrimary file: `packages/coding-agent/src/sdk/host/session-runtime.ts`.\n\nRetain:\n\n- AgentSession-local host/transport startup and shutdown;\n- endpoint URL, token, and generation publication;\n- control, query, event, replay, and reverse-provider semantics;\n- readiness and activation;\n- exact generation registration and unregistration with Broker.\n\nThe runtime owns live local resources; Broker owns the global transaction and permission to create or retire the runtime.\n\n### Provider-owned transport and presentation\n\nRetain outside SDK session lifecycle:\n\n- provider credential/account fingerprint;\n- one active provider poller or connection;\n- cursor, conflict handling, rate limit, retry, and backoff;\n- topic/thread/message/callback/attachment state;\n- provider delivery receipts and presentation reconciliation;\n- provider process lease, heartbeat, restart, and shutdown.\n\n### Authority to remove from Telegram/chat daemon\n\nPrimary files:\n\n- `packages/coding-agent/src/sdk/bus/telegram-daemon.ts`\n- `packages/coding-agent/src/sdk/bus/lifecycle-orchestrator.ts`\n\nRemove:\n\n- lifecycle control server ownership;\n- lifecycle endpoint credential and pending request ownership;\n- chat-owned lifecycle outcome ledger;\n- tmux/process lifecycle effects;\n- logical SessionId allocation or preallocation;\n- direct endpoint credentials and endpoint discovery;\n- provider-owned session discovery roots;\n- provider-specific endpoint-generation and reconnect authority.\n\nProvider update IDs may remain idempotency inputs, but not a second lifecycle result ledger.\n\n## Target architecture\n\n```text\nProvider boundary\n┌─────────────────────────────────────────────────────────────┐\n│ ChatDaemonSupervisor │\n│ provider lease, poller/connection, cursor, retry/rate limit│\n│ │\n│ ProviderAdapter │\n│ provider update → ChatIntent │\n│ PresentationEvent → provider payload │\n│ topic/thread/message/callback mappings │\n└────────────────────────┬────────────────────────────────────┘\n │ opaque intents/events\n ▼\nSDK core\n┌─────────────────────────────────────────────────────────────┐\n│ SessionRouter │\n│ Broker-index discovery, endpoint credential custody, │\n│ SDK connections, replay/reconnect, attachment leases, │\n│ prompt and ask-answer routing │\n│ │\n│ SessionLifecycleService │\n│ actor/capability authorization, canonical requests, │\n│ idempotency namespace, lifecycle certainty │\n│ │\n│ Broker │\n│ one ledger, one executor, index, process/effect recovery │\n│ │\n│ SessionSdkSessionRuntime │\n│ AgentSession, endpoint generation, controls/events/teardown│\n└─────────────────────────────────────────────────────────────┘\n```\n\nDependency direction:\n\n```text\nProviderAdapter\n → SessionRouter\n → SessionLifecycleService\n → Broker\n → SessionSdkSessionRuntime\n```\n\nForbidden paths:\n\n- Broker/runtime depending on provider concepts;\n- provider adapter calling Broker lifecycle internals;\n- provider adapter invoking tmux or process signals for session lifecycle;\n- provider adapter reading endpoint files or tokens;\n- provider adapter generating SessionIds;\n- provider state participating in Broker recovery proof.\n\n## Target components\n\n### SessionLifecycleService\n\nRecommended under `packages/coding-agent/src/sdk/lifecycle/`. It is a provider-neutral facade over existing Broker operations, not another state machine or ledger.\n\nResponsibilities:\n\n- authenticated actor and explicit capability input;\n- operation-specific authorization;\n- canonical lifecycle request normalization;\n- idempotency derived from actor namespace, caller request key, operation, and target;\n- invocation of the existing Broker path;\n- bounded lifecycle result and certainty;\n- no provider, topic, thread, tmux, or shell concepts.\n\n### SessionRouter\n\nRecommended under `packages/coding-agent/src/sdk/router/`.\n\nResponsibilities:\n\n- consume Broker `SessionIndex` snapshots and index sequence;\n- resolve exact endpoint authority through Broker;\n- remain the sole non-runtime holder of endpoint credentials;\n- own SDK connections, event replay, and reconnect;\n- revoke attachment leases when endpoint/index/router generation changes;\n- route provider-neutral prompts and answers through standard SDK operations;\n- reconcile prompt acceptance and terminal outcomes;\n- emit provider-neutral presentation events;\n- never perform process lifecycle effects.\n\nAttachment authority must bind session identity, endpoint generation/incarnation, and router epoch. Its provider representation should be opaque.\n\n### ChatDaemonSupervisor\n\nOwns only the provider-account process lease, poller/connection, cursor, API retry/backoff/rate limits, and independent provider restart/shutdown. It never creates, resumes, closes, or signals a GJC session.\n\n### ProviderAdapter\n\nValidates inbound provider identity, normalizes `ChatIntent`, renders `PresentationEvent`, owns provider-created presentation bindings and delivery receipts, and stores returned SessionIds only as opaque mapping keys.\n\n## Ownership matrix\n\n| Authority | Creator | Mutator/retirer | Persistence | Provider role |\n|---|---|---|---|---|\n| Logical SessionId | Broker lifecycle | Broker lifecycle | Broker/managed session | Store returned opaque ID |\n| Lifecycle idempotency | Service derives, Broker records | Broker ledger | `LifecycleLedger` | Supply stable request identity |\n| Broker root lease | Broker | Exact owner | lock/discovery/heartbeat | None |\n| Effect marker | Broker | Broker | ledger/lifecycle artifacts | None |\n| Process incarnation | OS, observed by core | Immutable | effect evidence | None |\n| Endpoint URL/token/generation | Session runtime | Session runtime | endpoint/index | Must not receive |\n| Attachment lease | SessionRouter | SessionRouter | Prefer ephemeral | Use opaque handle |\n| Provider transport lease | Supervisor | Supervisor | provider state | Full owner |\n| Provider cursor/rate state | Supervisor | Supervisor | provider state | Full owner |\n| Topic/thread/message binding | Adapter | Adapter CAS | provider mapping | Full owner |\n| Session lifecycle result | Broker | Broker ledger | `LifecycleLedger` | Read only |\n| Provider delivery result | Adapter | Adapter | provider delivery state | Cannot alter lifecycle |\n\n## Lifecycle flows\n\n### Create\n\n1. Adapter validates provider identity and policy.\n2. Existing topic/thread adoption creates a provider-local reservation keyed by `providerRequestKey`, not a SessionId.\n3. Adapter sends a create intent to Router.\n4. Router calls lifecycle service with actor, capability, target, and stable idempotency.\n5. Broker begins the sole ledger entry, allocates SessionId, records effect identity, spawns, and proves readiness.\n6. Router attaches to the exact endpoint authority.\n7. Adapter CAS-binds its reservation to the returned SessionId.\n8. Retry of the same provider request returns the same Broker result and cannot spawn another session.\n\n### Attach\n\nRouter reads Broker index, resolves exact generation/incarnation, opens the authenticated SDK connection, replays from its cursor, and issues an attachment lease. Provider stores only the opaque attachment and presentation mapping.\n\n### Prompt and ask answer\n\nAdapter sends an authenticated intent with provider request identity and attachment lease. Router validates authority and invokes standard SDK controls. Runtime reconciliation remains authoritative for accepted, in-flight, terminal, unknown, and one-shot answer outcomes. Provider retries query/replay rather than duplicate effects.\n\n### Close\n\nAdapter submits intent only. Lifecycle service authorizes and Broker persists the effect, uses the exact endpoint first, escalates only with incarnation/effect fencing, and requires unregister, endpoint removal, and process-exit proof. Router revokes attachments. Provider presentation cleanup happens afterward and cannot make a confirmed close uncertain.\n\n### Resume and endpoint rotation\n\nBroker validates saved-session ownership and returns an exact live endpoint or performs canonical spawn/readiness. Runtime owns token/generation rotation and Broker registration. Router revokes old leases, resolves the new endpoint, reconnects, and replays. Provider never receives old or new credentials.\n\n### Provider restart\n\nSupervisor reacquires only provider transport authority and reloads cursor/presentation state. Router reconstructs attachments from Broker index. Provider restart alone never creates, resumes, closes, or mutates a session. Submitted operations replay through the same Broker idempotency identity.\n\n### Broker/runtime recovery\n\nBroker reconciles only from its ledger, effect markers, process incarnation, endpoint/index registration, readiness/failure records, and exact cleanup evidence. Provider records are never lifecycle proof. Unprovable effects remain `terminal_uncertain`.\n\n## Migration plan\n\nEach stage cuts over one authority and deletes the obsolete path in the same logical change. No dual writers or fallback executors.\n\n### Stage 1: canonical lifecycle service\n\n- Add provider-neutral lifecycle service over existing Broker operations.\n- Define actor/capability, canonical request, idempotency, and certainty contracts.\n- Reuse `Broker.handleRequest`, `executeLifecycle`, and `LifecycleLedger`.\n- Add focused authorization, replay, conflict, and uncertainty tests.\n\n### Stage 2: chat lifecycle cutover\n\n- Route Telegram create/close/resume through Router and lifecycle service.\n- Preserve provider parsing, authentication, policy, and rendering.\n- Remove daemon lifecycle server, token, client, pending table, request sequence, chat lifecycle ledger/audit, and tmux/process effects.\n- Verify one chat operation creates one Broker ledger identity.\n\n### Stage 3: remove provider SessionId allocation\n\n- Replace `intendedSessionId` with a provider-local reservation keyed by provider request identity.\n- CAS-bind the provider resource to the Broker-returned SessionId.\n- Test crashes before Broker call, after Broker intent, after readiness, and before binding.\n\n### Stage 4: SDK-owned SessionRouter\n\n- Route discovery through Broker index and exact endpoint resolution.\n- Move credentials, connections, replay, reconnect, and attachment generation into Router.\n- Introduce opaque attachment leases.\n- Remove notification-root registration, provider root scanning, and direct endpoint access.\n\n### Stage 5: provider supervisor/adapter boundary\n\n- Retain provider lease, cursor, connection, retry/backoff, rate limits, delivery, mappings, callbacks, and attachments.\n- Apply the Router contract to Telegram, Discord, and Slack.\n- Remove provider edges to lifecycle execution, tmux, process signaling, Broker cleanup, and endpoint credentials.\n\n### Stage 6: singular recovery and deletion\n\n- Broker owns lifecycle recovery.\n- Router reconstructs attachments.\n- Provider supervisor reconstructs only transport and presentation.\n- Delete obsolete lifecycle orchestrator/control-runtime modules.\n- Leave no feature flag, dual writer, silent fallback, or compatibility shim.\n\n## Required invariants\n\n1. Only Broker lifecycle may spawn, signal, close, resume, delete, or persist lifecycle terminality.\n2. Provider retry cannot create two sessions or effects.\n3. Stale endpoint/router/attachment authority cannot submit prompt or answer.\n4. Provider transport failure or restart cannot mutate session lifecycle.\n5. Provider presentation state is recoverable but non-authoritative.\n6. Endpoint credentials remain inside runtime, Broker resolution, and Router.\n7. Provider cleanup failure cannot rewrite a lifecycle result.\n8. Recovery cannot signal or delete a successor process, endpoint, or transcript.\n9. Prompt acceptance remains distinct from completion.\n10. Timeout never invents success or failure.\n\n## Verification\n\n### Static architecture gates\n\nFail provider modules that import lifecycle execution internals, tmux/session process lifecycle, process signals for session lifecycle, direct endpoint discovery/credentials, or logical SessionId generation.\n\n### Crash matrix\n\nInject provider-triggered create failures after reservation, lifecycle admission, Broker ledger begin, effect persistence, child spawn, endpoint publication, readiness/index registration, and provider binding. Retry must produce one SessionId, one effect identity, at most one live owned process, and one Broker result.\n\n### Endpoint rotation\n\nRender work under generation N, rotate to N+1, reject the N lease, reconnect through Broker exact resolution, and replay without duplicate prompt or answer effects.\n\n### Provider restart\n\nRestart the supervisor during active prompt, unanswered ask, create, close, and endpoint rotation. Broker/process/runtime evidence changes only for operations already submitted; provider delivery alone is reconciled.\n\n### Close and cleanup\n\nConfirm close independently of topic/thread deletion. Exercise PID reuse, endpoint replacement, stale index generation, and exact cleanup races. Preserve `terminal_uncertain` when proof is unavailable.\n\n### Credential boundary\n\nRouter/adapter payloads and provider state must not contain endpoint URL/token, Broker token, or lifecycle effect credentials.\n\nStart focused verification with `packages/coding-agent/test/sdk-broker-lifecycle-e2e.test.ts`, Telegram ownership/lifecycle/restart/reconciliation suites, SDK host/runtime lifecycle tests, Coordinator lifecycle/idempotency tests, and ACP create/load/resume/cancel tests when shared paths change.\n\n## Implementation order\n\nPrefer atomic commits:\n\n1. lifecycle-service contract and tests;\n2. Telegram lifecycle cutover and old server/ledger deletion;\n3. provider reservation and Broker-owned SessionId;\n4. Router contract and direct endpoint-access removal;\n5. Telegram supervisor/adapter split;\n6. Discord and Slack Router adoption;\n7. obsolete-path deletion, architecture gates, and docs.\n\nDo not mix provider rendering, topic UX, or unrelated notification features into this migration.\n\n## Primary files\n\nExpected existing files:\n\n- `packages/coding-agent/src/sdk/broker/broker.ts`\n- `packages/coding-agent/src/sdk/broker/lifecycle.ts`\n- `packages/coding-agent/src/sdk/host/session-runtime.ts`\n- `packages/coding-agent/src/sdk/bus/telegram-daemon.ts`\n- `packages/coding-agent/src/sdk/bus/lifecycle-orchestrator.ts`\n- shared chat-daemon/session-control modules under `packages/coding-agent/src/sdk/bus/`\n- Coordinator and ACP adapters where they consume the same core contracts\n\nExpected new core areas:\n\n- `packages/coding-agent/src/sdk/lifecycle/`\n- `packages/coding-agent/src/sdk/router/`\n\nMove or rename existing services where possible instead of introducing parallel abstractions.\n\n## Risks\n\n- **High:** temporary dual authority during cutover.\n- **High:** provider, Router, and Broker idempotency namespace drift.\n- **High:** endpoint credential duplication or leakage.\n- **High:** successor destruction if existing fences are simplified.\n- **Medium:** crash-safe reservation-to-session binding.\n- **Medium:** event replay duplication versus effect duplication.\n- **Medium:** Telegram-specific requirements leaking into SDK core.\n\n## Definition of done\n\n- TUI, CLI, ACP, Coordinator, Telegram, Discord, and Slack lifecycle mutations enter one Broker path.\n- One durable ledger determines lifecycle terminality.\n- Provider daemons cannot allocate SessionIds, signal session processes, or read endpoint credentials.\n- Providers retain only transport and presentation ownership.\n- Router is the sole external attachment authority and credential-bearing SDK client manager.\n- Provider restart cannot create, resume, close, or mutate a session by itself.\n- Stale attachments fail closed.\n- Crash, retry, rotation, and cleanup tests pass.\n- Obsolete lifecycle server, chat lifecycle ledger, root scanning, and compatibility paths are removed.\n- SDK, bot integration, notification, ACP, and Coordinator docs describe the final ownership model consistently.\n",
|
|
93
|
-
"sdk-rpc-parity-audit.md": "# SDK v3 RPC parity audit\n\n**Status:** internal, closed-inventory audit. This is a comparison of the retired\nRPC contract at `6e147d58~1:docs/rpc.md` with SDK v3; it is not an event-plane\nparity claim. The CLI rejects the retired `--mode rpc`, `rpc-ui`, and `bridge`\nmodes and directs external control to the SDK (`packages/coding-agent/src/cli/args.ts:117-127`).\n\nThe historical issue files (01–08, 11–21) are archived under `issues/archive/` as provenance only. Their current disposition is recorded in `issues/README.md`: implementation findings are resolved, retired RPC documentation findings are obsolete, and persistent-session/registry items (09/10) remain deferred architectural follow-ups. Do not treat this closed audit as an active implementation backlog.\n\n## Method and classifications\n\nThe inventory below is **closed**. Command, frame, and sub-protocol rows were\nrecovered from `git show 6e147d58~1:docs/rpc.md`; the supplemental\n`rpc-sessions` registry and `--listen` Unix-socket rows were recovered from\nparent-commit source because they do not appear in that document:\n`6e147d58~1:packages/coding-agent/src/cli/args.ts:157-158`,\n`6e147d58~1:packages/coding-agent/src/modes/rpc/rpc-mode.ts:892-907,984-992`,\nand\n`6e147d58~1:packages/coding-agent/src/modes/shared/agent-wire/session-registry.ts:1-53`.\n`SDK equivalent` means a current operation or documented SDK protocol covers the\ncontrol/query intent, not that its transport or event semantics are identical.\n`transport-gap — retired` means the direct `gjc sdk serve` relay and standalone transport clients were removed when `SessionRouter` became the sole external attachment authority.\n`phase-2-gap` means no equivalent has been implemented by this audit.\n\nOperation names and their stated roles are from\n`packages/coding-agent/src/sdk/protocol/operation-registry.ts:66-166`; dispatch\ncoverage is from `packages/coding-agent/src/sdk/host/control/dispatch.ts:138-253`.\nSDK protocol and lifecycle references use stable heading references in\n`docs/sdk.md`. Command, frame, and sub-protocol rows cite\n`6e147d58~1:docs/rpc.md`; the two supplemental rows cite the parent-commit\nsources above.\n\n## Closed command inventory\n\n| Retired family | Retired command | SDK v3 equivalent or classification | Evidence |\n| --- | --- | --- | --- |\n| Prompting | `prompt` | `turn.prompt` | retired doc; registry:67; dispatch:139-140 |\n| Prompting | `steer` | Partial SDK equivalent: `turn.steer` is text-only and loses retired `images` | `6e147d58~1:docs/rpc.md:77`; registry:68; dispatch:141-142 |\n| Prompting | `follow_up` | Partial SDK equivalent: `turn.follow_up` is text-only and loses retired `images` | `6e147d58~1:docs/rpc.md:78`; registry:69; dispatch:143-144 |\n| Prompting | `abort` | `turn.abort` | retired doc; registry:70; dispatch:145-146 |\n| Prompting | `abort_and_prompt` | `turn.abort_and_prompt` | retired doc; registry:71; dispatch:147-148 |\n| Prompting | `new_session` | Partial SDK equivalent: `session.new` takes no input and loses retired `parentSession` | `6e147d58~1:docs/rpc.md:81`; registry:93; dispatch:196-197 |\n| State | `get_state` | Partial SDK equivalent: query bundle `context.get` (includes `systemPrompt`), `tools.list` (Q20), `models.list/current`, `todo.list`, `queue.messages.list`, `session.metadata`, and `session.stats`; no one-shot legacy-shaped snapshot, no retired `dumpTools` include-toggle/exact dump schema, and some legacy snapshot fields remain absent | `6e147d58~1:docs/rpc.md:85,169-222`; registry:132-152; sdk/bus/index.ts:1804-1808,1852-1855; host/query/handlers.ts:91,116; docs/sdk.md “Protocol” and “Model catalog query (Q10)” |\n| State | `set_todos` | `todo.replace` | retired doc; registry:78; dispatch:166-167 |\n| State | `set_host_tools` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_tools.register` | `6e147d58~1:docs/rpc.md:87,255-291`; registry:105,164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| State | `set_host_uri_schemes` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_uri.register` | `6e147d58~1:docs/rpc.md:88,293-323`; registry:106,165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| State | `workflow_gate_response` | `workflow.gate_answer` (durable Q12 gate ID) | retired doc; registry:73; dispatch:151-157; docs/sdk.md “Durable workflow controls and Q12” |\n| Model | `set_model` | `model.set` | retired doc; registry:79; dispatch:168-169 |\n| Model | `set_default_model_selection` | `model.set` with `thinkingLevel`; equivalent active-model/default-selection intent, not the retired durable-selector response envelope | retired doc; registry:79; dispatch:168-169; docs/sdk.md “Model catalog query (Q10)” |\n| Model | `cycle_model` | `model.cycle` | retired doc; registry:80; dispatch:170-171 |\n| Model | `get_available_models` | `models.list/current` / Q10 | retired doc; registry:141; docs/sdk.md “Model catalog query (Q10)” |\n| Thinking | `set_thinking_level` | `thinking.set` | retired doc; registry:81; dispatch:172-173 |\n| Thinking | `cycle_thinking_level` | `thinking.cycle` | retired doc; registry:82; dispatch:174-175 |\n| Queue modes | `set_steering_mode` | `queue.steering_mode.set` | retired doc; registry:84; dispatch:178-179 |\n| Queue modes | `set_follow_up_mode` | `queue.follow_up_mode.set` | retired doc; registry:85; dispatch:180-181 |\n| Queue modes | `set_interrupt_mode` | `queue.interrupt_mode.set` | retired doc; registry:86; dispatch:182-183 |\n| Compaction | `compact` | Partial SDK equivalent: `compaction.run` takes no input and loses retired `customInstructions` | `6e147d58~1:docs/rpc.md:111`; registry:87; dispatch:184-185 |\n| Compaction | `set_auto_compaction` | `compaction.auto.set` | retired doc; registry:88; dispatch:186-187 |\n| Retry | `set_auto_retry` | `retry.auto.set` | retired doc; registry:89; dispatch:188-189 |\n| Retry | `abort_retry` | `retry.abort` | retired doc; registry:90; dispatch:190-191 |\n| Bash | `bash` | `bash.execute` | retired doc; registry:91; dispatch:192-193 |\n| Bash | `abort_bash` | `bash.abort` | retired doc; registry:92; dispatch:194-195 |\n| Session | `get_session_stats` | `session.stats` | retired doc; registry:146; docs/sdk.md “Protocol” |\n| Session | `export_html` | Partial SDK equivalent: `session.export_html` takes no input and loses retired `outputPath` | `6e147d58~1:docs/rpc.md:127`; registry:101; dispatch:212-213 |\n| Session | `switch_session` | Partial SDK equivalent: retired `switch_session` was path-addressed (`sessionPath`), while `session.switch` is ID-addressed | `6e147d58~1:docs/rpc.md:128`; registry:97; dispatch:204-205 |\n| Session | `branch` | `session.branch` | retired doc; registry:98; dispatch:206-207 |\n| Session | `get_branch_messages` | `session.branch_candidates` plus `transcript.list`/`transcript.body`; no identical combined payload | retired doc; registry:132-133,147; docs/sdk.md “Protocol” |\n| Session | `get_last_assistant_text` | `session.last_assistant` | retired doc; registry:148; docs/sdk.md “Protocol” |\n| Session | `set_session_name` | `session.rename` | retired doc; registry:99; dispatch:208-209 |\n| Messages | `get_messages` | `transcript.list` and `transcript.body`; no identical monolithic payload | retired doc; registry:132-133; docs/sdk.md “Protocol” |\n\n## Closed framing, sub-protocol, registry, and transport inventory\n\n| Retired family | Retired frame, protocol, or transport | SDK v3 equivalent or classification | Evidence |\n| --- | --- | --- | --- |\n| Outbound frame | `ready` | transport-gap — closed by Phase 1; WebSocket connection/authentication replaces JSONL readiness | retired doc; docs/sdk.md §Endpoint discovery |\n| Outbound frame | `response` | transport-gap — closed by Phase 1; SDK control request/response replaces JSONL `RpcResponse` | retired doc; registry:66-119; dispatch:138-253 |\n| Outbound frame | canonical `event` | phase-2-gap; no renderer-grade canonical `AgentSessionEvent` stream | retired doc; docs/sdk.md §Protocol |\n| Outbound frame | `workflow_gate` | Partial SDK equivalent: `action_needed` with `workflowGateId`, plus Q12; not the retired frame/schema | retired doc; docs/sdk.md §Server → client, §Durable workflow controls and Q12 |\n| Outbound frame | `extension_ui_request` | phase-2-gap for extension UI methods; `action_needed` covers only generic asks | retired doc; docs/sdk.md §Server → client |\n| Outbound frame | `host_tool_call`, `host_tool_cancel` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_tool.invoke/cancel/update/result` with `host_tools.register` | `6e147d58~1:docs/rpc.md:45-46,357`; registry:105,164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Outbound frame | `host_uri_request`, `host_uri_cancel` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_uri.read/write/cancel/result` with `host_uri.register` | `6e147d58~1:docs/rpc.md:46,357`; registry:106,165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Outbound frame | `extension_error` | phase-2-gap; no SDK extension-error frame contract | retired doc; docs/sdk.md §Protocol |\n| Inbound frame | `RpcCommand` | SDK control and query operations | retired doc; registry:66-157; dispatch:138-253 |\n| Inbound frame | `workflow_gate_response` | `workflow.gate_answer` | retired doc; registry:73; docs/sdk.md “Durable workflow controls and Q12” |\n| Inbound frame | `extension_ui_response` | phase-2-gap except generic `reply` for an `action_needed` ask | retired doc; docs/sdk.md §Client → server |\n| Inbound frame | `host_tool_update`, `host_tool_result` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_tool.invoke/cancel/update/result` | `6e147d58~1:docs/rpc.md:54`; registry:164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Inbound frame | `host_uri_result` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_uri.read/write/cancel/result` | `6e147d58~1:docs/rpc.md:55`; registry:165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Workflow gate sub-protocol | `workflow_gate` / `workflow_gate_response` with schema and durable broker semantics | Partial SDK equivalent: `action_needed`, `reply`, Q12 `workflow.gates.list`, and `workflow.gate_answer`; IDs and authority rules differ | retired doc; registry:73,143; docs/sdk.md “Answer semantics” and “Durable workflow controls and Q12” |\n| Extension UI sub-protocol | select/confirm/input/editor/cancel/notify/status/widget/title/editor-text | phase-2-gap; generic action presentation is not extension UI parity | retired doc; docs/sdk.md §Server → client |\n| Host tool sub-protocol | registration, call/cancel, update/result | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_tools.register` plus reverse callback operations | `6e147d58~1:docs/rpc.md:45,54,255-291,357`; registry:105,164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Host URI sub-protocol | scheme registration, read/write/cancel/result | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_uri.register` plus reverse callback operations | `6e147d58~1:docs/rpc.md:46,55,293-323,357`; registry:106,165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Unattended sub-protocol | `negotiate_unattended` declaration/budget/scopes/allowlist | phase-2-gap | retired doc; docs/sdk.md §Coordinator MCP question pull loop |\n| `rpc-sessions` registry | Cross-process session registry and reattach semantics | phase-2-gap. Per-session discovery files are only partial endpoint location, not a registry/reattach protocol | parent source: `6e147d58~1:packages/coding-agent/src/modes/rpc/rpc-mode.ts:892-907,984-992`; `6e147d58~1:packages/coding-agent/src/modes/shared/agent-wire/session-registry.ts:1-53`; docs/sdk.md §Endpoint discovery, §Architecture |\n| Transport | stdio JSONL | retired. Direct stdio relay was removed; managed clients use Router-owned capabilities. | parent source: `6e147d58~1:packages/coding-agent/src/modes/rpc/rpc-mode.ts:892-907,984-992`; removal evidence: SDK-owned session lifecycle handoff |\n| Transport | `--listen` Unix socket | retired. Direct Unix-socket relay was removed; managed clients use Router-owned capabilities. | parent source: `6e147d58~1:packages/coding-agent/src/cli/args.ts:157-158`; removal evidence: SDK-owned session lifecycle handoff |\n\n## Five-gap reduction verdict\n\nSDK v3 has broad control/query coverage: the operation registry includes turn,\nmodel, thinking, queue, compaction, retry, bash, session, host callback, and\nworkflow operations (`operation-registry.ts:66-166`), and control dispatch\nimplements the control path (`dispatch.ts:138-253`). That does **not** erase the\nuser-perceived reduction. It is **REAL** across five dimensions:\n\n1. **stdio JSONL and Unix-socket transports.** The interim `gjc sdk serve` relay was retired; external attachment now requires Router-owned opaque capabilities and never forwards endpoint credentials.\n2. **Typed Python client.** The interim public Python transport client was retired with the direct relay; cross-process integrations require Router-owned capabilities.\n3. **`negotiate_unattended`.** No fail-closed unattended negotiation with the\n retired declaration, budget, scope, and allowlist exists: this remains Phase 2.\n4. **Cross-process session registry/reattach.** Discovery files locate a live\n endpoint but do not provide the retired registry or reattach lifecycle: this\n remains Phase 2.\n5. **Renderer-grade full event stream.** SDK v3's minimal frames and optional\n threaded-client frames are not the retired canonical session event stream.\n **No event-plane parity is claimed.**\n\n## Ranked Phase-2 follow-up register — NOT implemented\n\n1. **Unattended negotiation equivalent — NOT implemented.** Add a fail-closed\n equivalent to `negotiate_unattended` only with explicit actor, budget, scopes,\n allowlist, and audit enforcement. Partial equivalent only: Q12\n `workflow.gates.list` plus the Coordinator MCP pull loop can enumerate and\n answer durable workflow gates; they are not unattended negotiation\n (`docs/sdk.md §Coordinator MCP question pull loop`).\n2. **Reattachment semantics — implemented in SDK core.** `SessionRouter` exclusively resolves private endpoint publication, retains credentials, reconnects, replays, rotates transports, and issues exact opaque attachment capabilities. External adapters do not consume discovery files or raw endpoint credentials.\n3. **Full event stream — NOT implemented.** Define a renderer-grade session\n event contract only if consumers require it. Partial equivalent only:\n `action_needed`, `action_resolved`, `reply_rejected`, and optional threaded\n frames such as `turn_stream` exist, but there is **no `onSessionEvent`-style\n SDK equivalent** (`docs/sdk.md §Server → client`).\n\n## Completeness checklist\n\n- [x] Prompting — every retired command represented.\n- [x] State — every retired command represented.\n- [x] Model — every retired command represented.\n- [x] Thinking — every retired command represented.\n- [x] Queue modes — every retired command represented.\n- [x] Compaction — every retired command represented.\n- [x] Retry — every retired command represented.\n- [x] Bash — every retired command represented.\n- [x] Session — every retired command represented.\n- [x] Messages — every retired command represented.\n- [x] Outbound and inbound frame categories — every retired category represented.\n- [x] Workflow gate sub-protocol represented.\n- [x] Extension UI sub-protocol represented.\n- [x] Host tool sub-protocol represented.\n- [x] Host URI sub-protocol represented.\n- [x] `negotiate_unattended` sub-protocol represented.\n- [x] `rpc-sessions` registry represented from parent-commit source (supplemental to the recovered document inventory).\n- [x] stdio JSONL represented from the recovered document inventory; `--listen` Unix-socket transport represented from parent-commit source.\n",
|
|
94
|
-
"sdk-session-cli.md": "# SDK session CLI\n\n`gjc sdk session` is the broker-bound command family for operating live GJC SDK\nsessions from the terminal. It replaces the removed `gjc daemon session` route.\n\nThe command family has six semantic verbs — `list`, `inspect`,\n`send`, `status`, `tail`, and `retire` — plus the explicit `raw` hatch that dispatches one\nSDK operation as `control`, `query`, or `global`.\n\nThe session CLI is advisory tooling over the SDK: every semantic verb resolves\nsessions through the SDK broker, and output is rendered through a versioned,\ncredential-free DTO. Endpoint credentials are never printed.\n\n## Broker authority\n\n`list`, `inspect`, `send`, `status`, and `tail` resolve sessions through the\nSDK broker and Router. The Router validates indexed endpoint authority and keeps\nthe connection credential in SDK core; the CLI receives only credential-free\nresults. The broker is started on demand (`ensureBroker`) when discovery is\nabsent, and an unavailable broker fails closed with a typed operational error\n(exit 1).\n\n`--agent-dir` selects the broker state directory; `--repo` selects the\nworkspace directory used for saved-session resolution (default: the current\ndirectory).\n\n## Semantic verbs\n\n### list\n\n`gjc sdk session list` queries the broker `session.list` global and projects\nevery indexed session into the versioned row DTO (`SESSION_ROWS_VERSION`). Each\nrow is credential-free and carries:\n\nThe list is fully paginated before scope filtering. By default its effective\nscope is `repo`. Select a scope with:\n\n```sh\ngjc sdk session list --scope repo|cwd|worktree|all [--repo <path>]\n```\n\n`--repo` is the selected workspace path and defaults to the process cwd. The\nresult reports the effective `scope` and a bounded credential-free `selection`\ndescriptor containing the canonical selected path and, for Git selections,\nthe canonical worktree root and Git common directory.\n\n- `repo` matches the canonical Git common directory, so the main checkout and\n linked worktrees are included while another repository is excluded.\n- `worktree` matches only the selected path's canonical containing worktree.\n- `cwd` matches only the exact canonical selected workspace; nested directories\n do not match.\n- `all` preserves the complete unfiltered Broker listing.\n\nFor a path outside Git, `repo` and `worktree` fail with the typed\n`not_a_repository` operational error and an actionable suggestion to use\n`cwd` or `all`; they never broaden the result. `cwd` remains available for an\nexact canonical path match. Unreadable or removed row workspaces are excluded\nfrom Git scopes deterministically and reported in `warnings`.\n\nThe raw global `session.list` route remains unfiltered, and `inspect`, `send`,\n`status`, `tail`, `retire`, and raw control/query behavior is unchanged.\n\n- `sessionId` and the `locator` (`cwd`, `worktreeRoot`, `stateRoot`), where `cwd`\n is the canonical workspace directory and `worktreeRoot` is the canonical Git\n worktree root or `null` outside a worktree;\n- `endpointGeneration`, `pid`, `live`, `deleted` (tombstone), `indexSeq`;\n- `hostIncarnation` and `identityProvenance` (`composite` | `legacy`);\n- `activity` (`{state: active|idle, at}`) and `lastHeartbeatAt`;\n- `terminalUncertain`, `lifecycleRequestId`, `endpointMtimeMs`;\n- `ambiguous` when the same `sessionId` has more than one unresolved\n authority-fencing `stateRoot` (cross-repo duplicate). A proven non-endpoint\n bookkeeping registration (the direct-session GC fence row, endpoint\n generation 0) stays indexed without fencing endpoint attachment; every other\n unresolved root, including an unproven generation-0 `lifecycle_terminal`\n claim, still fences.\n\n### inspect\n\n`gjc sdk session inspect <sessionId>` renders one indexed row from the broker.\nIt never reads endpoint discovery records directly: a missing or unavailable\nbroker fails closed rather than exposing endpoint authority outside SDK core.\n\n### send\n\n`gjc sdk session send <sessionId> --text <prompt>` submits an ordered\n`turn.prompt` carrying a caller-chosen operation reference (a ULID by default,\nor `--op-ref`). The result envelope reports `accepted` with the receipt and the\noperation reference used for later reconciliation.\n\n- `--wait` polls `turn.result` with `kind: \"prompt\"` until the prompt reaches a\n terminal state or the wait window (`--timeout-ms`, default 30s) elapses.\n `send --wait` never cancels a running turn; a window that elapses before a\n terminal state is reported as `wait_timeout` with the last observed status.\n\n- `--text` and the JSON input sources (`--json-input`,\n `--json-input-file` — which must be a `0600` regular file —\n `--json-input-stdin`) are mutually exclusive for the prompt body.\n\n### status\n\n`gjc sdk session status <sessionId> <opRef>` performs a lossless `turn.result`\nlookup with `kind: \"prompt\"` for a previously submitted operation reference and\nreturns the full reconciliation record plus a `summary.completed` flag.\nSee [lossless prompt results](#lossless-prompt-results).\n\n\n### tail\n\n`gjc sdk session tail <sessionId>` replays the retained transcript from the\ndurable checkpoint and then follows the live event-ring frames, emitting the\ndefault tail kinds (session lifecycle and turn lifecycle events) plus retained\ntranscript entries.\n\n- `--strict` fails closed with `retention_gap` (exit 1) when retained history\n or the event ring dropped entries before the checkpoint.\n- `--until-idle` exits once the current turn reaches a terminal state; a session\n close exits any tail as `terminal: true`, while a bare live tail otherwise\n remains attached until `--timeout-ms`. Lifecycle\n events that carry a `(generation, seq)` position are reconciled and emitted in\n that canonical order rather than arrival order, so a retained terminal event\n from an earlier turn does not complete a newer turn that is still running, and\n a terminal event observed live is not undone by replayed history that arrives\n after it. Positioned event items are emitted canonically ahead of an\n arrival-ordered segment of events that carry no position; such an event stays\n visible but cannot supersede a positioned turn state. Different lifecycle\n kinds claiming the same `(generation, seq)` fail closed with `protocol_error`\n (exit 1) because no order between them can be proven, and that conflict\n outranks an otherwise successful idle or close completion. A canonical\n position is a pair of non-negative safe integers. A replayed event-ring row that\n states either coordinate property must state both validly; a row claiming only\n one coordinate, or a null, negative, fractional, non-finite, or\n unsafe-integer coordinate, fails closed with `protocol_error` rather than\n being treated as unpositioned. Only a row that states neither coordinate is\n unpositioned. Retained transcript entries are deduplicated independently of\n event-ring rows and are not event-ring authority, so a transcript row that\n projects the same kind and position as a real event never suppresses it. For a\n specific prompt operation, `status <sessionId> <opRef>` remains the lossless\n authority.\n- `--all-events` widens the emitted set to every event-ring kind.\n- `--cursor` resumes from a saved signed checkpoint claim. `session.checkpoint`\n verifies the unexpired claim and exchanges it for a fresh connection-owned\n cursor pinned to the exact prior revision; direct cross-connection cursor\n consumption remains rejected, so reconnect never echoes or rewinds a cursor.\n- `--timeout-ms` bounds live follow; a session whose lifecycle already ended\n (terminal or `terminalUncertain`) replays retained history and exits instead\n of hanging.\n\nA deleted session has no tail (`session_deleted`). A stopped session replays\nits retained transcript without an endpoint (offline source), bounded to the\nmost recent retained entries.\n\n### retire\n\n`gjc sdk session retire <sessionId>` is the official semantic wrapper for the\n`session.reconcile_uncertain` broker global. It retires an indexed\n`terminalUncertain` create effect only when exactly one matching uncertain\ncreate identity exists, the recorded host is proven exited, the endpoint is\nabsent, and any lifecycle marker/readiness leftovers are regular files bound to\nthat same PID and incarnation. The broker removes only those verified\nleftovers, appends terminal `session_closed` evidence, and converts the\nmatching lifecycle receipt to a terminal error. Ambiguous identity, live or\nunverifiable hosts, endpoint presence, malformed leftovers, and mismatches\nrefuse without signalling a process. Supply `--idempotency-key`; the JSON\ninput is read from a `0600` file or stdin when it contains proof material.\n\n## Raw hatch\n\n`gjc sdk session raw <control|query|global>` dispatches exactly one SDK\noperation and returns the broker/host response:\n\n- `raw control <sessionId> --op <operation>` — one control operation with\n `--json-input*`; `--confirm` confirms destructive control operations.\n- `raw query <sessionId> --query <operation>` — one query; `--cursor` passes a\n continuation cursor.\n- `raw global --op <operation>` — one broker global. Lifecycle globals\n (`session.create`, `session.fork`, `session.resume`, `session.close`,\n `session.delete`, `session.reconcile_uncertain`) require `--idempotency-key`.\n\nA separately connected local controller can stop the active turn and its exact\nowned work with an explicitly confirmed operator abort:\n\n```sh\ngjc sdk session raw control <sessionId> \\\n --op turn.abort \\\n --json-input '{\"mode\":\"terminal\",\"scope\":\"owned\",\"operator\":true}' \\\n --idempotency-key '<unique-key>' \\\n --confirm\n```\n\nThe JSON input accepts only operation fields (`mode`, `scope`, `operator`).\n`--confirm` and `--idempotency-key` are CLI authority inputs, not JSON fields.\nThe exact `operator:true` shape is routed through the local Broker, which\nrevalidates the current endpoint identity and injects a process-bound private\ncapability before dispatch. MCP, ACP, notifications, and ordinary SDK endpoint\nrequests cannot mint operator authority: copying `operator:true` and\n`confirm:true` into a public control frame is rejected. Omitting confirmation,\nomitting the key, or supplying `operator:false` fails closed without invoking\nthe terminal-abort surface. Non-operator terminal abort retains its existing\nconnection-ownership semantics.\n\n`session.get_endpoint` is refused unconditionally: endpoint credentials remain\nan SDK-core implementation detail. The raw hatch validates operation names and\nadapter dispositions up front and never renders endpoint-disclosure results.\n\n## Lossless prompt results\n\n`turn.result` with `kind: \"prompt\"` reports `accepted`, `in_flight`,\n`terminal_ok`, or `failed`; only retained-record capacity eviction yields\n`unknown`. `turn.prompt_status` remains a legacy prompt-only alias. A prompt\nthat is active at process restart is finalized from its durable pending outcome\n(or `prompt_failed` when it has none), so it never reports as `unknown` while a\nrecord exists.\n\n`unknown` means uncertainty, never proof of non-execution: do not reuse an\noperation reference as a retry mechanism (`client_ref_conflict` while the\nrecord is retained; after eviction a reused ref may be admitted again with the\nprior outcome unknown). Use one fresh operation reference per logical prompt\nand reconcile with `status`.\n\n## Checkpoint gaps\n\n`tail` reports a `retention_gap` when retained history or the event ring\ndropped entries before the durable checkpoint: the gap carries the missing\nsequence range (`missing.from`/`missing.to`) and a `resync` checkpoint.\n`--strict` turns any gap into `retention_gap` with exit 1; without `--strict`,\ntail continues from the resync position and reports the gap in the envelope.\n\n\n## Migration from the removed daemon session route\n\n`gjc daemon session` is removed and no alias is provided. Migrate:\n\n| Removed route | Replacement |\n| --- | --- |\n| `gjc daemon session list` | `gjc sdk session list` |\n| `gjc daemon session inspect <sessionId>` | `gjc sdk session inspect <sessionId>` |\n| `gjc daemon session send <sessionId> --text <prompt>` | `gjc sdk session send <sessionId> --text <prompt>` |\n| `gjc daemon session tail <sessionId>` | `gjc sdk session tail <sessionId>` |\n| raw control/query dispatch | `gjc sdk session raw control|query|global` |\n\nThe broker-bound surface replaces the daemon-owned routing: sessions are\nresolved through the SDK broker with validated endpoint identity instead of\ndirect discovery-file reads, and output is versioned and credential-free.\n\n## Exit codes and error envelope\n\nVerbs exit `0` on success and write JSON to stdout. Failures write a JSON error\nenvelope to stdout with a non-zero exit: usage errors exit `2`, operational\nfailures (broker unavailable, session unavailable, retention gap, wait\ntimeout) exit `1`. Error details are recursively redacted of secret-shaped\nfields before rendering.\n\n## Scoped search (`gjc sdk search`)\n\n`gjc sdk search [--scope repo|pwd|global] [--limit N] [--cursor <token>] [--json]`\nlists broker-visible sessions inside one exact scope. The default scope is\n`repo`: the identical canonical Git worktree of the invoking directory, never a\npath prefix or subtree. `pwd` matches the exact canonical working directory and\n`global` covers every broker-visible row.\n\nEvery result — table and JSON, populated and empty — carries a scope/status\nenvelope: the requested scope, the canonical resolved scope, a status\n(`populated`, `empty`, `not-in-git-worktree`, `unavailable`), and the\nobservation time. Running `--scope repo` outside a Git worktree is a successful\nempty result (`not-in-git-worktree`, exit 0) and never falls back to `pwd` or\n`global`. Broker unavailability keeps the locally resolved scope, exits\nnon-zero, and stays credential-free. Continuation cursors are frozen: a\ncontinuation that supplies a different scope or anchor fails with\n`scope_cursor_mismatch` instead of re-scoping.\n\nRows are probed only after scope filtering, through broker/router-owned\ncredential-free attachments, yielding `reachable`, `unreachable`, or `stale`.\n\n## Local-only spawn (`gjc sdk spawn`)\n\n`gjc sdk spawn --cwd <dir> --prompt <task> [--model <selector>] [--profile <name>] [--json]`\ncreates one task-seeded background child session through the broker. It is\nlegal only inside a live interactive `gjc --master` session: the command needs\nthe master's transient capability (threaded through the master session\nenvironment) and the broker verifies it against the live effective master host\nbefore any effect. Spawn is prohibited on MCP, ACP, daemon CLI/raw session CLI,\nTelegram, Discord, and Slack surfaces.\n\nEach invocation uses a fresh idempotency identity. One identity produces at\nmost one child substrate and one seed prompt; repeated requests replay the\nstored outcome. A semantically new task requires a new invocation, never a\nretry of an old identity. `spawn_in_progress` and `terminal_uncertain` are\nhonest durable states: inspect with `gjc sdk search` or session status instead\nof retrying blindly.\n\nThe task text and master capability never persist anywhere in broker state —\nno plaintext, hash, or derived verifier appears in the lifecycle ledger, spawn\nauthority journal, receipts, logs, or output. Spawn output renders only safe\nfields: result code, claim id, child session id, substrate kind, and opaque\nseed facts.\n\nClose spawned children through the standard `session.close` path; the broker\ncloses only an exactly re-proven substrate and retains uncertainty on identity\nmismatch. Children orphaned by confirmed master loss are reaped after\n`sdk.masterOrphanGraceMs` (default 120000 ms, bounded 60000..3600000), with the\norphan clock preserved across broker restarts.\n",
|
|
95
|
-
"sdk.md": "# Gajae-Code SDK\n\nFor embedding GJC in-process, see [the embedding SDK guide](./sdk-embedding.md).\nFor a beginner-friendly application development guide (recipes, customization, and surface selection), see [Building applications on the SDK](./sdk-app-guide.md).\n\n<p align=\"center\">\n <img src=\"../assets/telegram-mobile-hero.png\" alt=\"Gajae Code mobile answers for coding agents hero illustration\" width=\"100%\" />\n</p>\n\nThe SDK exposes a generic action/reply protocol without requiring integrations to scrape the terminal. SDK core owns all managed attachment discovery and credential-bearing clients through `SessionRouter`; Telegram, Discord, Slack, ACP, MCP, and CLI adapters receive only capability-scoped operations and never endpoint credentials.\n\n> Status: the Rust core (`crates/gjc-sdk`) provides the session-local wire protocol and endpoint record. TypeScript SDK core provides Broker lifecycle authority and `SessionRouter` attachment authority. Endpoint records and tokens are internal implementation details, not an external attachment surface.\n\n## External attachment policy\n\nExternal and managed integrations attach through SDK-core surfaces only:\n\n- lifecycle mutations use `SessionLifecycleService` and the Broker lifecycle ledger;\n- live session controls use opaque `SessionAttachment` capabilities issued by `SessionRouter`;\n- endpoint URL/token discovery, raw WebSocket relays, and `gjc sdk serve` are not public attachment mechanisms;\n- lifecycle-equivalent per-session controls are prohibited on Telegram, Discord, Slack, ACP, MCP, and daemon CLI adapters.\n\nFor terminal-side session operation, use the broker-bound [SDK session CLI](./sdk-session-cli.md):\n`gjc sdk session list|inspect|send|status|tail` plus the explicit `raw`\n`control|query|global` hatch. The CLI resolves the exact attachment through SDK\ncore and emits credential-free JSON.\n\n## Migration from removed external transports\n\nThe retired `--mode rpc`, `rpc-ui`, `bridge`, and `gjc sdk serve` transports\nhave no replacement wire client. Process-isolated controllers use Coordinator\nMCP, `gjc sdk session`, or a configured managed adapter. In-process applications\nuse the [embedding SDK](./sdk-embedding.md).\n\n## External-agent SDK skills\n\nThe generated `sdk-skills/` bundle provides host-neutral guidance for scripts\nthat invoke the broker-bound session CLI. It is intentionally separate from\nGJC's four internal workflow skills, the coordinator MCP plugin, and the SDK MCP\nadapter. Its TypeScript and Python templates do not discover endpoints or create\ntransport clients.\n\nThe bundle owns exactly six files:\n\n- `manifest.json`\n- `gjc-sdk-discover/SKILL.md`\n- `gjc-sdk-operate/SKILL.md`\n- `gjc-sdk-author/SKILL.md`\n- `gjc-sdk-author/templates/direct-sdk.ts`\n- `gjc-sdk-author/templates/direct-sdk.py`\n\nRegenerate with `bun run generate-sdk-skills`; CI checks byte-for-byte content\nand rejects unexpected files with `bun run check:sdk-skills`.\n\n### Bundle format version\n\n`manifest.json` is the versioned root of the on-disk bundle contract. It\nidentifies `formatVersion` (currently `1`) and the exact relative file closure\nthat regeneration owns. Consumers must treat a bundle whose manifest is missing,\nmalformed, or declares an unsupported version as unreadable and fail closed. The\nskill prompts are authored as static Markdown sources under\n`scripts/gjc-sdk-skills/prompts/`; the generator copies them verbatim and\n`check:sdk-skills` proves the committed bundle matches the generated artifacts.\n\n### Trust boundary\n\nThe templates invoke only the broker-bound CLI. `SessionRouter` keeps endpoint\nresolution, credentials, SDK clients, replay, reconnect, and rotation inside SDK\ncore. The templates' fixed allowlists and nonce-bound approval are trusted-local\nprocedural safeguards, not capability isolation; lifecycle and attachment\nauthority remain enforced by Broker and Router.\n\nNo renderer-grade cross-process event stream is exposed to external scripts.\nUse managed adapters or Coordinator MCP for event-driven orchestration; see the\n[RPC-to-SDK v3 parity audit](./sdk-rpc-parity-audit.md) for remaining gaps.\n\n## Architecture\n\n```\nBroker lifecycle → Session runtime endpoint → SessionRouter → opaque adapter capability\n```\n\nThe Broker is the sole lifecycle executor and durable terminal authority. `SessionRouter` is the sole credential-bearing external attachment manager. Provider supervisors own only provider transport and presentation state.\n\n- **One endpoint per top-level session.** Each top-level session runs its own loopback WebSocket server. Subagents do not host endpoints. The Broker index is the authoritative live-session catalog, and `SessionRouter` multiplexes managed provider attachments across indexed sessions.\n- **Hosted by default.** SDK hosting is independent of notification configuration. Set `GJC_SDK_DISABLE=1` to opt out of hosting for a top-level session.\n- **Notification delivery is optional.** Configure and enable a managed notification adapter only when remote delivery is needed; the SDK endpoint remains available without one.\n- **Managed integrations use opaque attachments.** Telegram, Discord, Slack, ACP, MCP, and CLI adapters compose SDK-core services; they do not discover endpoint files or receive URL/token credentials.\n- **Zero wire-protocol change.** New transports do not require changes to `crates/gjc-sdk` or the JSON protocol.\n- **tmux-agnostic.** The endpoint behaves identically with or without tmux.\n\n## Internal endpoint publication\n\nA running session publishes an implementation-private credential record for Broker resolution. `SessionRouter` is the sole consumer of the resolved URL/token pair and the sole owner of per-session SDK clients, replay, reconnect, rotation, prepared activation, and opaque attachment capabilities.\n\nThe record path, schema, credential transport, and handshake are not public client contracts. ACP, MCP, Coordinator, CLI, provider daemons, extensions, and integrations must use Router-issued attachments or Broker lifecycle services; they must not scan state roots, parse discovery files, retain endpoint credentials, or open raw per-session WebSockets. Broker and Router validate process identity, endpoint generation, incarnation, and file integrity before attachment, and fail closed on stale or uncertain state.\n\n### Internal broker launch isolation\n\nWhen the SDK starts its default internal broker or session host from the published TypeScript source, GJC uses a fixed Bun launch policy: `--no-env-file`, a product-owned empty `bunfig.toml`, absolute product entrypoint paths, and no inherited `BUN_OPTIONS` or mutable compiled-mode markers. The broker bootstraps from the product SDK directory rather than the caller project; a session host still runs with the lifecycle-authorized workspace as its process cwd.\n\nThis boundary prevents a child from newly loading caller-cwd or user-global Bun preload/dotenv policy. It cannot determine how a value already present in the parent environment was originally loaded, so ordinary provider/GJC environment values remain inherited. Default internal children, including compiled self-spawns, remove inherited `BUN_OPTIONS` so parent eval/test/inspect/debug/runtime options cannot be replayed into a detached child. Compiled binaries otherwise retain their existing self-spawn command contract, corroborated by a dedicated embedded marker and exact anchored Bun virtual-filesystem identity. The explicit `GJC_SDK_SESSION_COMMAND` session-host override remains a trusted legacy operator boundary and is not parsed as a shell-safe general command API. There is no broker-command override.\n\nBroker and per-session discovery tokens remain in their authoritative private discovery files for SDK-core resolution. Launch errors, logs, and diagnostics redact those tokens and never include the child environment or isolation configuration contents.\n\n## Internal protocol\n\nThe following frame shapes are SDK-core implementation details for maintainers.\nExternal integrations must not construct these frames, attach to session\ntransports, or receive endpoint credentials; use a Router-issued attachment, the\nbroker-bound CLI, or Coordinator MCP instead.\n\n### Server → client\n\n`action_needed` — something needs attention:\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"act_9e31\", \"kind\": \"ask\",\n \"sessionId\": \"sess-1\", \"workflowGateId\": \"wg_run_stage_1\",\n \"question\": \"Proceed?\", \"options\": [\"Yes\", \"No\"], \"recommendedIndex\": 1 }\n```\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"act_a42f\", \"kind\": \"ask\",\n \"sessionId\": \"sess-1\", \"question\": \"Choose a target\", \"options\": [\"A\", \"B\"] }\n```\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"idle-sess-1-7\", \"kind\": \"idle\",\n \"sessionId\": \"sess-1\", \"summary\": \"finished refactor; awaiting next step\" }\n```\n\n- `id` is an opaque, transient presentation/action ID. It is the **only** authority accepted by generic `reply.id` through the current Router-issued attachment. It is not a durable workflow ID.\n- `workflowGateId?: string` is optional, additive SDK v3 correlation metadata, present only for the active presentation of a durable workflow gate. When present, it equals that gate's Q12 `gate_id`. Its public correlation key is `(sessionId, workflowGateId)` on the current Router-issued attachment; it never authorizes generic `reply`.\n- `kind: \"ask\"` is answerable in interactive/TUI and SDK workflow-gate sessions. `kind: \"idle\"` is notify-only and ephemeral (not replayed to attachments that start later). Ordinary asks and idle frames omit `workflowGateId`.\n- `recommendedIndex?: number` is optional, zero-based display metadata for `options`. Clients must validate that it is an in-range integer and ignore malformed values. Raw option labels and reply indices remain authoritative; never decorate submitted answers or infer a recommendation from position. The additive field is wire-compatible, but Rust consumers constructing the public `ActionNeeded` struct by literal must provide `recommended_index: None` when no recommendation exists.\n- This corrects the pre-v3 documentation invariant that `action_needed.id == gate_id`: they are deliberately different values. Clients must not preserve that invariant, infer a relationship from question/options/order, or retain private route, claim, receipt, epoch, token, or endpoint-generation maps.\n\n`action_resolved` — a pending action is now terminal and **non-repliable**:\n\n```json\n{ \"type\": \"action_resolved\", \"id\": \"act_9e31\", \"resolvedBy\": \"local\" }\n```\n\n`resolvedBy` is `local` (a local/direct control retired the presentation), `client` (a remote generic reply won), or `timeout`.\n\n`reply_rejected` — sent only to the client whose reply failed:\n\n```json\n{ \"type\": \"reply_rejected\", \"id\": \"act_9e31\", \"reason\": \"already_answered\" }\n```\n\nReasons: `already_answered`, `unknown_action`, `invalid_answer`,\n`resolver_unavailable`, `idempotency_conflict`, `unauthorized`.\n\nThe frames above are the internal transport contract implemented by SDK-core attachments. Managed adapters may receive optional server → client frames they can render or ignore: `identity_header` (one-time per-session repo/branch/machine header; Telegram topic-capable sessions additionally carry `telegramTopicsEnabled`), `context_update` (last message, task, goal, token usage, model, diff), `turn_stream` (live/finalized turn output), `image_attachment` (agent-produced images), `activity` (busy/idle, drives the typing indicator), `inbound_ack` (delivery state of an injected user message), `session_closed` (endpoint teardown; threaded adapters may delete/archive the remote conversation), `config_update` (current verbosity/redact), `hello` (server capability/version), and `pong`.\n\n### Internal inbound frames\n\nSDK core creates inbound frames only after Router attachment checks. External\nintegrations must not construct or persist them. Managed adapters use their\nopaque `SessionAttachment`, and process-isolated scripts use `gjc sdk session`;\nneither path receives transport credentials.\n\n## Model catalog query (Q10)\n\nThe SDK exposes the model catalog through the paged Q10 registry query. `Q10`,\n`models.list/current`, `models.list`, and `models.current` are exact aliases:\neach returns the same paged registry array, not a current-model singleton or a\nfiltered list. Continue using the returned cursor until `page.complete` is\ntrue.\n\nEach row preserves the five legacy fields (`provider`, `id`, `name`,\n`contextWindow`, and `maxTokens`) and additively includes `reasoning`,\n`thinking`, and `current`. `currentThinkingLevel` appears only on the current\nrow when the live session has a thinking level. The exported DTO types are\n`Q10Model`, `Q10ThinkingCapabilities`, `Q10ThinkingEffort`,\n`Q10SettableThinkingLevel`, `Q10CurrentThinkingLevel`, and\n`Q10ThinkingMode`, all from `@gajae-code/coding-agent/sdk`; there is no public\n`/sdk/models` subpath.\n\n```json\n{\n \"provider\": \"runtime-provider\",\n \"id\": \"reasoning-model\",\n \"name\": \"Reasoning Model\",\n \"contextWindow\": 128000,\n \"maxTokens\": 8192,\n \"reasoning\": true,\n \"thinking\": {\n \"validLevels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"],\n \"minLevel\": \"minimal\",\n \"maxLevel\": \"high\",\n \"mode\": \"effort\",\n \"defaultLevel\": \"low\"\n },\n \"current\": true,\n \"currentThinkingLevel\": \"high\"\n}\n```\n\n`thinking.validLevels` is always present and starts with `\"off\"`; it is the\ncanonical menu for `model.set` and never contains `\"inherit\"`. For a\nnon-reasoning model it is exactly `[\"off\"]`. Successful reasoning rows always\ninclude `minLevel`, `maxLevel`, and `mode`; only `defaultLevel` and raw `levels`\nare optional. Raw `levels` deliberately keeps its descriptor order and\nduplicates, while `validLevels` is the canonical, deduplicated menu clients\nshould render. `\"inherit\"` is a current-state readback value only and is rejected\nas a `model.set` input.\n\nMalformed reasoning descriptors are not client-recoverable catalog data. The\nquery returns the SDK's safe `internal` error rather than exposing a partially\nformed row or descriptor details.\n### Model profiles as synthetic models (`gajae-code/<profile>`)\n\nThe Q10 catalog also exposes model profiles as logical synthetic models under\nthe reserved provider namespace `gajae-code`, e.g. `gajae-code/codex-eco`.\nThese rows let clients (such as ACP model pickers) offer presets like ordinary\nmodels without provider-specific metadata:\n\n```json\n{\n \"provider\": \"gajae-code\",\n \"id\": \"codex-eco\",\n \"name\": \"Codex Eco\",\n \"contextWindow\": 222222,\n \"maxTokens\": 8888,\n \"reasoning\": false,\n \"thinking\": { \"validLevels\": [\"off\"] },\n \"current\": false\n}\n```\n\n- `gajae-code/<profile>` is a **logical namespace, not a callable provider**. No\n API transport, credentials, or streaming route is registered for it; send the\n value back through the generic `model.set` control (or the ACP `Model`\n select) to activate the profile.\n- Synthetic rows are **availability-filtered**: only profiles whose required and\n alternative providers have usable stored credentials are listed. The profile\n id suffix is parsed losslessly after the first namespace slash, so profile ids\n containing additional slashes or punctuation round-trip exactly.\n- `contextWindow`/`maxTokens` mirror the profile's resolvable default model when\n available and otherwise fall back to the shared unknown-model constants\n (222222 / 8888); the profile's real default model remains authoritative.\n- Synthetic rows are non-reasoning with `validLevels: [\"off\"]`: a `model.set`\n on a synthetic id with any thinking level other than `off` is rejected with\n `invalid_input`, and only an absent or `off` level is forwarded as a session\n override.\n- **Current-state semantics:** while a profile is active for the session, exactly\n the synthetic row carries `current: true` with `currentThinkingLevel:\n \"inherit\"`, and the underlying concrete row is not marked current. A persisted\n `modelProfile.default` alone (without an in-session active marker) never\n creates a synthetic current row. Selecting a concrete `provider/model` clears\n the active marker and restores concrete current semantics.\n- **Selecting a synthetic profile is session-scoped.** `model.set` with\n `gajae-code/<profile>` activates the full profile in the live session without\n writing `modelProfile.default`, `modelRoles`, or\n `task.agentModelOverrides`. Persisting a profile remains an explicit TUI\n choice (`/model` → default), mirroring `gjc --mpreset <profile> --default`.\n Unknown or ambiguous synthetic ids fail with `invalid_input`; missing profile\n credentials fail with the existing authentication-required error.\n- `gajae-code` is **reserved**: a user-defined `models.yml` provider of the same\n name disables the synthetic facade (rows are omitted and synthetic selection\n is rejected) rather than being silently shadowed. Q27 (`models.profiles.list`)\n remains the full profile catalog with explicit `available` status; Q10 is the\n availability-aware facade for client selection.\n`config.patch` mutations are serialized through the same session admission\nboundary as profile activation and default-model selection, so a patch racing\na synthetic `gajae-code/*` selection (or another patch) is applied in a\ndeterministic order and is never lost or clobbered by an activation rollback.\nThe cost is the same as `model.set`: an external `config.patch` queues behind\nany in-flight prompt admission rather than applying mid-turn.\n\n## Prompt acceptance, termination, and reconciliation (Q26)\n\n`runtime.capabilities.promptTerminalOutcomeVersion` is `1` when this contract is available. Its normalized TypeScript terminal outcome is:\n\n```ts\ntype SdkPromptTerminalOutcome =\n\t| {\n\t\t\tkind: \"stopped\";\n\t\t\treason: \"end_turn\" | \"max_tokens\" | \"max_turn_requests\" | \"refusal\" | \"cancelled\";\n\t\t\tprovenance: \"agent\" | \"client_cancel\";\n\t }\n\t| {\n\t\t\tkind: \"failed\";\n\t\t\tcode: \"prompt_failed\" | \"prompt_deadline_exceeded\";\n\t\t\tmessage: string;\n\t\t\tprovenance: \"agent_failed\" | \"deadline\";\n\t };\n```\n\n`turn.prompt` returns `{ accepted: true, commandId, turnId, clientRef? }` only after\nits asynchronous preflight accepts the prompt. That receipt is a durable,\n**non-terminal pending claim**, not a process-durable terminal result. The SDK\nlater finalizes that claim with exactly one `SdkPromptTerminalOutcome`; cleanup\nmay follow only after the claim is durable.\n\nThe authoritative public reconciliation query is `Q26` / `turn.result`, scoped\nto the same live session runtime. Callers supply `kind: \"prompt\"`; its `outcome`\nfield is exposed only after finalization. A pending claim is never represented\nor exposed as a terminal outcome. `turn.prompt_status` remains a legacy\nprompt-only alias that injects the same `kind`.\n\nEvery prompt and skill status response includes `receiptState`: active records are `absent`; terminal records are `present`, `missing`, or `unknown`; unknown lookup is `unknown`. Ordinary success requires `status: \"terminal_ok\"`, `receiptState: \"present\"`, and readable non-empty text or an artifact path. A failed execution may retain a partial `present` receipt. Legacy version-1 reconciliation records without this additive field remain readable and project `unknown` rather than optimistic success.\n\nCallers that must recover from a lost acknowledgement should assign one fresh\n`clientRef` (a trimmed, non-empty string of at most 128 characters) to each\nlogical prompt, then reconcile through the broker-bound CLI:\n\n```sh\ngjc sdk session raw query <sessionId> --query turn.result \\\n --json-input '{\"kind\":\"prompt\",\"clientRef\":\"request-018f\"}'\n```\n\nThe alternate selector uses the same `kind` with\n`{\"kind\":\"prompt\",\"commandId\":\"command-id\",\"turnId\":\"turn-id\"}` as its JSON input.\n\nThe result status is `accepted`, `in_flight`, `terminal_ok`, `failed`, or\n`unknown`. Known records include `acceptedAt`; in-flight and terminal records add\n`startedAt` and/or `terminalAt`; finalized records include `outcome`; failed records\nalso include a bounded sanitized `error.code` and `error.message`. Cursors, partial\ngenerated-ID pairs, mixed selectors, and extra selector fields are rejected.\n\nCorrelated `agent_end` and `agent_failed` frames carry the same finalized\n`outcome`. Clients must correlate those frames and Q26 by the prompt identifiers,\nnot infer terminality from stream activity or an earlier pending claim.\n\nReconciliation state survives client disconnect/reconnect. With the session-private durable store (`.sdk-reconciliation/`), accepted and terminal prompt records also survive **GJC session-process restart** for the same session identity within capacity, subject to crash-consistent fsync. A non-terminal prompt record at restart finalizes its pending outcome and receipt state. A stopped prompt without receipt evidence becomes `terminal_ok + missing`; failed prompt or skill settlement without body evidence becomes `unknown`. Eviction or absence still returns honest `unknown`; that means the prior outcome is unknowable, not that execution did not occur. Active records are capped at 128 per kind and are never aged into terminal. Terminal records are capped at 256 per kind and evicted oldest-terminal first, with no age-based eviction. Reconciliation stores no prompt, transcript, credential, or provider-response body.\n\n`turn.prompt` remains ordered and non-idempotent. Its envelope `idempotencyKey`\ndoes not replay a response or produce `idempotency_conflict`. A retained duplicate\n`clientRef` fails before execution with `client_ref_conflict`, but callers must not\nreuse a `clientRef` as a retry mechanism: after eviction the same value can identify\na new prompt while the old outcome remains unknown.\n\n`turn.abort` returns a typed disposition. A caller that does not own the target\nreceives `resource_gone`; it must not treat that result as cancellation of another\nprompt.\n\n`sdk.promptDeadlineMs` defaults to `1_800_000`. It accepts only safe integers in\n`[60_000, 86_400_000]`; there is no disable value. The SDK snapshots the setting\nwhen the prompt is durably accepted as the initial inactivity lease. Fresh\n**attributable** progress for the exact accepted `commandId`/`turnId` — `tool_execution_start` /\n`tool_execution_end` observed at the prompt/agent runtime boundary — renews the deadline to\n`lastProgressAt + sdk.promptDeadlineMs`, bounded by the hard maximum `sdk.promptMaxRuntimeMs`\n(default `21_600_000`, same `60_000–86_400_000` range). Only tool-execution boundaries for the\naccepted turn count; heartbeats, streaming text/thinking deltas, retries, other turns/sessions, and\nunrelated session noise do not renew the lease, and out-of-order delivery never shortens it. The\nhard maximum is never unbounded: every renewal is capped at `acceptedAt + sdk.promptMaxRuntimeMs` so a\nwedged or continuously noisy prompt still reaches a deterministic terminal outcome. Terminalization then has a fixed `10_000` ms\ngrace period, which is not configurable. A controlled terminal failure reaches ACP\nas JSON-RPC `-32603` with `data.code` of `prompt_failed` or\n`prompt_deadline_exceeded`.\n\n## Skill invoke reconciliation\n\n`skill.invoke` accepts optional `clientRef` and returns an early accepted receipt\n`{ accepted: true, commandId, turnId, clientRef?, name, path, lineCount?, args? }` after\ndurable/preflight accept (SDK control path), not after skill completion. Query prior\nstatus with `Q26` / `turn.result` and `kind: \"skill\"`. `skill.invoke_status`\nremains a legacy skill-only alias that injects the same `kind`. Kind-scoped indexes\nmean prompt and skill `clientRef` values never collide. Skill records use the same\ncapacity/retention limits, but an active skill record at restart settles with\n`error.code = process_restart`.\n\n## Correlated steer acknowledgement (Q30)\n\n`turn.steer` accepts an optional `clientRef` (trimmed, non-empty, at most 128 characters). When present, GJC hashes the exact validated steer text with SHA-256 and durably reserves `dispatching` before queueing. The result contains `sessionId`, `clientRef`, `status: accepted | rejected | uncertain`, known `acceptedAt` or `terminalAt`, and bounded error metadata; it never echoes text.\n\nReplay the same `clientRef` with the same text to recover the retained result without dispatching again. Reuse with different text returns `client_ref_conflict`. A live `dispatching` record and a restart during dispatch both project `uncertain`; GJC never automatically redispatches an ordered control whose first effect is unknowable. Query the same session with:\n\n```json\n{ \"type\": \"query_request\", \"query\": \"turn.steer_status\", \"input\": { \"clientRef\": \"steer-018f\" } }\n```\n\nQ31 returns `accepted`, `rejected`, `uncertain`, or `unknown` and never dispatches work. Settled steer records share the 256-record oldest-terminal-first capacity bound with no age-based eviction; live dispatching records are not terminal-evicted. Existing uncorrelated `turn.steer` calls retain their legacy non-idempotent behavior. Version-1 reconciliation remains additive, and only digest plus bounded metadata is stored—never steer text.\n\n## Model profile discovery and validation (Q27)\n\n`Q27` / `models.profiles.list` pages the effective model-profile catalog owned by\nthe attached session. Rows are sorted by exact ID and contain only:\n\n```json\n{ \"id\": \"codex-medium\", \"displayName\": \"codex-medium\", \"source\": \"builtin\" }\n```\n\n`source` is `builtin` or `configured`. Profiles from `<agentDir>/models.yml`\noverride built-ins with the same exact ID, including their display label. Profile\nIDs are not trimmed, case-folded, sanitized, or restricted to safe-token names;\ndiscover the exact ID and send it unchanged. The retired `codex-standard` alias is\nfallback-only and never shadows a configured profile with that exact ID.\n\nQ27 uses retained-revision, connection-bound pagination. Continue an issued cursor\nto finish its stable snapshot; a fresh cursorless query observes the current\nregistry. The query accepts no root, path, or selector input. An invalid or\nunreadable `models.yml` fails closed with `model_profile_registry_error` rather\nthan returning a plausible built-ins-only catalog.\n\nBroker `session.create`, `session.fork`, and `session.resume` validate `modelPreset`\nbefore spawning against the same `<broker.settings.agentDir>/models.yml` authority\nthat the child receives through `GJC_AGENT_DIR` / `GJC_CODING_AGENT_DIR`. Unknown\nIDs return `unknown_model_profile`. Both typed errors include bounded `details`\nwith `requestedProfile` where applicable, whole exact `availableProfiles` entries\nthat fit the detail budget, and `discoveryQuery: \"models.profiles.list\"`. The\ndiscovery pointer is authoritative when the bounded error cannot include every ID.\n\nThe same lifecycle operations accept an optional `modelId`: an explicit\n`provider/model` pin with `gjc --model` grammar (#4707). Coordinators resolve it\nagainst the full model registry (the CLI `--model` surface, not the\nauthenticated-only subset) before sending the create request, so unknown ids are\nrejected before any session exists. The broker guards only its shape; the child\napplies it exactly like a CLI `--model` selection, which also means an explicit\n`modelId` wins over `modelPreset` when both are supplied — the same precedence as\n`gjc --mpreset <profile> --model <model>`. The full effective order is:\n\n```\nmodelId pin > modelPreset > configured modelProfile.default > role/resume/default\n```\n\nThe pin is a guarantee, not a preference. The coordinator validates against its\nown registry and the child owns the registry that actually serves requests, so\nthe two can drift (a model removed, a provider disabled, an extension that\nfailed to register). On drift the child fails session construction before\nreadiness and before any profile application, disposes the partial session, and\nreports an error naming the pinned selector — it never publishes success on a\nsubstituted model. Coordinator validation refreshes the registry per request\n(offline, from the on-disk discovery cache) so ids added or removed after an\nearlier pin are judged against current contents.\n\n### Active provider query (Q29)\n\n`Q29` / `providers.list/active` pages the providers currently eligible for model\nselection through the same authenticated, retained-snapshot envelope as Q10. Each\nrow is the non-secret DTO `{ provider, connectionKind }`, where `connectionKind`\nis `credential` or `credentialless`.\n\nProvider IDs are returned exactly as they appear in Q10 `model.provider`: existing\nmixed-case, spaced, punctuated, and long custom IDs are preserved without aliases\nor normalization. Rows are deduplicated and ordered by UTF-8 provider bytes.\nJoin Q29 to Q10 by exact provider ID; Q10 remains the full configured catalog.\n\nA credentialed discovery-only provider appears only after fresh discovery proves\nthe exact model is usable. Static configured models can appear without a network\nprobe. The query never invokes a model, refreshes credentials, probes a remote\naccount, or exposes credentials, account metadata, paths, or provider responses.\n\nResolver failures are atomic and return\n`{ \"code\": \"internal\", \"message\": \"Unable to resolve active providers.\" }`.\nThey omit a page and restart metadata. An expired continuation follows the shared\ncursor contract and returns `error.code: \"cursor_expired\"` with\n`error.restartQuery: true`. Malformed cursor strings return `invalid_cursor`;\ncross-query or selector mismatches return `invalid_input`.\n\n## Answer semantics\n\nA remote reply answers a pending ask in every session state:\n\n- **Interactive / TUI mode:** the ask tool races the local selector against the\n remote reply (first valid answer wins). A client submits generic `reply` using\n the active presentation `id`; a local answer emits `action_resolved`\n (`resolvedBy: \"local\"`) and that presentation becomes non-repliable.\n- **SDK workflow gate:** generic `reply` still uses the active presentation\n `id`, never `workflowGateId`. The resolved gate drives the session the same\n way a local answer would.\n\nA session has at most one active answerable presentation. Interactive asks and durable workflow gates are serialized; further Q12 gates wait in a durable queue. A same-server reconnect replays the active `action_needed` with the same presentation ID. After a process restart, previously pending or accepted-but-unadvanced records are quarantined diagnostics and a reconstructed workflow remints fresh durable gate and presentation IDs. Terminal, stale, and reissued action IDs never regain authority.\n\nGeneric and direct controls may race. Once the native generic claim is acquired, it wins; a direct control that atomically retires the exact unclaimed active presentation first wins instead. Losing direct controls fail without advancing the gate, and losing generic replies are stale/non-repliable. Clients must not retry by matching text, durable IDs, or presentation history; they must fail closed rather than guess when session or action identity is unsafe or ambiguous.\n\n### Durable workflow controls and Q12\n\n`workflow.gate_answer` and `workflow.plan_approve` operate on the durable Q12\n`gate_id`, not `action_needed.id`. Managed adapters bind `expectedSessionId`\nfrom their current Router-issued attachment. Process-isolated operators use the\nraw-control flow documented in the [SDK session CLI guide](./sdk-session-cli.md); they do not construct protocol frames.\n\n`expectedSessionId` omission remains accepted and audited for the entire SDK v3 line so deployed v3 control clients continue to work; new clients must send it now. It cannot become mandatory, or be removed from the controls, before SDK v4 and at least one full published deprecation release/window with deployed-client notice. A supplied session mismatch is rejected before the gate resolver runs. Neither control accepts a presentation ID, remaps an old ID to a reminted gate, or uses heuristic matching.\n\nQ12 (`workflow.gates.list`) exposes durable query records and additive SDK v3 diagnostics. A pending record preserves its workflow fields including `gate_id` and adds `id: \"pending:<gate_id>\"` and `tag: \"pending\"`. A restart quarantine diagnostic uses `id: \"diagnostic:<gate_id>\"`, `tag: \"quarantined\"`, and optional `lifecycle` containing `state: \"quarantined\"`, its restart reason, `quarantinedAt`, and an optional `supersededByGateId` after a remint. Diagnostics are query-only: they cannot be routed, answered, or promoted. Treat Q12 as the durable status surface, not as generic-reply authority.\n\n### Coordinator MCP question pull loop\n\nThe Coordinator MCP bridge is a separate, public-safe pull surface for external coordinators. `gjc_coordinator_list_questions` requires `session_id` and reconciles pending `workflow.gates.list` rows on every call, returning bounded public `questions`, `diagnostics`, and `reconciliation`. It accepts `status: \"pending\"`; `status: \"open\"` remains a compatibility alias. Multiple pending rows can be returned. A pending row carries its safe question shape, public option ids, and `answer_binding`, never raw/private gate payloads or values.\n\n`gjc_coordinator_submit_question_answer` requires `session_id`, `turn_id`, `question_id`, `answer_binding`, `answer`, `idempotency_key`, and `allow_mutation: true`. It re-lists/revalidates after restart and resolves through `workflow.gate_answer`, not generic `ask.answer`. An incomplete reconciliation returns `terminal_uncertain`; stale, terminal, missing, or ownership-mismatched rows cannot be answered. Re-list after restart rather than retaining old identifiers. An identical retry with the same idempotency key replays the accepted result; conflicting reuse returns `idempotency_conflict`.\n\nThis contract does not change #2549/#2551 or unattended plain-CLI behavior.\n\n### Rust and N-API compatibility\n\nThe Rust `ActionNeeded`, `ServerMessage`, and `register_ask` APIs remain\nlegacy-compatible and uncorrelated. Correlation is available through additive\nRust workflow-frame decoding/current-reader APIs and the workflow registration\npath; consumers that need correlation must opt in explicitly. N-API likewise\nretains `registerAsk`, and adds `registerWorkflowGateAsk` for a correlated wire\nframe plus `registerArbitratedAsk` and `retireIfUnclaimed` for in-process\npresentation arbitration. The arbitration lease and all claim/receipt/epoch\nstate remain private: these APIs do not create a public authority value.\n\n### Runtime and native addon release pairing\n\nThe `@gajae-code/coding-agent` runtime and `@gajae-code/natives` native addon ship from the same source release at exact matching package versions. The native loader requires the matching version sentinel; mixed native/runtime versions are unsupported and must not claim SDK compatibility.\n\n## Minimal provider adapter example\n\nProvider integrations compose SDK core's `SessionRouter`; they never read endpoint files or retain URL/token credentials:\n\n```js\nimport { router } from \"@gajae-code/coding-agent/sdk\";\n\nconst sessionRouter = new router.SessionRouter({\n agentDir,\n deps: {\n onAttachment: (attachment) => provider.bind(attachment.sessionId, attachment),\n onFrame: (attachment, frame) => provider.render(attachment.sessionId, frame.body),\n onSessionRemoved: (attachment) => provider.unbind(attachment.sessionId),\n },\n});\n\nawait sessionRouter.start();\nconst attachment = sessionRouter.attachment(sessionId);\nif (!attachment) throw new Error(\"session attachment unavailable\");\nawait attachment.send({ type: \"reply\", id: actionId, answer });\n```\n\nTelegram, Discord, Slack, and third-party adapters own only their provider transport and presentation state. `SessionRouter` performs exact endpoint resolution, credential custody, replay, reconnect, rotation, and dispatch-time stale-lease rejection.\n\n### Exact generation reconciliation\n\nManaged consumers that close or delete a session can reconcile one previously\npersisted attachment generation without retaining endpoint credentials:\n\n```ts\nconst proof = await sessionRouter.generationStatus(sessionId, endpointGeneration);\n\nswitch (proof.status) {\n case \"current\":\n // This exact generation is the current live indexed authority.\n break;\n case \"retired\":\n // A retained host-unregister, close, or delete event positively retired it.\n break;\n case \"replaced\":\n // A different live generation is current; use proof.currentGeneration.\n break;\n case \"unknown\":\n // Do not infer retirement or retry a possibly-applied lifecycle mutation.\n break;\n}\n```\n\n`generationStatus(sessionId, endpointGeneration)` is credential-free and may be\ncalled after `SessionLifecycleService` returns from `session.close` or\n`session.delete`, and after `SessionRouter.stop()`. Its evidence contains only\n`source: \"session_index\"`, a coherent `observedIndexSeq`, the successful\nstate's `evidenceIndexSeq`, and, for positive retirement, the safe terminal\nevent kind. It never exposes endpoint URL/token data, process IDs, locators,\nprivate Broker responses, or lifecycle cleanup payloads.\n\nThe result is an observation of one locked index reconciliation cut:\n\n- `current` requires the queried generation to be the current live, unambiguous\n indexed authority.\n- `retired` requires a retained exact-generation `host_unregistered`,\n `session_closed`, or `session_deleted` event. Missing attachment, missing\n endpoint publication, Router shutdown, or a dead/unreachable host never imply\n retirement.\n- `replaced` requires both prior observation of the queried generation and a\n strictly greater current live generation. Same-generation reconnect remains\n current; reuse, regression, or safe-integer wrap is classified as unknown\n rather than manufacturing a successor relationship.\n- `unknown` is fail-closed. Reasons distinguish invalid input, unavailable or\n incomplete index reconciliation, an unobserved session/generation, ambiguous\n authority, and detected generation reuse. Consumers must preserve uncertain\n lifecycle state rather than treating `unknown` as retired. Expired positive\n evidence is reported specifically as `proof_expired`.\n\nProof survives Broker, Router, and consumer process restart because it is read\nfrom the durable session index, not Router attachment memory. Positive terminal\nproof has the session-index `maxAgeMs` lifetime (30 days by default), even when a\ndelete tombstone is retained longer for audit. Row-bound compaction may evict a\nwhole session sooner. After proof expiry, compaction eviction, or loss of a\ncomplete index, `generationStatus` returns `unknown`, never a synthetic\nretirement proof. This bounds the public proof surface independently of durable\nBroker audit retention.\n\n## Fallback chains\n\nModel-role selectors may be ordered fallback chains; see [Fallback chains](./models.md#fallback-chains) for configuration and retry-budget details. Resolution-time skips do not consume attempts. When a request-time retry advances to another eligible entry, the selected default fallback remains sticky for later prompts in that session until an explicit model selection or a chain reset changes it.\n\n`model_fallback_switched { eventId, from, to, reason, role, scope, activeIndex, chainLength, attemptsUsed }` is the canonical session lifecycle event for every real fallback-model switch. It replaces the legacy `retry_fallback_applied` / `retry_fallback_succeeded` event names. Embedding clients can subscribe to this in-process session event; managed adapters receive only the status projections their SDK-core integration supports.\n\n\n## Managed session-directory adapter guidance\n\nSDK adapters that need to inspect saved sessions must import only the supported public surface from `@gajae-code/coding-agent/sdk`:\n\n```ts\nimport {\n SESSION_DIRECTORY_API_VERSION,\n listManagedSessionCandidates,\n resolveManagedSessionScope,\n} from \"@gajae-code/coding-agent/sdk\";\n\nif (SESSION_DIRECTORY_API_VERSION !== 1) throw new Error(\"Unsupported session-directory API\");\nconst resolved = await resolveManagedSessionScope({ cwd: process.cwd() });\nif (resolved.kind === \"resolved\") {\n const listing = await listManagedSessionCandidates({ scope: resolved.scope });\n // Consume only listing.kind === \"complete\" and its owned candidates.\n}\n```\n\nThis is a readonly resolver/listing contract. Do not import `@gajae-code/coding-agent/session/internal/*`, derive `v2-…` names, write bindings, or implement migration/cleanup in an adapter; private internal subpaths are intentionally unavailable from the packaged module. Treat `network_unsupported`, binding/security errors, incomplete listings, invalid candidates, and foreign candidates as non-authoritative results rather than retrying with a guessed path.\n\nThe resolver uses canonical native identity: supported POSIX and Windows local aliases can designate one scope, while UNC/network workspaces are unsupported. Scope digests are collision-resistant identifiers, not injective aliases, credentials, or authentication. The owner-only checks protect managed local storage paths but do not authenticate an adapter or make hostile concurrent filesystem races safe. Adapters that need mutations must use the higher-level lifecycle/session APIs rather than the readonly directory API.\n## Managed notification adapters\n\nGJC ships managed SDK adapters for Telegram, Discord, and Slack. `SessionRouter` resolves one session-owned endpoint per attachment and keeps every endpoint credential inside SDK core. Provider daemons receive only opaque attachment capabilities; they neither change the wire protocol nor expose a remote shell.\n\nThe recommended interactive path is `/settings` → **Notifications**. It owns\nsetup, health, test, recovery, reconnect, local enablement, and Telegram\nremoval without exposing stored credentials.\n`gjc notify setup` remains the authoritative CLI fallback for headless and\nautomated environments.\n\nNotification credentials and `notifications.*` settings are global-only.\nProject notification keys are\nignored and runtime notification overrides are rejected. Telegram pairing\nrevalidates the complete bot-token/chat identity immediately before polling and\nagain before activation. A foreign or unknown owner is never killed, reloaded, or taken over;\nsetup fails closed without saving or exposing the raw token.\n\nConfiguration completeness, provider-local quarantine, durable desired intent, effective enablement, runtime readiness, and delivery outcomes are separate contracts. The global `notifications.enabled` master never erases provider credentials or desired flags. `/settings` edits secrets through explicit `keep`, `replace`, or `remove` actions, commits only the selected provider in one CAS batch, and reports post-commit observer or activation failures without pretending the durable save rolled back. Malformed provider-local values are quarantined for explicit repair while safe sibling providers remain usable; malformed global notification structure remains fail-closed.\n\n`GJC_NOTIFICATIONS=0` suppresses only automatic generic current-session admission. Explicit `/notify on` can opt the current session back in without mutating durable provider state, and direct provider APIs remain governed by provider effectiveness and their own runtime readiness. Telegram, Discord, and Slack attachments are reconstructed through `SessionRouter`; no provider receives the shared endpoint token.\n\n- [Telegram notification onboarding](./telegram-onboarding.md) documents\n `gjc notify setup` and private-chat pairing.\n- [Discord notification onboarding](./discord-onboarding.md) documents\n `gjc notify setup discord`, required configuration, thread lifecycle, and\n least-privilege permissions.\n- [Slack notification onboarding](./slack-onboarding.md) documents\n `gjc notify setup slack`, Socket Mode configuration, immediate envelope ack,\n and thread lifecycle.\n\n`gjc notify status` reports provider completeness, repair/quarantine state, desired intent, effective enablement, and masked tokens. Destination identifiers remain visible and may be sensitive. The Discord and Slack setup commands are non-interactive and require their documented identifier and token flags; supply secrets through an approved local mechanism, not examples, committed files, shell history, logs, or chat. `gjc notify health --provider <provider> --probe` performs a provider-owned REST diagnostic even when complete credentials are intentionally inactive, while `gjc notify test --provider <provider>` additionally requires effective enablement and runtime readiness.\n\nSession lifecycle and attachment routing are SDK-core services shared by every\nchat provider. `SessionLifecycleService` authorizes typed create, fork, resume,\nclose, delete, and list requests, derives the Broker idempotency identity, and\nprojects credential-free outcomes. The Broker remains the only lifecycle\nexecutor and durable terminal authority.\n\n`SessionRouter` consumes the Broker `SessionIndex`, resolves exact endpoint\nauthority, retains endpoint credentials and SDK clients, and owns replay,\nreconnect, rotation, and stale-attachment revocation. Telegram, Discord, and\nSlack receive only opaque current-generation attachments. Provider daemons own\ntransport leases, cursors, rate limits, threads/topics/messages, presentation\njournals, and delivery receipts; they cannot read endpoint files or tokens,\nallocate SessionIds, or perform session process lifecycle effects.\n\n\n## Managed Telegram daemon (bundled reference client)\n\nThe managed Telegram client is a provider supervisor and presentation adapter.\nIt owns the single `getUpdates` poller and Telegram topic state, while\n`SessionRouter` reconstructs SDK attachments from Broker state. A provider\nrestart never creates, resumes, closes, or mutates a GJC session by itself.\n\nFor Telegram forum topics, the daemon deletes the presentation topic through\n`deleteForumTopic` (falling back to `closeForumTopic` when deletion is unavailable)\nwhen `SessionRouter` retires the current attachment or when the daemon confirms\nthat no eligible local session still owns the topic. A resumed session creates or\nrebinds a fresh current-generation topic before sending again. Topic cleanup is\nbest-effort and cannot change the Broker lifecycle result.\n\n### Singleton poller and trust model\n\nTelegram `getUpdates` allows only one active long-poll owner per bot token. The\nmanaged daemon enforces **one bot token = one getUpdates poller** with a local\nlock/state file under the agent directory. New sessions attach to the existing\nfresh daemon owner instead of starting another poller, preventing Telegram 409\nconflicts.\n\nThe trust model is intentionally strict:\n\n- setup pairs exactly one private Telegram chat;\n- runtime accepts updates only from that paired chat id;\n- groups, supergroups, channels, and unpaired users never receive session names,\n action ids, pending status, or configuration hints;\n- daemon state stores a token fingerprint, not the raw bot token.\n\n### Routing in private-chat topics\n\nThe paired private chat prefers Telegram topics for coordinator/lifecycle sessions\n(Threaded Mode). The daemon tags messages by session, stores compact callback\naliases for inline buttons, and routes replies back to the exact session/action.\nOrdinary sessions use flat delivery and do not create topics. A forum-enabled\nsupergroup is no longer required: when the bot owner enables Threaded Mode in\n@BotFather, the daemon creates topics only for admitted orchestration sessions.\nGJC cannot enable Threaded Mode through the Bot API; setup only verifies the\ncapability and guides the manual BotFather toggle.\n\nIf BotFather's per-bot **Bot Settings** menu does not show **Threads Settings**\nor **Threaded Mode**, the supported fallback is the normal private-chat pairing.\nSetup can be saved as `threaded=unverified`/`threaded=unknown`, and the daemon\nstill tries topics when Telegram allows them. When `createForumTopic` is refused,\nthe daemon does not drop the send: it routes the notification to the normal\n(flat) paired private chat and posts a one-time nudge: `Flat Telegram private chat\nsupports outbound notifications and inline ask buttons only. Enable Threaded Mode\nin @BotFather > Bot Settings > Threads Settings for free-text replies and session\ncommands.` Pairing is private-only, so flat delivery stays within the user's own\nprivate DM.\n\nSupported reply paths:\n\n- tap an inline button on an ask notification;\n- reply inside the session's thread/topic (replies are thread-native; the\n topic identifies the session, so no session tag is needed).\n\nIn threaded mode the user can also adjust per-session behaviour with in-thread\nconfig commands: `/verbose` (per-tool-turn assistant text), `/lean` (settled\nassistant answer at idle plus immediate ask lead-ins; the default),\n`/verbosity <lean|verbose>`, and `/redact <on|off>`. The legacy\n`/answer <session-tag> <answer>` command is removed — replies are routed by the\ntopic they arrive in.\n\nFlat fallback keeps outbound notifications and inline-button answers working, but\nplain free-text never guesses from the global pending-ask set. Free-text replies\nand `/verbose`/`/lean`/`/verbosity`/`/redact` commands are thread-native and\nrequire Threaded Mode/topic routing. Enable Threaded Mode in @BotFather > Bot\nSettings > Threads Settings when you need free-text replies or session commands.\nDo not pair a group, supergroup, or channel to work around a missing BotFather\nmenu; the bundled setup flow is\nprivate-chat only, and non-private chat ids remain fail-closed to avoid session\ndata leaks.\nBecause the flat private chat has no per-session topic, flat idle markers carry\na short session tag (`🟢 Agent idle · <tag>`, last six characters of the session\nid) so concurrent sessions stay distinguishable. The tag never appears on asks\nor in threaded/topic delivery, where the topic itself identifies the session.\n\nUnknown, expired, or restart-unvalidated callback aliases fail closed: the daemon\nsends guidance and does not guess a target session or action.\n\n### Discord and Slack setup\n\nDiscord and Slack use the same internal notification events and reply protocol as\nTelegram. Store only runtime credentials in local GJC settings or environment;\nnever paste bot tokens, webhook URLs, transcripts, prompts, host paths, or raw logs\ninto docs, tests, issues, or PR comments.\n\nConfiguration keys:\n\n```yaml\nnotifications:\n enabled: true\n discord:\n botToken: \"<local Discord bot token>\"\n applicationId: \"<Discord application id>\"\n guildId: \"<Discord guild id>\"\n parentChannelId: \"<Discord parent channel id>\"\n slack:\n botToken: \"<local Slack bot token>\"\n appToken: \"<local Slack app-level token>\"\n workspaceId: \"<Slack workspace id>\"\n channelId: \"<Slack channel id>\"\n authorizedUserId: \"<Slack user id authorized for inbound replies and commands>\"\n redact: true\n```\n\nThe bundled adapters intentionally render public-safe message bodies and return\nroute metadata only for pending internal actions. They do not own polling,\nsession scans, daemon locks, rate limits, or SDK lifecycle. Production transport\nsenders should consume the adapter payloads and keep all credential-bearing HTTP\nor gateway details outside logged payloads.\n### Redaction\n\n`notifications.redact` strips sensitive content before remote delivery, but\n**asks are exempt**: an ask is an interactive prompt the human must read and\nanswer remotely, so its `question` and `options` are always sent unredacted\n(otherwise it would be unanswerable). When redaction is enabled, `idle`\nsummaries are removed and streamed content frames (`turn_stream`,\n`context_update`, `image_attachment`) are suppressed at their emit sites. When\nredaction is disabled, all content is delivered unchanged.\n\n### Local `/notify`\n\nInside a GJC session, `/notify` controls the current session only:\n\n- `/notify status` reports enabled/disabled state, daemon observation when known,\n and redaction state without printing secrets;\n- `/notify off` disables the current session's notification endpoint and removes\n its discovery record without mutating global Settings;\n- `/notify on` re-enables the current session when global setup is complete and\n `GJC_NOTIFICATIONS=0` is not forcing opt-out.\n\n## Session lifecycle and attachment surfaces\n\nSDK core exposes two related provider-neutral capabilities:\n\n1. **`SessionLifecycleService`** accepts an authenticated actor, an explicit\n operation capability, a stable caller request key, and a typed target. It\n derives one Broker idempotency key and invokes the canonical Broker lifecycle\n operation. Results never expose endpoint URLs, tokens, process identities,\n cleanup paths, or raw Broker receipts.\n2. **`SessionRouter`** owns live attachment discovery and transport. It validates\n the exact indexed endpoint generation, keeps credentials and `SdkClient`\n instances private, replays from the attachment cursor, reconnects after\n rotation, and revokes stale capabilities. Provider-facing attachments expose\n only `sessionId`, `generation`, `isCurrent()`, and `send()`.\n\n### Dispatch-boundary observers (managed router path)\n\n`SessionRouter.request(sessionId, frame, expectedGeneration?, expectedAttachment?, options)`\naccepts two optional synchronous observers in `options` — the supported\ndispatch-boundary surface for transport-close-aware consumers (#4640):\n\n- **`beforeDispatch(context)`** — fires immediately before the wire write.\n Throwing (or any synchronous failure) aborts the dispatch with nothing on the\n wire, no sent record, and a retryable rejection carrying the caller's own\n error. Returning a thenable (e.g. an `async` function) is a contract\n violation: the dispatch aborts pre-send and the eventual rejection is sunk.\n- **`onDispatch(context)`** — fires synchronously immediately after the frame is\n handed to the socket, never before. `context.frame.id` is the exact correlated\n identity a response must carry; from this point a transport close before the\n response settles the request as `uncertain_after_send`. Observer throws and\n returned-thenable rejections are sunk; they can neither displace settlement\n nor reach the process unhandled-rejection channel.\n\nThe observer `context.frame` is a **deep-frozen, credential-redacted copy**: the\ninjected session endpoint `token` (and any other credential field) exists only\non the internal wire frame and is never handed to observer code, and mutation\nattempts throw in strict mode. The raw credential-bearing `SdkClient` remains\nunexported (`./sdk/client` is blocked in the package export map); the router is\nthe only supported path to this boundary.\n\nThere is no daemon-owned lifecycle control endpoint, provider lifecycle ledger,\nnotification-root scanner, or provider-created SessionId. Telegram `/session_*`\ncommands call the SDK lifecycle service directly. A Telegram update or topic\nreservation supplies the stable provider request identity; the Broker allocates\nthe SessionId, and Telegram CAS-binds the returned opaque ID to its presentation\nmapping.\n\n### Lifecycle trust and recovery\n\n- paired provider identity and operation capability are checked before the\n Broker call;\n- retries reuse the same provider request key, so one request produces one\n Broker ledger identity and at most one lifecycle effect;\n- `terminal_uncertain` remains uncertain and is reconciled from Broker ledger,\n effect marker, process incarnation, endpoint/index, readiness, and exact\n cleanup evidence only;\n- provider transport restart reloads cursor and presentation state, while the\n Router reconstructs attachments from Broker state;\n- stale endpoint generations and attachments fail closed;\n- provider topic/thread/message cleanup cannot rewrite a confirmed lifecycle\n outcome.\n\n### Phone test guide (create / close / resume from Telegram)\n\nEnd-to-end manual check once `gjc notify setup` has paired your private chat:\n\n1. Run `gjc notify setup` and start or reload the Telegram provider supervisor.\n The supervisor owns only the Telegram poller and presentation state.\n2. Send `/session_create path <repo-dir>`, `/session_create worktree <repo>\n <branch>`, or `/session_create dir <newdir>`. The SDK lifecycle service submits\n one canonical Broker create request; the bot reports the credential-free\n outcome.\n3. `/session_recent` lists verified recent managed sessions.\n4. `/session_close <sessionId>` asks Broker lifecycle to close the exact managed\n session and preserves history.\n5. `/session_resume <sessionId|prefix>` resolves verified managed history,\n reattaches a live session or performs canonical Broker resume, and refuses\n ambiguous prefixes.\n\nCommands are accepted only from the paired chat. Duplicate Telegram updates and\nreplayed topic reservations reuse their original request identity; they never\nallocate or spawn a second session.\n\n## Master mode surfaces\n\n`gjc --master [--scope repo|pwd|global]` launches an interactive-TUI-only\nmaster session (default scope `repo`). The master receives one bundled\noperating-guidance block appended after custom system-prompt transformation and\nexactly one scoped, no-probe broker peer snapshot immediately before its first\naccepted provider request; idle masters collect nothing.\n\nMaster orchestration is broker-routed and best effort: scoped discovery via\n`gjc sdk search` (result-level scope envelopes, exact worktree/cwd identity,\nno scope fallback) and task-seeded children via the local-only `gjc sdk spawn`\n(see docs/sdk-session-cli.md). The broker is the sole authority for\nmaster-created lifecycle: an opaque idempotency identity serializes each spawn\nbehind a durable create-or-join claim, seed delivery is Q26-correlated (never\nblindly re-sent), close mutates only an exactly re-proven substrate, and\norphaned children converge through ordinary close after\n`sdk.masterOrphanGraceMs`. Task text and master capability are transient\ndispatch inputs and never persist in any durable store, log, or output.\n",
|
|
93
|
+
"sdk-rpc-parity-audit.md": "# SDK v3 RPC parity audit\n\n**Status:** internal, closed-inventory audit. This is a comparison of the retired\nRPC contract at `6e147d58~1:docs/rpc.md` with SDK v3; it is not an event-plane\nparity claim. The CLI rejects the retired `--mode rpc`, `rpc-ui`, and `bridge`\nmodes and directs external control to the SDK (`packages/coding-agent/src/cli/args.ts:117-127`).\n\nThe historical issue files (01–08, 11–21) are archived under `issues/archive/` as provenance only. Their current disposition is recorded in `issues/README.md`: implementation findings are resolved, retired RPC documentation findings are obsolete, and persistent-session/registry items (09/10) remain deferred architectural follow-ups. Do not treat this closed audit as an active implementation backlog.\n\n## Method and classifications\n\nThe inventory below is **closed**. Command, frame, and sub-protocol rows were\nrecovered from `git show 6e147d58~1:docs/rpc.md`; the supplemental\n`rpc-sessions` registry and `--listen` Unix-socket rows were recovered from\nparent-commit source because they do not appear in that document:\n`6e147d58~1:packages/coding-agent/src/cli/args.ts:157-158`,\n`6e147d58~1:packages/coding-agent/src/modes/rpc/rpc-mode.ts:892-907,984-992`,\nand\n`6e147d58~1:packages/coding-agent/src/modes/shared/agent-wire/session-registry.ts:1-53`.\n`SDK equivalent` means a current operation or documented SDK protocol covers the\ncontrol/query intent, not that its transport or event semantics are identical.\n`transport-gap — retired` means the direct `gjc sdk serve` relay and standalone transport clients were removed when `SessionRouter` became the sole external attachment authority.\n`phase-2-gap` means no equivalent has been implemented by this audit.\n\nOperation names and their stated roles are from\n`packages/coding-agent/src/sdk/protocol/operation-registry.ts:66-166`; dispatch\ncoverage is from `packages/coding-agent/src/sdk/host/control/dispatch.ts:138-253`.\nSDK protocol and lifecycle references use stable heading references in\n`docs/sdk.md`. Command, frame, and sub-protocol rows cite\n`6e147d58~1:docs/rpc.md`; the two supplemental rows cite the parent-commit\nsources above.\n\n## Closed command inventory\n\n| Retired family | Retired command | SDK v3 equivalent or classification | Evidence |\n| --- | --- | --- | --- |\n| Prompting | `prompt` | `turn.prompt` | retired doc; registry:67; dispatch:139-140 |\n| Prompting | `steer` | Partial SDK equivalent: `turn.steer` is text-only and loses retired `images` | `6e147d58~1:docs/rpc.md:77`; registry:68; dispatch:141-142 |\n| Prompting | `follow_up` | Partial SDK equivalent: `turn.follow_up` is text-only and loses retired `images` | `6e147d58~1:docs/rpc.md:78`; registry:69; dispatch:143-144 |\n| Prompting | `abort` | `turn.abort` | retired doc; registry:70; dispatch:145-146 |\n| Prompting | `abort_and_prompt` | `turn.abort_and_prompt` | retired doc; registry:71; dispatch:147-148 |\n| Prompting | `new_session` | Partial SDK equivalent: `session.new` takes no input and loses retired `parentSession` | `6e147d58~1:docs/rpc.md:81`; registry:93; dispatch:196-197 |\n| State | `get_state` | Partial SDK equivalent: query bundle `context.get` (includes `systemPrompt`), `tools.list` (Q20), `models.list/current`, `todo.list`, `queue.messages.list`, `session.metadata`, and `session.stats`; no one-shot legacy-shaped snapshot, no retired `dumpTools` include-toggle/exact dump schema, and some legacy snapshot fields remain absent | `6e147d58~1:docs/rpc.md:85,169-222`; registry:132-152; sdk/bus/index.ts:1804-1808,1852-1855; host/query/handlers.ts:91,116; docs/sdk.md “Protocol” and “Model catalog query (Q10)” |\n| State | `set_todos` | `todo.replace` | retired doc; registry:78; dispatch:166-167 |\n| State | `set_host_tools` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_tools.register` | `6e147d58~1:docs/rpc.md:87,255-291`; registry:105,164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| State | `set_host_uri_schemes` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_uri.register` | `6e147d58~1:docs/rpc.md:88,293-323`; registry:106,165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| State | `workflow_gate_response` | `workflow.gate_answer` (durable Q12 gate ID) | retired doc; registry:73; dispatch:151-157; docs/sdk.md “Durable workflow controls and Q12” |\n| Model | `set_model` | `model.set` | retired doc; registry:79; dispatch:168-169 |\n| Model | `set_default_model_selection` | `model.set` with `thinkingLevel`; equivalent active-model/default-selection intent, not the retired durable-selector response envelope | retired doc; registry:79; dispatch:168-169; docs/sdk.md “Model catalog query (Q10)” |\n| Model | `cycle_model` | `model.cycle` | retired doc; registry:80; dispatch:170-171 |\n| Model | `get_available_models` | `models.list/current` / Q10 | retired doc; registry:141; docs/sdk.md “Model catalog query (Q10)” |\n| Thinking | `set_thinking_level` | `thinking.set` | retired doc; registry:81; dispatch:172-173 |\n| Thinking | `cycle_thinking_level` | `thinking.cycle` | retired doc; registry:82; dispatch:174-175 |\n| Queue modes | `set_steering_mode` | `queue.steering_mode.set` | retired doc; registry:84; dispatch:178-179 |\n| Queue modes | `set_follow_up_mode` | `queue.follow_up_mode.set` | retired doc; registry:85; dispatch:180-181 |\n| Queue modes | `set_interrupt_mode` | `queue.tool_interrupt_policy.set` | retired doc; registry:86; dispatch:182-183; renamed from `queue.interrupt_mode.set` (values `abort_tools` / `finish_tools`) |\n| Compaction | `compact` | Partial SDK equivalent: `compaction.run` takes no input and loses retired `customInstructions` | `6e147d58~1:docs/rpc.md:111`; registry:87; dispatch:184-185 |\n| Compaction | `set_auto_compaction` | `compaction.auto.set` | retired doc; registry:88; dispatch:186-187 |\n| Retry | `set_auto_retry` | `retry.auto.set` | retired doc; registry:89; dispatch:188-189 |\n| Retry | `abort_retry` | `retry.abort` | retired doc; registry:90; dispatch:190-191 |\n| Bash | `bash` | `bash.execute` | retired doc; registry:91; dispatch:192-193 |\n| Bash | `abort_bash` | `bash.abort` | retired doc; registry:92; dispatch:194-195 |\n| Session | `get_session_stats` | `session.stats` | retired doc; registry:146; docs/sdk.md “Protocol” |\n| Session | `export_html` | Partial SDK equivalent: `session.export_html` takes no input and loses retired `outputPath` | `6e147d58~1:docs/rpc.md:127`; registry:101; dispatch:212-213 |\n| Session | `switch_session` | Partial SDK equivalent: retired `switch_session` was path-addressed (`sessionPath`), while `session.switch` is ID-addressed | `6e147d58~1:docs/rpc.md:128`; registry:97; dispatch:204-205 |\n| Session | `branch` | `session.branch` | retired doc; registry:98; dispatch:206-207 |\n| Session | `get_branch_messages` | `session.branch_candidates` plus `transcript.list`/`transcript.body`; no identical combined payload | retired doc; registry:132-133,147; docs/sdk.md “Protocol” |\n| Session | `get_last_assistant_text` | `session.last_assistant` | retired doc; registry:148; docs/sdk.md “Protocol” |\n| Session | `set_session_name` | `session.rename` | retired doc; registry:99; dispatch:208-209 |\n| Messages | `get_messages` | `transcript.list` and `transcript.body`; no identical monolithic payload | retired doc; registry:132-133; docs/sdk.md “Protocol” |\n\n## Closed framing, sub-protocol, registry, and transport inventory\n\n| Retired family | Retired frame, protocol, or transport | SDK v3 equivalent or classification | Evidence |\n| --- | --- | --- | --- |\n| Outbound frame | `ready` | transport-gap — closed by Phase 1; WebSocket connection/authentication replaces JSONL readiness | retired doc; docs/sdk.md §Endpoint discovery |\n| Outbound frame | `response` | transport-gap — closed by Phase 1; SDK control request/response replaces JSONL `RpcResponse` | retired doc; registry:66-119; dispatch:138-253 |\n| Outbound frame | canonical `event` | phase-2-gap; no renderer-grade canonical `AgentSessionEvent` stream | retired doc; docs/sdk.md §Protocol |\n| Outbound frame | `workflow_gate` | Partial SDK equivalent: `action_needed` with `workflowGateId`, plus Q12; not the retired frame/schema | retired doc; docs/sdk.md §Server → client, §Durable workflow controls and Q12 |\n| Outbound frame | `extension_ui_request` | phase-2-gap for extension UI methods; `action_needed` covers only generic asks | retired doc; docs/sdk.md §Server → client |\n| Outbound frame | `host_tool_call`, `host_tool_cancel` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_tool.invoke/cancel/update/result` with `host_tools.register` | `6e147d58~1:docs/rpc.md:45-46,357`; registry:105,164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Outbound frame | `host_uri_request`, `host_uri_cancel` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_uri.read/write/cancel/result` with `host_uri.register` | `6e147d58~1:docs/rpc.md:46,357`; registry:106,165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Outbound frame | `extension_error` | phase-2-gap; no SDK extension-error frame contract | retired doc; docs/sdk.md §Protocol |\n| Inbound frame | `RpcCommand` | SDK control and query operations | retired doc; registry:66-157; dispatch:138-253 |\n| Inbound frame | `workflow_gate_response` | `workflow.gate_answer` | retired doc; registry:73; docs/sdk.md “Durable workflow controls and Q12” |\n| Inbound frame | `extension_ui_response` | phase-2-gap except generic `reply` for an `action_needed` ask | retired doc; docs/sdk.md §Client → server |\n| Inbound frame | `host_tool_update`, `host_tool_result` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_tool.invoke/cancel/update/result` | `6e147d58~1:docs/rpc.md:54`; registry:164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Inbound frame | `host_uri_result` | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: reverse `host_uri.read/write/cancel/result` | `6e147d58~1:docs/rpc.md:55`; registry:165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Workflow gate sub-protocol | `workflow_gate` / `workflow_gate_response` with schema and durable broker semantics | Partial SDK equivalent: `action_needed`, `reply`, Q12 `workflow.gates.list`, and `workflow.gate_answer`; IDs and authority rules differ | retired doc; registry:73,143; docs/sdk.md “Answer semantics” and “Durable workflow controls and Q12” |\n| Extension UI sub-protocol | select/confirm/input/editor/cancel/notify/status/widget/title/editor-text | phase-2-gap; generic action presentation is not extension UI parity | retired doc; docs/sdk.md §Server → client |\n| Host tool sub-protocol | registration, call/cancel, update/result | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_tools.register` plus reverse callback operations | `6e147d58~1:docs/rpc.md:45,54,255-291,357`; registry:105,164; dispatch:220-221; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Host URI sub-protocol | scheme registration, read/write/cancel/result | Partial SDK equivalent — provider-only/machine attachment; not installed on the ordinary per-session endpoint: `host_uri.register` plus reverse callback operations | `6e147d58~1:docs/rpc.md:46,55,293-323,357`; registry:106,165; dispatch:222-223; sdk/bus/index.ts:1654,1726-1738,2325-2327 |\n| Unattended sub-protocol | `negotiate_unattended` declaration/budget/scopes/allowlist | phase-2-gap | retired doc; docs/sdk.md §Coordinator MCP question pull loop |\n| `rpc-sessions` registry | Cross-process session registry and reattach semantics | phase-2-gap. Per-session discovery files are only partial endpoint location, not a registry/reattach protocol | parent source: `6e147d58~1:packages/coding-agent/src/modes/rpc/rpc-mode.ts:892-907,984-992`; `6e147d58~1:packages/coding-agent/src/modes/shared/agent-wire/session-registry.ts:1-53`; docs/sdk.md §Endpoint discovery, §Architecture |\n| Transport | stdio JSONL | retired. Direct stdio relay was removed; managed clients use Router-owned capabilities. | parent source: `6e147d58~1:packages/coding-agent/src/modes/rpc/rpc-mode.ts:892-907,984-992`; removal evidence: SDK-owned session lifecycle handoff |\n| Transport | `--listen` Unix socket | retired. Direct Unix-socket relay was removed; managed clients use Router-owned capabilities. | parent source: `6e147d58~1:packages/coding-agent/src/cli/args.ts:157-158`; removal evidence: SDK-owned session lifecycle handoff |\n\n## Five-gap reduction verdict\n\nSDK v3 has broad control/query coverage: the operation registry includes turn,\nmodel, thinking, queue, compaction, retry, bash, session, host callback, and\nworkflow operations (`operation-registry.ts:66-166`), and control dispatch\nimplements the control path (`dispatch.ts:138-253`). That does **not** erase the\nuser-perceived reduction. It is **REAL** across five dimensions:\n\n1. **stdio JSONL and Unix-socket transports.** The interim `gjc sdk serve` relay was retired; external attachment now requires Router-owned opaque capabilities and never forwards endpoint credentials.\n2. **Typed Python client.** The interim public Python transport client was retired with the direct relay; cross-process integrations require Router-owned capabilities.\n3. **`negotiate_unattended`.** No fail-closed unattended negotiation with the\n retired declaration, budget, scope, and allowlist exists: this remains Phase 2.\n4. **Cross-process session registry/reattach.** Discovery files locate a live\n endpoint but do not provide the retired registry or reattach lifecycle: this\n remains Phase 2.\n5. **Renderer-grade full event stream.** SDK v3's minimal frames and optional\n threaded-client frames are not the retired canonical session event stream.\n **No event-plane parity is claimed.**\n\n## Ranked Phase-2 follow-up register — NOT implemented\n\n1. **Unattended negotiation equivalent — NOT implemented.** Add a fail-closed\n equivalent to `negotiate_unattended` only with explicit actor, budget, scopes,\n allowlist, and audit enforcement. Partial equivalent only: Q12\n `workflow.gates.list` plus the Coordinator MCP pull loop can enumerate and\n answer durable workflow gates; they are not unattended negotiation\n (`docs/sdk.md §Coordinator MCP question pull loop`).\n2. **Reattachment semantics — implemented in SDK core.** `SessionRouter` exclusively resolves private endpoint publication, retains credentials, reconnects, replays, rotates transports, and issues exact opaque attachment capabilities. External adapters do not consume discovery files or raw endpoint credentials.\n3. **Full event stream — NOT implemented.** Define a renderer-grade session\n event contract only if consumers require it. Partial equivalent only:\n `action_needed`, `action_resolved`, `reply_rejected`, and optional threaded\n frames such as `turn_stream` exist, but there is **no `onSessionEvent`-style\n SDK equivalent** (`docs/sdk.md §Server → client`).\n\n## Completeness checklist\n\n- [x] Prompting — every retired command represented.\n- [x] State — every retired command represented.\n- [x] Model — every retired command represented.\n- [x] Thinking — every retired command represented.\n- [x] Queue modes — every retired command represented.\n- [x] Compaction — every retired command represented.\n- [x] Retry — every retired command represented.\n- [x] Bash — every retired command represented.\n- [x] Session — every retired command represented.\n- [x] Messages — every retired command represented.\n- [x] Outbound and inbound frame categories — every retired category represented.\n- [x] Workflow gate sub-protocol represented.\n- [x] Extension UI sub-protocol represented.\n- [x] Host tool sub-protocol represented.\n- [x] Host URI sub-protocol represented.\n- [x] `negotiate_unattended` sub-protocol represented.\n- [x] `rpc-sessions` registry represented from parent-commit source (supplemental to the recovered document inventory).\n- [x] stdio JSONL represented from the recovered document inventory; `--listen` Unix-socket transport represented from parent-commit source.\n",
|
|
94
|
+
"sdk-session-cli.md": "# SDK session CLI\n\n`gjc sdk session` is the broker-bound command family for operating live GJC SDK\nsessions from the terminal. It replaces the removed `gjc daemon session` route.\n\nThe command family has six semantic verbs — `list`, `inspect`,\n`send`, `status`, `tail`, and `retire` — plus the explicit `raw` hatch that dispatches one\nSDK operation as `control`, `query`, or `global`.\n\nThe session CLI is advisory tooling over the SDK: every semantic verb resolves\nsessions through the SDK broker, and output is rendered through a versioned,\ncredential-free DTO. Endpoint credentials are never printed.\n\n## Broker authority\n\n`list`, `inspect`, `send`, `status`, and `tail` resolve sessions through the\nSDK broker and Router. The Router validates indexed endpoint authority and keeps\nthe connection credential in SDK core; the CLI receives only credential-free\nresults. The broker is started on demand (`ensureBroker`) when discovery is\nabsent, and an unavailable broker fails closed with a typed operational error\n(exit 1).\n\n`--agent-dir` selects the broker state directory; `--repo` selects the\nworkspace directory used for saved-session resolution (default: the current\ndirectory).\n\n## Semantic verbs\n\n### list\n\n`gjc sdk session list` queries the broker `session.list` global and projects\nevery indexed session into the versioned row DTO (`SESSION_ROWS_VERSION`). Each\nrow is credential-free and carries:\n\nThe list is fully paginated before scope filtering. By default its effective\nscope is `repo`. Select a scope with:\n\n```sh\ngjc sdk session list --scope repo|cwd|worktree|all [--repo <path>]\n```\n\n`--repo` is the selected workspace path and defaults to the process cwd. The\nresult reports the effective `scope` and a bounded credential-free `selection`\ndescriptor containing the canonical selected path and, for Git selections,\nthe canonical worktree root and Git common directory.\n\n- `repo` matches the canonical Git common directory, so the main checkout and\n linked worktrees are included while another repository is excluded.\n- `worktree` matches only the selected path's canonical containing worktree.\n- `cwd` matches only the exact canonical selected workspace; nested directories\n do not match.\n- `all` preserves the complete unfiltered Broker listing.\n\nFor a path outside Git, `repo` and `worktree` fail with the typed\n`not_a_repository` operational error and an actionable suggestion to use\n`cwd` or `all`; they never broaden the result. `cwd` remains available for an\nexact canonical path match. Unreadable or removed row workspaces are excluded\nfrom Git scopes deterministically and reported in `warnings`.\n\nThe raw global `session.list` route remains unfiltered, and `inspect`, `send`,\n`status`, `tail`, `retire`, and raw control/query behavior is unchanged.\n\n- `sessionId` and the `locator` (`cwd`, `worktreeRoot`, `stateRoot`), where `cwd`\n is the canonical workspace directory and `worktreeRoot` is the canonical Git\n worktree root or `null` outside a worktree;\n- `endpointGeneration`, `pid`, `live`, `deleted` (tombstone), `indexSeq`;\n- `hostIncarnation` and `identityProvenance` (`composite` | `legacy`);\n- `activity` (`{state: active|idle, at}`) and `lastHeartbeatAt`;\n- `terminalUncertain`, `lifecycleRequestId`, `endpointMtimeMs`;\n- `ambiguous` when the same `sessionId` has more than one unresolved\n authority-fencing `stateRoot` (cross-repo duplicate). A proven non-endpoint\n bookkeeping registration (the direct-session GC fence row, endpoint\n generation 0) stays indexed without fencing endpoint attachment; every other\n unresolved root, including an unproven generation-0 `lifecycle_terminal`\n claim, still fences.\n\n### inspect\n\n`gjc sdk session inspect <sessionId>` renders one indexed row from the broker.\nIt never reads endpoint discovery records directly: a missing or unavailable\nbroker fails closed rather than exposing endpoint authority outside SDK core.\n\n### send\n\n`gjc sdk session send <sessionId> --text <prompt>` submits an ordered\n`turn.prompt` carrying a caller-chosen operation reference (a ULID by default,\nor `--op-ref`). The result envelope reports `accepted` with the receipt and the\noperation reference used for later reconciliation.\n\n- `--wait` polls `turn.result` with `kind: \"prompt\"` until the prompt reaches a\n terminal state or the wait window (`--timeout-ms`, default 30s) elapses.\n `send --wait` never cancels a running turn; a window that elapses before a\n terminal state is reported as `wait_timeout` with the last observed status.\n\n- `--text` and the JSON input sources (`--json-input`,\n `--json-input-file` — which must be a `0600` regular file —\n `--json-input-stdin`) are mutually exclusive for the prompt body.\n\n### status\n\n`gjc sdk session status <sessionId> <opRef>` performs a lossless `turn.result`\nlookup with `kind: \"prompt\"` for a previously submitted operation reference and\nreturns the full reconciliation record plus a `summary.completed` flag.\nSee [lossless prompt results](#lossless-prompt-results).\n\n\n### tail\n\n`gjc sdk session tail <sessionId>` replays the retained transcript from the\ndurable checkpoint and then follows the live event-ring frames, emitting the\ndefault tail kinds (session lifecycle and turn lifecycle events) plus retained\ntranscript entries.\n\n- `--strict` fails closed with `retention_gap` (exit 1) when retained history\n or the event ring dropped entries before the checkpoint.\n- `--until-idle` exits once the current turn reaches a terminal state; a session\n close exits any tail as `terminal: true`, while a bare live tail otherwise\n remains attached until `--timeout-ms`. Each item carries `revision` (the durable checkpoint revision); the triple `(revision,generation,seq)` is unique for the session, while `generation:seq` alone is revision-local and restarts each accepted prompt — key idempotency on the triple. Lifecycle\n events that carry a `(generation, seq)` position are reconciled and emitted in\n that canonical order rather than arrival order, so a retained terminal event\n from an earlier turn does not complete a newer turn that is still running, and\n a terminal event observed live is not undone by replayed history that arrives\n after it. Positioned event items are emitted canonically ahead of an\n arrival-ordered segment of events that carry no position; such an event stays\n visible but cannot supersede a positioned turn state. Different lifecycle\n kinds claiming the same `(generation, seq)` fail closed with `protocol_error`\n (exit 1) because no order between them can be proven, and that conflict\n outranks an otherwise successful idle or close completion. A canonical\n position is a pair of non-negative safe integers. A replayed event-ring row that\n states either coordinate property must state both validly; a row claiming only\n one coordinate, or a null, negative, fractional, non-finite, or\n unsafe-integer coordinate, fails closed with `protocol_error` rather than\n being treated as unpositioned. Only a row that states neither coordinate is\n unpositioned. Retained transcript entries are deduplicated independently of\n event-ring rows and are not event-ring authority, so a transcript row that\n projects the same kind and position as a real event never suppresses it. For a\n specific prompt operation, `status <sessionId> <opRef>` remains the lossless\n authority.\n- `--all-events` widens the emitted set to every event-ring kind.\n- `--cursor` resumes from a saved signed checkpoint claim. `session.checkpoint`\n verifies the unexpired claim and exchanges it for a fresh connection-owned\n cursor pinned to the exact prior revision; direct cross-connection cursor\n consumption remains rejected, so reconnect never echoes or rewinds a cursor.\n- `--timeout-ms` bounds live follow; a session whose lifecycle already ended\n (terminal or `terminalUncertain`) replays retained history and exits instead\n of hanging.\n\nA deleted session has no tail (`session_deleted`). A stopped session replays\nits retained transcript without an endpoint (offline source), bounded to the\nmost recent retained entries.\n\n### retire\n\n`gjc sdk session retire <sessionId>` is the official semantic wrapper for the\n`session.reconcile_uncertain` broker global. It retires an indexed\n`terminalUncertain` create effect only when exactly one matching uncertain\ncreate identity exists, the recorded host is proven exited, the endpoint is\nabsent, and any lifecycle marker/readiness leftovers are regular files bound to\nthat same PID and incarnation. The broker removes only those verified\nleftovers, appends terminal `session_closed` evidence, and converts the\nmatching lifecycle receipt to a terminal error. Ambiguous identity, live or\nunverifiable hosts, endpoint presence, malformed leftovers, and mismatches\nrefuse without signalling a process. Supply `--idempotency-key`; the JSON\ninput is read from a `0600` file or stdin when it contains proof material.\n\n## Raw hatch\n\n`gjc sdk session raw <control|query|global>` dispatches exactly one SDK\noperation and returns the broker/host response:\n\n- `raw control <sessionId> --op <operation>` — one control operation with\n `--json-input*`; `--confirm` confirms destructive control operations.\n- `raw query <sessionId> --query <operation>` — one query; `--cursor` passes a\n continuation cursor.\n- `raw global --op <operation>` — one broker global. Lifecycle globals\n (`session.create`, `session.fork`, `session.resume`, `session.close`,\n `session.delete`, `session.reconcile_uncertain`) require `--idempotency-key`.\n\nA separately connected local controller can stop the active turn and its exact\nowned work with an explicitly confirmed operator abort:\n\n```sh\ngjc sdk session raw control <sessionId> \\\n --op turn.abort \\\n --json-input '{\"mode\":\"terminal\",\"scope\":\"owned\",\"operator\":true}' \\\n --idempotency-key '<unique-key>' \\\n --confirm\n```\n\nThe JSON input accepts only operation fields (`mode`, `scope`, `operator`).\n`--confirm` and `--idempotency-key` are CLI authority inputs, not JSON fields.\nThe exact `operator:true` shape is routed through the local Broker, which\nrevalidates the current endpoint identity and injects a process-bound private\ncapability before dispatch. MCP, ACP, notifications, and ordinary SDK endpoint\nrequests cannot mint operator authority: copying `operator:true` and\n`confirm:true` into a public control frame is rejected. Omitting confirmation,\nomitting the key, or supplying `operator:false` fails closed without invoking\nthe terminal-abort surface. Non-operator terminal abort retains its existing\nconnection-ownership semantics.\n\n`session.get_endpoint` is refused unconditionally: endpoint credentials remain\nan SDK-core implementation detail. The raw hatch validates operation names and\nadapter dispositions up front and never renders endpoint-disclosure results.\n\n## Lossless prompt results\n\n`turn.result` with `kind: \"prompt\"` reports `accepted`, `in_flight`,\n`terminal_ok`, or `failed`; only retained-record capacity eviction yields\n`unknown`. `turn.prompt_status` remains a legacy prompt-only alias. A prompt\nthat is active at process restart is finalized from its durable pending outcome\n(or `prompt_failed` when it has none), so it never reports as `unknown` while a\nrecord exists.\n\n`unknown` means uncertainty, never proof of non-execution: do not reuse an\noperation reference as a retry mechanism (`client_ref_conflict` while the\nrecord is retained; after eviction a reused ref may be admitted again with the\nprior outcome unknown). Use one fresh operation reference per logical prompt\nand reconcile with `status`.\n\n## Checkpoint gaps\n\n`tail` reports a `retention_gap` when retained history or the event ring\ndropped entries before the durable checkpoint: the gap carries the missing\nsequence range (`missing.from`/`missing.to`) and a `resync` checkpoint.\n`--strict` turns any gap into `retention_gap` with exit 1; without `--strict`,\ntail continues from the resync position and reports the gap in the envelope.\n\n\n## Migration from the removed daemon session route\n\n`gjc daemon session` is removed and no alias is provided. Migrate:\n\n| Removed route | Replacement |\n| --- | --- |\n| `gjc daemon session list` | `gjc sdk session list` |\n| `gjc daemon session inspect <sessionId>` | `gjc sdk session inspect <sessionId>` |\n| `gjc daemon session send <sessionId> --text <prompt>` | `gjc sdk session send <sessionId> --text <prompt>` |\n| `gjc daemon session tail <sessionId>` | `gjc sdk session tail <sessionId>` |\n| raw control/query dispatch | `gjc sdk session raw control|query|global` |\n\nThe broker-bound surface replaces the daemon-owned routing: sessions are\nresolved through the SDK broker with validated endpoint identity instead of\ndirect discovery-file reads, and output is versioned and credential-free.\n\n## Exit codes and error envelope\n\nVerbs exit `0` on success and write JSON to stdout. Failures write a JSON error\nenvelope to stdout with a non-zero exit: usage errors exit `2`, operational\nfailures (broker unavailable, session unavailable, retention gap, wait\ntimeout) exit `1`. Error details are recursively redacted of secret-shaped\nfields before rendering.\n\n## Scoped search (`gjc sdk search`)\n\n`gjc sdk search [--scope repo|pwd|global] [--limit N] [--cursor <token>] [--json]`\nlists broker-visible sessions inside one exact scope. The default scope is\n`repo`: the identical canonical Git worktree of the invoking directory, never a\npath prefix or subtree. `pwd` matches the exact canonical working directory and\n`global` covers every broker-visible row.\n\nEvery result — table and JSON, populated and empty — carries a scope/status\nenvelope: the requested scope, the canonical resolved scope, a status\n(`populated`, `empty`, `not-in-git-worktree`, `unavailable`), and the\nobservation time. Running `--scope repo` outside a Git worktree is a successful\nempty result (`not-in-git-worktree`, exit 0) and never falls back to `pwd` or\n`global`. Broker unavailability keeps the locally resolved scope, exits\nnon-zero, and stays credential-free. Continuation cursors are frozen: a\ncontinuation that supplies a different scope or anchor fails with\n`scope_cursor_mismatch` instead of re-scoping.\n\nRows are probed only after scope filtering, through broker/router-owned\ncredential-free attachments, yielding `reachable`, `unreachable`, or `stale`.\n\n## Local-only spawn (`gjc sdk spawn`)\n\n`gjc sdk spawn --cwd <dir> --prompt <task> [--model <selector>] [--profile <name>] [--json]`\ncreates one task-seeded background child session through the broker. It is\nlegal only inside a live interactive `gjc --master` session: the command needs\nthe master's transient capability (threaded through the master session\nenvironment) and the broker verifies it against the live effective master host\nbefore any effect. Spawn is prohibited on MCP, ACP, daemon CLI/raw session CLI,\nTelegram, Discord, and Slack surfaces.\n\nEach invocation uses a fresh idempotency identity. One identity produces at\nmost one child substrate and one seed prompt; repeated requests replay the\nstored outcome. A semantically new task requires a new invocation, never a\nretry of an old identity. `spawn_in_progress` and `terminal_uncertain` are\nhonest durable states: inspect with `gjc sdk search` or session status instead\nof retrying blindly.\n\nThe task text and master capability never persist anywhere in broker state —\nno plaintext, hash, or derived verifier appears in the lifecycle ledger, spawn\nauthority journal, receipts, logs, or output. Spawn output renders only safe\nfields: result code, claim id, child session id, substrate kind, and opaque\nseed facts.\n\nClose spawned children through the standard `session.close` path; the broker\ncloses only an exactly re-proven substrate and retains uncertainty on identity\nmismatch. Children orphaned by confirmed master loss are reaped after\n`sdk.masterOrphanGraceMs` (default 120000 ms, bounded 60000..3600000), with the\norphan clock preserved across broker restarts.\n",
|
|
95
|
+
"sdk.md": "# Gajae-Code SDK\n\nFor embedding GJC in-process, see [the embedding SDK guide](./sdk-embedding.md).\nFor a beginner-friendly application development guide (recipes, customization, and surface selection), see [Building applications on the SDK](./sdk-app-guide.md).\n\n<p align=\"center\">\n <img src=\"../assets/telegram-mobile-hero.png\" alt=\"Gajae Code mobile answers for coding agents hero illustration\" width=\"100%\" />\n</p>\n\nThe SDK exposes a generic action/reply protocol without requiring integrations to scrape the terminal. SDK core owns all managed attachment discovery and credential-bearing clients through `SessionRouter`; Telegram, Discord, Slack, ACP, MCP, and CLI adapters receive only capability-scoped operations and never endpoint credentials.\n\n> Status: the Rust core (`crates/gjc-sdk`) provides the session-local wire protocol and endpoint record. TypeScript SDK core provides Broker lifecycle authority and `SessionRouter` attachment authority. Endpoint records and tokens are internal implementation details, not an external attachment surface.\n\n## External attachment policy\n\nExternal and managed integrations attach through SDK-core surfaces only:\n\n- lifecycle mutations use `SessionLifecycleService` and the Broker lifecycle ledger;\n- live session controls use opaque `SessionAttachment` capabilities issued by `SessionRouter`;\n- endpoint URL/token discovery, raw WebSocket relays, and `gjc sdk serve` are not public attachment mechanisms;\n- lifecycle-equivalent per-session controls are prohibited on Telegram, Discord, Slack, ACP, MCP, and daemon CLI adapters.\n\nFor terminal-side session operation, use the broker-bound [SDK session CLI](./sdk-session-cli.md):\n`gjc sdk session list|inspect|send|status|tail` plus the explicit `raw`\n`control|query|global` hatch. The CLI resolves the exact attachment through SDK\ncore and emits credential-free JSON.\n\n## Migration from removed external transports\n\nThe retired `--mode rpc`, `rpc-ui`, `bridge`, and `gjc sdk serve` transports\nhave no replacement wire client. Process-isolated controllers use Coordinator\nMCP, `gjc sdk session`, or a configured managed adapter. In-process applications\nuse the [embedding SDK](./sdk-embedding.md).\n\n## External-agent SDK skills\n\nThe generated `sdk-skills/` bundle provides host-neutral guidance for scripts\nthat invoke the broker-bound session CLI. It is intentionally separate from\nGJC's four internal workflow skills, the coordinator MCP plugin, and the SDK MCP\nadapter. Its TypeScript and Python templates do not discover endpoints or create\ntransport clients.\n\nThe bundle owns exactly six files:\n\n- `manifest.json`\n- `gjc-sdk-discover/SKILL.md`\n- `gjc-sdk-operate/SKILL.md`\n- `gjc-sdk-author/SKILL.md`\n- `gjc-sdk-author/templates/direct-sdk.ts`\n- `gjc-sdk-author/templates/direct-sdk.py`\n\nRegenerate with `bun run generate-sdk-skills`; CI checks byte-for-byte content\nand rejects unexpected files with `bun run check:sdk-skills`.\n\n### Bundle format version\n\n`manifest.json` is the versioned root of the on-disk bundle contract. It\nidentifies `formatVersion` (currently `1`) and the exact relative file closure\nthat regeneration owns. Consumers must treat a bundle whose manifest is missing,\nmalformed, or declares an unsupported version as unreadable and fail closed. The\nskill prompts are authored as static Markdown sources under\n`scripts/gjc-sdk-skills/prompts/`; the generator copies them verbatim and\n`check:sdk-skills` proves the committed bundle matches the generated artifacts.\n\n### Trust boundary\n\nThe templates invoke only the broker-bound CLI. `SessionRouter` keeps endpoint\nresolution, credentials, SDK clients, replay, reconnect, and rotation inside SDK\ncore. The templates' fixed allowlists and nonce-bound approval are trusted-local\nprocedural safeguards, not capability isolation; lifecycle and attachment\nauthority remain enforced by Broker and Router.\n\nNo renderer-grade cross-process event stream is exposed to external scripts.\nUse managed adapters or Coordinator MCP for event-driven orchestration; see the\n[RPC-to-SDK v3 parity audit](./sdk-rpc-parity-audit.md) for remaining gaps.\n\n## Architecture\n\n```\nBroker lifecycle → Session runtime endpoint → SessionRouter → opaque adapter capability\n```\n\nThe Broker is the sole lifecycle executor and durable terminal authority. `SessionRouter` is the sole credential-bearing external attachment manager. Provider supervisors own only provider transport and presentation state.\n\n- **One endpoint per top-level session.** Each top-level session runs its own loopback WebSocket server. Subagents do not host endpoints. The Broker index is the authoritative live-session catalog, and `SessionRouter` multiplexes managed provider attachments across indexed sessions.\n- **Hosted by default.** SDK hosting is independent of notification configuration. Set `GJC_SDK_DISABLE=1` to opt out of hosting for a top-level session.\n- **Notification delivery is optional.** Configure and enable a managed notification adapter only when remote delivery is needed; the SDK endpoint remains available without one.\n- **Managed integrations use opaque attachments.** Telegram, Discord, Slack, ACP, MCP, and CLI adapters compose SDK-core services; they do not discover endpoint files or receive URL/token credentials.\n- **Zero wire-protocol change.** New transports do not require changes to `crates/gjc-sdk` or the JSON protocol.\n- **tmux-agnostic.** The endpoint behaves identically with or without tmux.\n\n## Internal endpoint publication\n\nA running session publishes an implementation-private credential record for Broker resolution. `SessionRouter` is the sole consumer of the resolved URL/token pair and the sole owner of per-session SDK clients, replay, reconnect, rotation, prepared activation, and opaque attachment capabilities.\n\nThe record path, schema, credential transport, and handshake are not public client contracts. ACP, MCP, Coordinator, CLI, provider daemons, extensions, and integrations must use Router-issued attachments or Broker lifecycle services; they must not scan state roots, parse discovery files, retain endpoint credentials, or open raw per-session WebSockets. Broker and Router validate process identity, endpoint generation, incarnation, and file integrity before attachment, and fail closed on stale or uncertain state.\n\n### Internal broker launch isolation\n\nWhen the SDK starts its default internal broker or session host from the published TypeScript source, GJC uses a fixed Bun launch policy: `--no-env-file`, a product-owned empty `bunfig.toml`, absolute product entrypoint paths, and no inherited `BUN_OPTIONS` or mutable compiled-mode markers. The broker bootstraps from the product SDK directory rather than the caller project; a session host still runs with the lifecycle-authorized workspace as its process cwd.\n\nThis boundary prevents a child from newly loading caller-cwd or user-global Bun preload/dotenv policy. It cannot determine how a value already present in the parent environment was originally loaded, so ordinary provider/GJC environment values remain inherited. Default internal children, including compiled self-spawns, remove inherited `BUN_OPTIONS` so parent eval/test/inspect/debug/runtime options cannot be replayed into a detached child. Compiled binaries otherwise retain their existing self-spawn command contract, corroborated by a dedicated embedded marker and exact anchored Bun virtual-filesystem identity. The explicit `GJC_SDK_SESSION_COMMAND` session-host override remains a trusted legacy operator boundary and is not parsed as a shell-safe general command API. There is no broker-command override.\n\nBroker and per-session discovery tokens remain in their authoritative private discovery files for SDK-core resolution. Launch errors, logs, and diagnostics redact those tokens and never include the child environment or isolation configuration contents.\n\n## Internal protocol\n\nThe following frame shapes are SDK-core implementation details for maintainers.\nExternal integrations must not construct these frames, attach to session\ntransports, or receive endpoint credentials; use a Router-issued attachment, the\nbroker-bound CLI, or Coordinator MCP instead.\n\n### Server → client\n\n`action_needed` — something needs attention:\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"act_9e31\", \"kind\": \"ask\",\n \"sessionId\": \"sess-1\", \"workflowGateId\": \"wg_run_stage_1\",\n \"question\": \"Proceed?\", \"options\": [\"Yes\", \"No\"], \"recommendedIndex\": 1 }\n```\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"act_a42f\", \"kind\": \"ask\",\n \"sessionId\": \"sess-1\", \"question\": \"Choose a target\", \"options\": [\"A\", \"B\"] }\n```\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"idle-sess-1-7\", \"kind\": \"idle\",\n \"sessionId\": \"sess-1\", \"summary\": \"finished refactor; awaiting next step\" }\n```\n\n- `id` is an opaque, transient presentation/action ID. It is the **only** authority accepted by generic `reply.id` through the current Router-issued attachment. It is not a durable workflow ID.\n- `workflowGateId?: string` is optional, additive SDK v3 correlation metadata, present only for the active presentation of a durable workflow gate. When present, it equals that gate's Q12 `gate_id`. Its public correlation key is `(sessionId, workflowGateId)` on the current Router-issued attachment; it never authorizes generic `reply`.\n- `kind: \"ask\"` is answerable in interactive/TUI and SDK workflow-gate sessions. `kind: \"idle\"` is notify-only and ephemeral (not replayed to attachments that start later). Ordinary asks and idle frames omit `workflowGateId`.\n- `recommendedIndex?: number` is optional, zero-based display metadata for `options`. Clients must validate that it is an in-range integer and ignore malformed values. Raw option labels and reply indices remain authoritative; never decorate submitted answers or infer a recommendation from position. The additive field is wire-compatible, but Rust consumers constructing the public `ActionNeeded` struct by literal must provide `recommended_index: None` when no recommendation exists.\n- This corrects the pre-v3 documentation invariant that `action_needed.id == gate_id`: they are deliberately different values. Clients must not preserve that invariant, infer a relationship from question/options/order, or retain private route, claim, receipt, epoch, token, or endpoint-generation maps.\n\n`action_resolved` — a pending action is now terminal and **non-repliable**:\n\n```json\n{ \"type\": \"action_resolved\", \"id\": \"act_9e31\", \"resolvedBy\": \"local\" }\n```\n\n`resolvedBy` is `local` (a local/direct control retired the presentation), `client` (a remote generic reply won), or `timeout`.\n\n`reply_rejected` — sent only to the client whose reply failed:\n\n```json\n{ \"type\": \"reply_rejected\", \"id\": \"act_9e31\", \"reason\": \"already_answered\" }\n```\n\nReasons: `already_answered`, `unknown_action`, `invalid_answer`,\n`resolver_unavailable`, `idempotency_conflict`, `unauthorized`.\n\n`bash_folded` — a foreground bash wait left the foreground and became a background job. Emitted once per fold into the replayable session event ring (so late-connecting clients receive it on `event_replay`), with `generic_safe` adapter disposition:\n\n```json\n{ \"type\": \"bash_folded\", \"sessionId\": \"sess-1\", \"jobId\": \"bg_3\", \"generation\": \"bg_3:1\", \"reason\": \"steer\" }\n```\n\n- `reason` is `steer` (a user steer arrived while the command had run past the 2 s grace window), `chord` (the `app.tool.backgroundFold` keybinding), `timer` (`bash.autoBackground`), or `sdk_control` (the `bash.background` control).\n- A replayed frame may describe a job that has since finished. `runtime.jobs.list` is the source of truth: each running/recent job carries `metadata.backgrounded` and, whenever it is `true` because of a fold, `metadata.foldReason` with the same values. The `bash.background` control (C52) returns `{ backgrounded: true, jobId }` on a fresh fold, `already_backgrounded` when no wait is foldable but a running job of this session was already folded (by steer, chord, timer, or a prior control call), `no_active_bash` when nothing is running or foldable, and `not_foldable` when a live wait refused the fold.\n- A `turn.steer` that folds a running bash behaves exactly like a TUI steer: the steer is consumed by the same turn at the tool boundary immediately after the fold result; `turn.steer_status` is unchanged.\n\nThe frames above are the internal transport contract implemented by SDK-core attachments. Managed adapters may receive optional server → client frames they can render or ignore: `identity_header` (one-time per-session repo/branch/machine header; Telegram topic-capable sessions additionally carry `telegramTopicsEnabled`), `context_update` (last message, task, goal, token usage, model, diff), `turn_stream` (live/finalized turn output), `image_attachment` (agent-produced images), `activity` (busy/idle, drives the typing indicator), `inbound_ack` (delivery state of an injected user message), `session_closed` (endpoint teardown; threaded adapters may delete/archive the remote conversation), `config_update` (current verbosity/redact), `hello` (server capability/version), and `pong`.\n\n### Internal inbound frames\n\nSDK core creates inbound frames only after Router attachment checks. External\nintegrations must not construct or persist them. Managed adapters use their\nopaque `SessionAttachment`, and process-isolated scripts use `gjc sdk session`;\nneither path receives transport credentials.\n\n## Model catalog query (Q10)\n\nThe SDK exposes the model catalog through the paged Q10 registry query. `Q10`,\n`models.list/current`, `models.list`, and `models.current` are exact aliases:\neach returns the same paged registry array, not a current-model singleton or a\nfiltered list. Continue using the returned cursor until `page.complete` is\ntrue.\n\nEach row preserves the five legacy fields (`provider`, `id`, `name`,\n`contextWindow`, and `maxTokens`) and additively includes `reasoning`,\n`thinking`, and `current`. `currentThinkingLevel` appears only on the current\nrow when the live session has a thinking level. The exported DTO types are\n`Q10Model`, `Q10ThinkingCapabilities`, `Q10ThinkingEffort`,\n`Q10SettableThinkingLevel`, `Q10CurrentThinkingLevel`, and\n`Q10ThinkingMode`, all from `@gajae-code/coding-agent/sdk`; there is no public\n`/sdk/models` subpath.\n\n```json\n{\n \"provider\": \"runtime-provider\",\n \"id\": \"reasoning-model\",\n \"name\": \"Reasoning Model\",\n \"contextWindow\": 128000,\n \"maxTokens\": 8192,\n \"reasoning\": true,\n \"thinking\": {\n \"validLevels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"],\n \"minLevel\": \"minimal\",\n \"maxLevel\": \"high\",\n \"mode\": \"effort\",\n \"defaultLevel\": \"low\"\n },\n \"current\": true,\n \"currentThinkingLevel\": \"high\"\n}\n```\n\n`thinking.validLevels` is always present and starts with `\"off\"`; it is the\ncanonical menu for `model.set` and never contains `\"inherit\"`. For a\nnon-reasoning model it is exactly `[\"off\"]`. Successful reasoning rows always\ninclude `minLevel`, `maxLevel`, and `mode`; only `defaultLevel` and raw `levels`\nare optional. Raw `levels` deliberately keeps its descriptor order and\nduplicates, while `validLevels` is the canonical, deduplicated menu clients\nshould render. `\"inherit\"` is a current-state readback value only and is rejected\nas a `model.set` input.\n\nMalformed reasoning descriptors are not client-recoverable catalog data. The\nquery returns the SDK's safe `internal` error rather than exposing a partially\nformed row or descriptor details.\n### Model profiles as synthetic models (`gajae-code/<profile>`)\n\nThe Q10 catalog also exposes model profiles as logical synthetic models under\nthe reserved provider namespace `gajae-code`, e.g. `gajae-code/codex-eco`.\nThese rows let clients (such as ACP model pickers) offer presets like ordinary\nmodels without provider-specific metadata:\n\n```json\n{\n \"provider\": \"gajae-code\",\n \"id\": \"codex-eco\",\n \"name\": \"Codex Eco\",\n \"contextWindow\": 222222,\n \"maxTokens\": 8888,\n \"reasoning\": false,\n \"thinking\": { \"validLevels\": [\"off\"] },\n \"current\": false\n}\n```\n\n- `gajae-code/<profile>` is a **logical namespace, not a callable provider**. No\n API transport, credentials, or streaming route is registered for it; send the\n value back through the generic `model.set` control (or the ACP `Model`\n select) to activate the profile.\n- Synthetic rows are **availability-filtered**: only profiles whose required and\n alternative providers have usable stored credentials are listed. The profile\n id suffix is parsed losslessly after the first namespace slash, so profile ids\n containing additional slashes or punctuation round-trip exactly.\n- `contextWindow`/`maxTokens` mirror the profile's resolvable default model when\n available and otherwise fall back to the shared unknown-model constants\n (222222 / 8888); the profile's real default model remains authoritative.\n- Synthetic rows are non-reasoning with `validLevels: [\"off\"]`: a `model.set`\n on a synthetic id with any thinking level other than `off` is rejected with\n `invalid_input`, and only an absent or `off` level is forwarded as a session\n override.\n- **Current-state semantics:** while a profile is active for the session, exactly\n the synthetic row carries `current: true` with `currentThinkingLevel:\n \"inherit\"`, and the underlying concrete row is not marked current. A persisted\n `modelProfile.default` alone (without an in-session active marker) never\n creates a synthetic current row. Selecting a concrete `provider/model` clears\n the active marker and restores concrete current semantics.\n- **Selecting a synthetic profile is session-scoped.** `model.set` with\n `gajae-code/<profile>` activates the full profile in the live session without\n writing `modelProfile.default`, `modelRoles`, or\n `task.agentModelOverrides`. Persisting a profile remains an explicit TUI\n choice (`/model` → default), mirroring `gjc --mpreset <profile> --default`.\n Unknown or ambiguous synthetic ids fail with `invalid_input`; missing profile\n credentials fail with the existing authentication-required error.\n- `gajae-code` is **reserved**: a user-defined `models.yml` provider of the same\n name disables the synthetic facade (rows are omitted and synthetic selection\n is rejected) rather than being silently shadowed. Q27 (`models.profiles.list`)\n remains the full profile catalog with explicit `available` status; Q10 is the\n availability-aware facade for client selection.\n`config.patch` mutations are serialized through the same session admission\nboundary as profile activation and default-model selection, so a patch racing\na synthetic `gajae-code/*` selection (or another patch) is applied in a\ndeterministic order and is never lost or clobbered by an activation rollback.\nThe cost is the same as `model.set`: an external `config.patch` queues behind\nany in-flight prompt admission rather than applying mid-turn.\n\n## Prompt acceptance, termination, and reconciliation (Q26)\n\n`runtime.capabilities.promptTerminalOutcomeVersion` is `1` when this contract is available. Its normalized TypeScript terminal outcome is:\n\n```ts\ntype SdkPromptTerminalOutcome =\n\t| {\n\t\t\tkind: \"stopped\";\n\t\t\treason: \"end_turn\" | \"max_tokens\" | \"max_turn_requests\" | \"refusal\" | \"cancelled\";\n\t\t\tprovenance: \"agent\" | \"client_cancel\";\n\t }\n\t| {\n\t\t\tkind: \"failed\";\n\t\t\tcode: \"prompt_failed\" | \"prompt_deadline_exceeded\";\n\t\t\tmessage: string;\n\t\t\tprovenance: \"agent_failed\" | \"deadline\";\n\t };\n```\n\n`turn.prompt` returns `{ accepted: true, commandId, turnId, clientRef? }` only after\nits asynchronous preflight accepts the prompt. That receipt is a durable,\n**non-terminal pending claim**, not a process-durable terminal result. The SDK\nlater finalizes that claim with exactly one `SdkPromptTerminalOutcome`; cleanup\nmay follow only after the claim is durable.\n\nThe authoritative public reconciliation query is `Q26` / `turn.result`, scoped\nto the same live session runtime. Callers supply `kind: \"prompt\"`; its `outcome`\nfield is exposed only after finalization. A pending claim is never represented\nor exposed as a terminal outcome. `turn.prompt_status` remains a legacy\nprompt-only alias that injects the same `kind`.\n\nEvery prompt and skill status response includes `receiptState`: active records are `absent`; terminal records are `present`, `missing`, or `unknown`; unknown lookup is `unknown`. Ordinary success requires `status: \"terminal_ok\"`, `receiptState: \"present\"`, and readable non-empty text or an artifact path. A failed execution may retain a partial `present` receipt. Legacy version-1 reconciliation records without this additive field remain readable and project `unknown` rather than optimistic success.\n\nCallers that must recover from a lost acknowledgement should assign one fresh\n`clientRef` (a trimmed, non-empty string of at most 128 characters) to each\nlogical prompt, then reconcile through the broker-bound CLI:\n\n```sh\ngjc sdk session raw query <sessionId> --query turn.result \\\n --json-input '{\"kind\":\"prompt\",\"clientRef\":\"request-018f\"}'\n```\n\nThe alternate selector uses the same `kind` with\n`{\"kind\":\"prompt\",\"commandId\":\"command-id\",\"turnId\":\"turn-id\"}` as its JSON input.\n\nThe result status is `accepted`, `in_flight`, `terminal_ok`, `failed`, or\n`unknown`. Known records include `acceptedAt`; in-flight and terminal records add\n`startedAt` and/or `terminalAt`; finalized records include `outcome`; failed records\nalso include a bounded sanitized `error.code` and `error.message`. Cursors, partial\ngenerated-ID pairs, mixed selectors, and extra selector fields are rejected.\n\nCorrelated `agent_end` and `agent_failed` frames carry the same finalized\n`outcome`. Clients must correlate those frames and Q26 by the prompt identifiers,\nnot infer terminality from stream activity or an earlier pending claim.\n\nReconciliation state survives client disconnect/reconnect. With the session-private durable store (`.sdk-reconciliation/`), accepted and terminal prompt records also survive **GJC session-process restart** for the same session identity within capacity, subject to crash-consistent fsync. A non-terminal prompt record at restart finalizes its pending outcome and receipt state. A stopped prompt without receipt evidence becomes `terminal_ok + missing`; failed prompt or skill settlement without body evidence becomes `unknown`. Eviction or absence still returns honest `unknown`; that means the prior outcome is unknowable, not that execution did not occur. Active records are capped at 128 per kind and are never aged into terminal. Terminal records are capped at 256 per kind and evicted oldest-terminal first, with no age-based eviction. Reconciliation stores no prompt, transcript, credential, or provider-response body.\n\n`turn.prompt` remains ordered and non-idempotent. Its envelope `idempotencyKey`\ndoes not replay a response or produce `idempotency_conflict`. A retained duplicate\n`clientRef` fails before execution with `client_ref_conflict`, but callers must not\nreuse a `clientRef` as a retry mechanism: after eviction the same value can identify\na new prompt while the old outcome remains unknown.\n\n`turn.abort` returns a typed disposition. A caller that does not own the target\nreceives `resource_gone`; it must not treat that result as cancellation of another\nprompt.\n\n`sdk.promptDeadlineMs` defaults to `1_800_000`. It accepts only safe integers in\n`[60_000, 86_400_000]`; there is no disable value. The SDK snapshots the setting\nwhen the prompt is durably accepted as the initial inactivity lease. Fresh\n**attributable** progress for the exact accepted `commandId`/`turnId` — `tool_execution_start` /\n`tool_execution_end` observed at the prompt/agent runtime boundary — renews the deadline to\n`lastProgressAt + sdk.promptDeadlineMs`, bounded by the hard maximum `sdk.promptMaxRuntimeMs`\n(default `21_600_000`, same `60_000–86_400_000` range). Only tool-execution boundaries for the\naccepted turn count; heartbeats, streaming text/thinking deltas, retries, other turns/sessions, and\nunrelated session noise do not renew the lease, and out-of-order delivery never shortens it. The\nhard maximum is never unbounded: every renewal is capped at `acceptedAt + sdk.promptMaxRuntimeMs` so a\nwedged or continuously noisy prompt still reaches a deterministic terminal outcome. Terminalization then has a fixed `10_000` ms\ngrace period, which is not configurable. A controlled terminal failure reaches ACP\nas JSON-RPC `-32603` with `data.code` of `prompt_failed` or\n`prompt_deadline_exceeded`.\n\n## Skill invoke reconciliation\n\n`skill.invoke` accepts optional `clientRef` and returns an early accepted receipt\n`{ accepted: true, commandId, turnId, clientRef?, name, path, lineCount?, args? }` after\ndurable/preflight accept (SDK control path), not after skill completion. Query prior\nstatus with `Q26` / `turn.result` and `kind: \"skill\"`. `skill.invoke_status`\nremains a legacy skill-only alias that injects the same `kind`. Kind-scoped indexes\nmean prompt and skill `clientRef` values never collide. Skill records use the same\ncapacity/retention limits, but an active skill record at restart settles with\n`error.code = process_restart`.\n\n## Correlated steer acknowledgement (Q30)\n\n`turn.steer` accepts an optional `clientRef` (trimmed, non-empty, at most 128 characters). When present, GJC hashes the exact validated steer text with SHA-256 and durably reserves `dispatching` before queueing. The result contains `sessionId`, `clientRef`, `status: accepted | rejected | uncertain`, known `acceptedAt` or `terminalAt`, and bounded error metadata; it never echoes text.\n\nReplay the same `clientRef` with the same text to recover the retained result without dispatching again. Reuse with different text returns `client_ref_conflict`. A live `dispatching` record and a restart during dispatch both project `uncertain`; GJC never automatically redispatches an ordered control whose first effect is unknowable. Query the same session with:\n\n```json\n{ \"type\": \"query_request\", \"query\": \"turn.steer_status\", \"input\": { \"clientRef\": \"steer-018f\" } }\n```\n\nQ31 returns `accepted`, `rejected`, `uncertain`, or `unknown` and never dispatches work. Settled steer records share the 256-record oldest-terminal-first capacity bound with no age-based eviction; live dispatching records are not terminal-evicted. Existing uncorrelated `turn.steer` calls retain their legacy non-idempotent behavior. Version-1 reconciliation remains additive, and only digest plus bounded metadata is stored—never steer text.\n\n## Model profile discovery and validation (Q27)\n\n`Q27` / `models.profiles.list` pages the effective model-profile catalog owned by\nthe attached session. Rows are sorted by exact ID and contain only:\n\n```json\n{ \"id\": \"codex-medium\", \"displayName\": \"codex-medium\", \"source\": \"builtin\" }\n```\n\n`source` is `builtin` or `configured`. Profiles from `<agentDir>/models.yml`\noverride built-ins with the same exact ID, including their display label. Profile\nIDs are not trimmed, case-folded, sanitized, or restricted to safe-token names;\ndiscover the exact ID and send it unchanged. The retired `codex-standard` alias is\nfallback-only and never shadows a configured profile with that exact ID.\n\nQ27 uses retained-revision, connection-bound pagination. Continue an issued cursor\nto finish its stable snapshot; a fresh cursorless query observes the current\nregistry. The query accepts no root, path, or selector input. An invalid or\nunreadable `models.yml` fails closed with `model_profile_registry_error` rather\nthan returning a plausible built-ins-only catalog.\n\nBroker `session.create`, `session.fork`, and `session.resume` validate `modelPreset`\nbefore spawning against the same `<broker.settings.agentDir>/models.yml` authority\nthat the child receives through `GJC_AGENT_DIR` / `GJC_CODING_AGENT_DIR`. Unknown\nIDs return `unknown_model_profile`. Both typed errors include bounded `details`\nwith `requestedProfile` where applicable, whole exact `availableProfiles` entries\nthat fit the detail budget, and `discoveryQuery: \"models.profiles.list\"`. The\ndiscovery pointer is authoritative when the bounded error cannot include every ID.\n\nThe same lifecycle operations accept an optional `modelId`: an explicit\n`provider/model` pin with `gjc --model` grammar (#4707). Coordinators resolve it\nagainst the full model registry (the CLI `--model` surface, not the\nauthenticated-only subset) before sending the create request, so unknown ids are\nrejected before any session exists. The broker guards only its shape; the child\napplies it exactly like a CLI `--model` selection, which also means an explicit\n`modelId` wins over `modelPreset` when both are supplied — the same precedence as\n`gjc --mpreset <profile> --model <model>`. The full effective order is:\n\n```\nmodelId pin > modelPreset > configured modelProfile.default > role/resume/default\n```\n\nThe pin is a guarantee, not a preference. The coordinator validates against its\nown registry and the child owns the registry that actually serves requests, so\nthe two can drift (a model removed, a provider disabled, an extension that\nfailed to register). On drift the child fails session construction before\nreadiness and before any profile application, disposes the partial session, and\nreports an error naming the pinned selector — it never publishes success on a\nsubstituted model. Coordinator validation refreshes the registry per request\n(offline, from the on-disk discovery cache) so ids added or removed after an\nearlier pin are judged against current contents.\n\n### Active provider query (Q29)\n\n`Q29` / `providers.list/active` pages the providers currently eligible for model\nselection through the same authenticated, retained-snapshot envelope as Q10. Each\nrow is the non-secret DTO `{ provider, connectionKind }`, where `connectionKind`\nis `credential` or `credentialless`.\n\nProvider IDs are returned exactly as they appear in Q10 `model.provider`: existing\nmixed-case, spaced, punctuated, and long custom IDs are preserved without aliases\nor normalization. Rows are deduplicated and ordered by UTF-8 provider bytes.\nJoin Q29 to Q10 by exact provider ID; Q10 remains the full configured catalog.\n\nA credentialed discovery-only provider appears only after fresh discovery proves\nthe exact model is usable. Static configured models can appear without a network\nprobe. The query never invokes a model, refreshes credentials, probes a remote\naccount, or exposes credentials, account metadata, paths, or provider responses.\n\nResolver failures are atomic and return\n`{ \"code\": \"internal\", \"message\": \"Unable to resolve active providers.\" }`.\nThey omit a page and restart metadata. An expired continuation follows the shared\ncursor contract and returns `error.code: \"cursor_expired\"` with\n`error.restartQuery: true`. Malformed cursor strings return `invalid_cursor`;\ncross-query or selector mismatches return `invalid_input`.\n\n## Answer semantics\n\nA remote reply answers a pending ask in every session state:\n\n- **Interactive / TUI mode:** the ask tool races the local selector against the\n remote reply (first valid answer wins). A client submits generic `reply` using\n the active presentation `id`; a local answer emits `action_resolved`\n (`resolvedBy: \"local\"`) and that presentation becomes non-repliable.\n- **SDK workflow gate:** generic `reply` still uses the active presentation\n `id`, never `workflowGateId`. The resolved gate drives the session the same\n way a local answer would.\n\nA session has at most one active answerable presentation. Interactive asks and durable workflow gates are serialized; further Q12 gates wait in a durable queue. A same-server reconnect replays the active `action_needed` with the same presentation ID. After a process restart, previously pending or accepted-but-unadvanced records are quarantined diagnostics and a reconstructed workflow remints fresh durable gate and presentation IDs. Terminal, stale, and reissued action IDs never regain authority.\n\nGeneric and direct controls may race. Once the native generic claim is acquired, it wins; a direct control that atomically retires the exact unclaimed active presentation first wins instead. Losing direct controls fail without advancing the gate, and losing generic replies are stale/non-repliable. Clients must not retry by matching text, durable IDs, or presentation history; they must fail closed rather than guess when session or action identity is unsafe or ambiguous.\n\n### Durable workflow controls and Q12\n\n`workflow.gate_answer` and `workflow.plan_approve` operate on the durable Q12\n`gate_id`, not `action_needed.id`. Managed adapters bind `expectedSessionId`\nfrom their current Router-issued attachment. Process-isolated operators use the\nraw-control flow documented in the [SDK session CLI guide](./sdk-session-cli.md); they do not construct protocol frames.\n\n`expectedSessionId` omission remains accepted and audited for the entire SDK v3 line so deployed v3 control clients continue to work; new clients must send it now. It cannot become mandatory, or be removed from the controls, before SDK v4 and at least one full published deprecation release/window with deployed-client notice. A supplied session mismatch is rejected before the gate resolver runs. Neither control accepts a presentation ID, remaps an old ID to a reminted gate, or uses heuristic matching.\n\nQ12 (`workflow.gates.list`) exposes durable query records and additive SDK v3 diagnostics. A pending record preserves its workflow fields including `gate_id` and adds `id: \"pending:<gate_id>\"` and `tag: \"pending\"`. A restart quarantine diagnostic uses `id: \"diagnostic:<gate_id>\"`, `tag: \"quarantined\"`, and optional `lifecycle` containing `state: \"quarantined\"`, its restart reason, `quarantinedAt`, and an optional `supersededByGateId` after a remint. Diagnostics are query-only: they cannot be routed, answered, or promoted. Treat Q12 as the durable status surface, not as generic-reply authority.\n\n### Coordinator MCP question pull loop\n\nThe Coordinator MCP bridge is a separate, public-safe pull surface for external coordinators. `gjc_coordinator_list_questions` requires `session_id` and reconciles pending `workflow.gates.list` rows on every call, returning bounded public `questions`, `diagnostics`, and `reconciliation`. It accepts `status: \"pending\"`; `status: \"open\"` remains a compatibility alias. Multiple pending rows can be returned. A pending row carries its safe question shape, public option ids, and `answer_binding`, never raw/private gate payloads or values.\n\n`gjc_coordinator_submit_question_answer` requires `session_id`, `turn_id`, `question_id`, `answer_binding`, `answer`, `idempotency_key`, and `allow_mutation: true`. It re-lists/revalidates after restart and resolves through `workflow.gate_answer`, not generic `ask.answer`. An incomplete reconciliation returns `terminal_uncertain`; stale, terminal, missing, or ownership-mismatched rows cannot be answered. Re-list after restart rather than retaining old identifiers. An identical retry with the same idempotency key replays the accepted result; conflicting reuse returns `idempotency_conflict`.\n\nThis contract does not change #2549/#2551 or unattended plain-CLI behavior.\n\n### Rust and N-API compatibility\n\nThe Rust `ActionNeeded`, `ServerMessage`, and `register_ask` APIs remain\nlegacy-compatible and uncorrelated. Correlation is available through additive\nRust workflow-frame decoding/current-reader APIs and the workflow registration\npath; consumers that need correlation must opt in explicitly. N-API likewise\nretains `registerAsk`, and adds `registerWorkflowGateAsk` for a correlated wire\nframe plus `registerArbitratedAsk` and `retireIfUnclaimed` for in-process\npresentation arbitration. The arbitration lease and all claim/receipt/epoch\nstate remain private: these APIs do not create a public authority value.\n\n### Runtime and native addon release pairing\n\nThe `@gajae-code/coding-agent` runtime and `@gajae-code/natives` native addon ship from the same source release at exact matching package versions. The native loader requires the matching version sentinel; mixed native/runtime versions are unsupported and must not claim SDK compatibility.\n\n## Minimal provider adapter example\n\nProvider integrations compose SDK core's `SessionRouter`; they never read endpoint files or retain URL/token credentials:\n\n```js\nimport { router } from \"@gajae-code/coding-agent/sdk\";\n\nconst sessionRouter = new router.SessionRouter({\n agentDir,\n deps: {\n onAttachment: (attachment) => provider.bind(attachment.sessionId, attachment),\n onFrame: (attachment, frame) => provider.render(attachment.sessionId, frame.body),\n onSessionRemoved: (attachment) => provider.unbind(attachment.sessionId),\n },\n});\n\nawait sessionRouter.start();\nconst attachment = sessionRouter.attachment(sessionId);\nif (!attachment) throw new Error(\"session attachment unavailable\");\nawait attachment.send({ type: \"reply\", id: actionId, answer });\n```\n\nTelegram, Discord, Slack, and third-party adapters own only their provider transport and presentation state. `SessionRouter` performs exact endpoint resolution, credential custody, replay, reconnect, rotation, and dispatch-time stale-lease rejection.\n\n### Exact generation reconciliation\n\nManaged consumers that close or delete a session can reconcile one previously\npersisted attachment generation without retaining endpoint credentials:\n\n```ts\nconst proof = await sessionRouter.generationStatus(sessionId, endpointGeneration);\n\nswitch (proof.status) {\n case \"current\":\n // This exact generation is the current live indexed authority.\n break;\n case \"retired\":\n // A retained host-unregister, close, or delete event positively retired it.\n break;\n case \"replaced\":\n // A different live generation is current; use proof.currentGeneration.\n break;\n case \"unknown\":\n // Do not infer retirement or retry a possibly-applied lifecycle mutation.\n break;\n}\n```\n\n`generationStatus(sessionId, endpointGeneration)` is credential-free and may be\ncalled after `SessionLifecycleService` returns from `session.close` or\n`session.delete`, and after `SessionRouter.stop()`. Its evidence contains only\n`source: \"session_index\"`, a coherent `observedIndexSeq`, the successful\nstate's `evidenceIndexSeq`, and, for positive retirement, the safe terminal\nevent kind. It never exposes endpoint URL/token data, process IDs, locators,\nprivate Broker responses, or lifecycle cleanup payloads.\n\nThe result is an observation of one locked index reconciliation cut:\n\n- `current` requires the queried generation to be the current live, unambiguous\n indexed authority.\n- `retired` requires a retained exact-generation `host_unregistered`,\n `session_closed`, or `session_deleted` event. Missing attachment, missing\n endpoint publication, Router shutdown, or a dead/unreachable host never imply\n retirement.\n- `replaced` requires both prior observation of the queried generation and a\n strictly greater current live generation. Same-generation reconnect remains\n current; reuse, regression, or safe-integer wrap is classified as unknown\n rather than manufacturing a successor relationship.\n- `unknown` is fail-closed. Reasons distinguish invalid input, unavailable or\n incomplete index reconciliation, an unobserved session/generation, ambiguous\n authority, and detected generation reuse. Consumers must preserve uncertain\n lifecycle state rather than treating `unknown` as retired. Expired positive\n evidence is reported specifically as `proof_expired`.\n\nProof survives Broker, Router, and consumer process restart because it is read\nfrom the durable session index, not Router attachment memory. Positive terminal\nproof has the session-index `maxAgeMs` lifetime (30 days by default), even when a\ndelete tombstone is retained longer for audit. Row-bound compaction may evict a\nwhole session sooner. After proof expiry, compaction eviction, or loss of a\ncomplete index, `generationStatus` returns `unknown`, never a synthetic\nretirement proof. This bounds the public proof surface independently of durable\nBroker audit retention.\n\n## Fallback chains\n\nModel-role selectors may be ordered fallback chains; see [Fallback chains](./models.md#fallback-chains) for configuration and retry-budget details. Resolution-time skips do not consume attempts. When a request-time retry advances to another eligible entry, the selected default fallback remains sticky for later prompts in that session until an explicit model selection or a chain reset changes it.\n\n`model_fallback_switched { eventId, from, to, reason, role, scope, activeIndex, chainLength, attemptsUsed }` is the canonical session lifecycle event for every real fallback-model switch. It replaces the legacy `retry_fallback_applied` / `retry_fallback_succeeded` event names. Embedding clients can subscribe to this in-process session event; managed adapters receive only the status projections their SDK-core integration supports.\n\n\n## Managed session-directory adapter guidance\n\nSDK adapters that need to inspect saved sessions must import only the supported public surface from `@gajae-code/coding-agent/sdk`:\n\n```ts\nimport {\n SESSION_DIRECTORY_API_VERSION,\n listManagedSessionCandidates,\n resolveManagedSessionScope,\n} from \"@gajae-code/coding-agent/sdk\";\n\nif (SESSION_DIRECTORY_API_VERSION !== 1) throw new Error(\"Unsupported session-directory API\");\nconst resolved = await resolveManagedSessionScope({ cwd: process.cwd() });\nif (resolved.kind === \"resolved\") {\n const listing = await listManagedSessionCandidates({ scope: resolved.scope });\n // Consume only listing.kind === \"complete\" and its owned candidates.\n}\n```\n\nThis is a readonly resolver/listing contract. Do not import `@gajae-code/coding-agent/session/internal/*`, derive `v2-…` names, write bindings, or implement migration/cleanup in an adapter; private internal subpaths are intentionally unavailable from the packaged module. Treat `network_unsupported`, binding/security errors, incomplete listings, invalid candidates, and foreign candidates as non-authoritative results rather than retrying with a guessed path.\n\nThe resolver uses canonical native identity: supported POSIX and Windows local aliases can designate one scope, while UNC/network workspaces are unsupported. Scope digests are collision-resistant identifiers, not injective aliases, credentials, or authentication. The owner-only checks protect managed local storage paths but do not authenticate an adapter or make hostile concurrent filesystem races safe. Adapters that need mutations must use the higher-level lifecycle/session APIs rather than the readonly directory API.\n## Managed notification adapters\n\nGJC ships managed SDK adapters for Telegram, Discord, and Slack. `SessionRouter` resolves one session-owned endpoint per attachment and keeps every endpoint credential inside SDK core. Provider daemons receive only opaque attachment capabilities; they neither change the wire protocol nor expose a remote shell.\n\nThe recommended interactive path is `/settings` → **Notifications**. It owns\nsetup, health, test, recovery, reconnect, local enablement, and Telegram\nremoval without exposing stored credentials.\n`gjc notify setup` remains the authoritative CLI fallback for headless and\nautomated environments.\n\nNotification credentials and `notifications.*` settings are global-only.\nProject notification keys are\nignored and runtime notification overrides are rejected. Telegram pairing\nrevalidates the complete bot-token/chat identity immediately before polling and\nagain before activation. A foreign or unknown owner is never killed, reloaded, or taken over;\nsetup fails closed without saving or exposing the raw token.\n\nConfiguration completeness, provider-local quarantine, durable desired intent, effective enablement, runtime readiness, and delivery outcomes are separate contracts. The global `notifications.enabled` master never erases provider credentials or desired flags. `/settings` edits secrets through explicit `keep`, `replace`, or `remove` actions, commits only the selected provider in one CAS batch, and reports post-commit observer or activation failures without pretending the durable save rolled back. Malformed provider-local values are quarantined for explicit repair while safe sibling providers remain usable; malformed global notification structure remains fail-closed.\n\n`GJC_NOTIFICATIONS=0` suppresses only automatic generic current-session admission. Explicit `/notify on` can opt the current session back in without mutating durable provider state, and direct provider APIs remain governed by provider effectiveness and their own runtime readiness. Telegram, Discord, and Slack attachments are reconstructed through `SessionRouter`; no provider receives the shared endpoint token.\n\n- [Telegram notification onboarding](./telegram-onboarding.md) documents\n `gjc notify setup` and private-chat pairing.\n- [Discord notification onboarding](./discord-onboarding.md) documents\n `gjc notify setup discord`, required configuration, thread lifecycle, and\n least-privilege permissions.\n- [Slack notification onboarding](./slack-onboarding.md) documents\n `gjc notify setup slack`, Socket Mode configuration, immediate envelope ack,\n and thread lifecycle.\n\n`gjc notify status` reports provider completeness, repair/quarantine state, desired intent, effective enablement, and masked tokens. Destination identifiers remain visible and may be sensitive. The Discord and Slack setup commands are non-interactive and require their documented identifier and token flags; supply secrets through an approved local mechanism, not examples, committed files, shell history, logs, or chat. `gjc notify health --provider <provider> --probe` performs a provider-owned REST diagnostic even when complete credentials are intentionally inactive, while `gjc notify test --provider <provider>` additionally requires effective enablement and runtime readiness.\n\nSession lifecycle and attachment routing are SDK-core services shared by every\nchat provider. `SessionLifecycleService` authorizes typed create, fork, resume,\nclose, delete, and list requests, derives the Broker idempotency identity, and\nprojects credential-free outcomes. The Broker remains the only lifecycle\nexecutor and durable terminal authority.\n\n`SessionRouter` consumes the Broker `SessionIndex`, resolves exact endpoint\nauthority, retains endpoint credentials and SDK clients, and owns replay,\nreconnect, rotation, and stale-attachment revocation. Telegram, Discord, and\nSlack receive only opaque current-generation attachments. Provider daemons own\ntransport leases, cursors, rate limits, threads/topics/messages, presentation\njournals, and delivery receipts; they cannot read endpoint files or tokens,\nallocate SessionIds, or perform session process lifecycle effects.\n\n\n## Managed Telegram daemon (bundled reference client)\n\nThe managed Telegram client is a provider supervisor and presentation adapter.\nIt owns the single `getUpdates` poller and Telegram topic state, while\n`SessionRouter` reconstructs SDK attachments from Broker state. A provider\nrestart never creates, resumes, closes, or mutates a GJC session by itself.\n\nFor Telegram forum topics, the daemon deletes the presentation topic through\n`deleteForumTopic` (falling back to `closeForumTopic` when deletion is unavailable)\nwhen `SessionRouter` retires the current attachment or when the daemon confirms\nthat no eligible local session still owns the topic. A resumed session creates or\nrebinds a fresh current-generation topic before sending again. Topic cleanup is\nbest-effort and cannot change the Broker lifecycle result.\n\n### Singleton poller and trust model\n\nTelegram `getUpdates` allows only one active long-poll owner per bot token. The\nmanaged daemon enforces **one bot token = one getUpdates poller** with a local\nlock/state file under the agent directory. New sessions attach to the existing\nfresh daemon owner instead of starting another poller, preventing Telegram 409\nconflicts.\n\nThe trust model is intentionally strict:\n\n- setup pairs exactly one private Telegram chat;\n- runtime accepts updates only from that paired chat id;\n- groups, supergroups, channels, and unpaired users never receive session names,\n action ids, pending status, or configuration hints;\n- daemon state stores a token fingerprint, not the raw bot token.\n\n### Routing in private-chat topics\n\nThe paired private chat prefers Telegram topics for coordinator/lifecycle sessions\n(Threaded Mode). The daemon tags messages by session, stores compact callback\naliases for inline buttons, and routes replies back to the exact session/action.\nOrdinary sessions use flat delivery and do not create topics. A forum-enabled\nsupergroup is no longer required: when the bot owner enables Threaded Mode in\n@BotFather, the daemon creates topics only for admitted orchestration sessions.\nGJC cannot enable Threaded Mode through the Bot API; setup only verifies the\ncapability and guides the manual BotFather toggle.\n\nIf BotFather's per-bot **Bot Settings** menu does not show **Threads Settings**\nor **Threaded Mode**, the supported fallback is the normal private-chat pairing.\nSetup can be saved as `threaded=unverified`/`threaded=unknown`, and the daemon\nstill tries topics when Telegram allows them. When `createForumTopic` is refused,\nthe daemon does not drop the send: it routes the notification to the normal\n(flat) paired private chat and posts a one-time nudge: `Flat Telegram private chat\nsupports outbound notifications and inline ask buttons only. Enable Threaded Mode\nin @BotFather > Bot Settings > Threads Settings for free-text replies and session\ncommands.` Pairing is private-only, so flat delivery stays within the user's own\nprivate DM.\n\nSupported reply paths:\n\n- tap an inline button on an ask notification;\n- reply inside the session's thread/topic (replies are thread-native; the\n topic identifies the session, so no session tag is needed).\n\nIn threaded mode the user can also adjust per-session behaviour with in-thread\nconfig commands: `/verbose` (per-tool-turn assistant text), `/lean` (settled\nassistant answer at idle plus immediate ask lead-ins; the default),\n`/verbosity <lean|verbose>`, and `/redact <on|off>`. The legacy\n`/answer <session-tag> <answer>` command is removed — replies are routed by the\ntopic they arrive in.\n\nFlat fallback keeps outbound notifications and inline-button answers working, but\nplain free-text never guesses from the global pending-ask set. Free-text replies\nand `/verbose`/`/lean`/`/verbosity`/`/redact` commands are thread-native and\nrequire Threaded Mode/topic routing. Enable Threaded Mode in @BotFather > Bot\nSettings > Threads Settings when you need free-text replies or session commands.\nDo not pair a group, supergroup, or channel to work around a missing BotFather\nmenu; the bundled setup flow is\nprivate-chat only, and non-private chat ids remain fail-closed to avoid session\ndata leaks.\nBecause the flat private chat has no per-session topic, flat idle markers carry\na short session tag (`🟢 Agent idle · <tag>`, last six characters of the session\nid) so concurrent sessions stay distinguishable. The tag never appears on asks\nor in threaded/topic delivery, where the topic itself identifies the session.\n\nUnknown, expired, or restart-unvalidated callback aliases fail closed: the daemon\nsends guidance and does not guess a target session or action.\n\n### Discord and Slack setup\n\nDiscord and Slack use the same internal notification events and reply protocol as\nTelegram. Store only runtime credentials in local GJC settings or environment;\nnever paste bot tokens, webhook URLs, transcripts, prompts, host paths, or raw logs\ninto docs, tests, issues, or PR comments.\n\nConfiguration keys:\n\n```yaml\nnotifications:\n enabled: true\n discord:\n botToken: \"<local Discord bot token>\"\n applicationId: \"<Discord application id>\"\n guildId: \"<Discord guild id>\"\n parentChannelId: \"<Discord parent channel id>\"\n slack:\n botToken: \"<local Slack bot token>\"\n appToken: \"<local Slack app-level token>\"\n workspaceId: \"<Slack workspace id>\"\n channelId: \"<Slack channel id>\"\n authorizedUserId: \"<Slack user id authorized for inbound replies and commands>\"\n redact: true\n```\n\nThe bundled adapters intentionally render public-safe message bodies and return\nroute metadata only for pending internal actions. They do not own polling,\nsession scans, daemon locks, rate limits, or SDK lifecycle. Production transport\nsenders should consume the adapter payloads and keep all credential-bearing HTTP\nor gateway details outside logged payloads.\n### Redaction\n\n`notifications.redact` strips sensitive content before remote delivery, but\n**asks are exempt**: an ask is an interactive prompt the human must read and\nanswer remotely, so its `question` and `options` are always sent unredacted\n(otherwise it would be unanswerable). When redaction is enabled, `idle`\nsummaries are removed and streamed content frames (`turn_stream`,\n`context_update`, `image_attachment`) are suppressed at their emit sites. When\nredaction is disabled, all content is delivered unchanged.\n\n### Local `/notify`\n\nInside a GJC session, `/notify` controls the current session only:\n\n- `/notify status` reports enabled/disabled state, daemon observation when known,\n and redaction state without printing secrets;\n- `/notify off` disables the current session's notification endpoint and removes\n its discovery record without mutating global Settings;\n- `/notify on` re-enables the current session when global setup is complete and\n `GJC_NOTIFICATIONS=0` is not forcing opt-out.\n\n## Session lifecycle and attachment surfaces\n\nSDK core exposes two related provider-neutral capabilities:\n\n1. **`SessionLifecycleService`** accepts an authenticated actor, an explicit\n operation capability, a stable caller request key, and a typed target. It\n derives one Broker idempotency key and invokes the canonical Broker lifecycle\n operation. Results never expose endpoint URLs, tokens, process identities,\n cleanup paths, or raw Broker receipts.\n2. **`SessionRouter`** owns live attachment discovery and transport. It validates\n the exact indexed endpoint generation, keeps credentials and `SdkClient`\n instances private, replays from the attachment cursor, reconnects after\n rotation, and revokes stale capabilities. Provider-facing attachments expose\n only `sessionId`, `generation`, `isCurrent()`, and `send()`.\n\n### Dispatch-boundary observers (managed router path)\n\n`SessionRouter.request(sessionId, frame, expectedGeneration?, expectedAttachment?, options)`\naccepts two optional synchronous observers in `options` — the supported\ndispatch-boundary surface for transport-close-aware consumers (#4640):\n\n- **`beforeDispatch(context)`** — fires immediately before the wire write.\n Throwing (or any synchronous failure) aborts the dispatch with nothing on the\n wire, no sent record, and a retryable rejection carrying the caller's own\n error. Returning a thenable (e.g. an `async` function) is a contract\n violation: the dispatch aborts pre-send and the eventual rejection is sunk.\n- **`onDispatch(context)`** — fires synchronously immediately after the frame is\n handed to the socket, never before. `context.frame.id` is the exact correlated\n identity a response must carry; from this point a transport close before the\n response settles the request as `uncertain_after_send`. Observer throws and\n returned-thenable rejections are sunk; they can neither displace settlement\n nor reach the process unhandled-rejection channel.\n\nThe observer `context.frame` is a **deep-frozen, credential-redacted copy**: the\ninjected session endpoint `token` (and any other credential field) exists only\non the internal wire frame and is never handed to observer code, and mutation\nattempts throw in strict mode. The raw credential-bearing `SdkClient` remains\nunexported (`./sdk/client` is blocked in the package export map); the router is\nthe only supported path to this boundary.\n\nThere is no daemon-owned lifecycle control endpoint, provider lifecycle ledger,\nnotification-root scanner, or provider-created SessionId. Telegram `/session_*`\ncommands call the SDK lifecycle service directly. A Telegram update or topic\nreservation supplies the stable provider request identity; the Broker allocates\nthe SessionId, and Telegram CAS-binds the returned opaque ID to its presentation\nmapping.\n\n### Lifecycle trust and recovery\n\n- paired provider identity and operation capability are checked before the\n Broker call;\n- retries reuse the same provider request key, so one request produces one\n Broker ledger identity and at most one lifecycle effect;\n- `terminal_uncertain` remains uncertain and is reconciled from Broker ledger,\n effect marker, process incarnation, endpoint/index, readiness, and exact\n cleanup evidence only;\n- provider transport restart reloads cursor and presentation state, while the\n Router reconstructs attachments from Broker state;\n- stale endpoint generations and attachments fail closed;\n- provider topic/thread/message cleanup cannot rewrite a confirmed lifecycle\n outcome.\n\n### Phone test guide (create / close / resume from Telegram)\n\nEnd-to-end manual check once `gjc notify setup` has paired your private chat:\n\n1. Run `gjc notify setup` and start or reload the Telegram provider supervisor.\n The supervisor owns only the Telegram poller and presentation state.\n2. Send `/session_create path <repo-dir>`, `/session_create worktree <repo>\n <branch>`, or `/session_create dir <newdir>`. The SDK lifecycle service submits\n one canonical Broker create request; the bot reports the credential-free\n outcome.\n3. `/session_recent` lists verified recent managed sessions.\n4. `/session_close <sessionId>` asks Broker lifecycle to close the exact managed\n session and preserves history.\n5. `/session_resume <sessionId|prefix>` resolves verified managed history,\n reattaches a live session or performs canonical Broker resume, and refuses\n ambiguous prefixes.\n\nCommands are accepted only from the paired chat. Duplicate Telegram updates and\nreplayed topic reservations reuse their original request identity; they never\nallocate or spawn a second session.\n\n## Master mode surfaces\n\n`gjc --master [--scope repo|pwd|global]` launches an interactive-TUI-only\nmaster session (default scope `repo`). The master receives one bundled\noperating-guidance block appended after custom system-prompt transformation and\nexactly one scoped, no-probe broker peer snapshot immediately before its first\naccepted provider request; idle masters collect nothing.\n\nMaster orchestration is broker-routed and best effort: scoped discovery via\n`gjc sdk search` (result-level scope envelopes, exact worktree/cwd identity,\nno scope fallback) and task-seeded children via the local-only `gjc sdk spawn`\n(see docs/sdk-session-cli.md). The broker is the sole authority for\nmaster-created lifecycle: an opaque idempotency identity serializes each spawn\nbehind a durable create-or-join claim, seed delivery is Q26-correlated (never\nblindly re-sent), close mutates only an exactly re-proven substrate, and\norphaned children converge through ordinary close after\n`sdk.masterOrphanGraceMs`. Task text and master capability are transient\ndispatch inputs and never persist in any durable store, log, or output.\n",
|
|
96
96
|
"secrets.md": "# Secret Obfuscation\n\nPrevents sensitive values (API keys, tokens, passwords) from being sent to LLM providers. When enabled, secrets are replaced with authenticated placeholders before leaving the process, and restored in tool call arguments returned by the model.\n\n## Enabling\n\nDisabled by default. Toggle via `/settings` UI or directly in `config.yml`:\n\n```yaml\nsecrets:\n enabled: true\n```\n\n## How it works\n\n1. On session startup, secrets are collected from two sources:\n - **Environment variables** whose names match common secret patterns (`KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `PASS`, `AUTH`, `CREDENTIAL`, `PRIVATE`, `OAUTH`) with values >= 8 characters\n - **`secrets.yml` files** (see below)\n\n2. Outbound text messages to the LLM have secret values replaced with authenticated, versioned placeholders like `#GJC1_…#`.\n\n3. Session context/tool arguments returned from the model are deep-walked and obfuscation placeholders are restored to original values before display or execution.\n\nTwo modes control what happens to each secret:\n\n| Mode | Behavior | Reversible |\n| --------------------- | ----------------------------------------------- | ----------------------------------------------- |\n| `obfuscate` (default) | Replaced with authenticated `#GJC1_…#` token | Yes (deobfuscated in tool args/session context) |\n| `replace` | Replaced with deterministic same-length string | No (one-way) |\n\nAuthenticated placeholders use a process-local key. Plain-secret tokens remain stable across sessions, reloads, and forks within the running process; after a process restart, earlier tokens intentionally remain opaque.\n\nRegex-discovered tokens are reversible only by the originating obfuscator instance. A fresh obfuscator in the same process or after restart keeps them opaque because regex matches are not reconstructed from persisted placeholders.\n\n## secrets.yml\n\nDefine custom secret entries in YAML. Two locations are checked:\n\n| Level | Path | Purpose |\n| ------- | -------------------------- | --------------------------- |\n| Global | `~/.gjc/agent/secrets.yml` | Plain and regex secrets across all projects |\n| Project | `<cwd>/.gjc/secrets.yml` | Project-specific plain secrets |\n\nProject plain entries override global plain entries with matching `content`; a global regex with the same `content` remains active. Project-scope regex entries are ignored because workspace-contained files are not trusted to supply executable regex patterns. This project scope includes `<cwd>/.gjc/secrets.yml` and any caller-supplied agent directory whose lexical or canonical path is contained within the workspace.\n\n### Schema\n\nEach entry in the array has these fields:\n\n| Field | Type | Required | Description |\n| ------------- | ---------------------------- | -------- | ------------------------------------------------- |\n| `type` | `\"plain\"` or `\"regex\"` | Yes | Match strategy |\n| `content` | string | Yes | The secret value (plain) or regex pattern (regex) |\n| `mode` | `\"obfuscate\"` or `\"replace\"` | No | Default: `\"obfuscate\"` |\n| `replacement` | string | No | Custom replacement (replace mode only) |\n| `flags` | string | No | Regex flags (regex type only) |\n\n### Examples\n\n#### Plain secrets\n\n```yaml\n# Obfuscate a specific API key (default mode)\n- type: plain\n content: sk-proj-abc123def456\n\n# Replace a database password with a fixed string\n- type: plain\n content: hunter2\n mode: replace\n replacement: \"********\"\n```\n\n#### Regex secrets\n\nRegex entries are supported only by agent configuration outside the current workspace (normally `~/.gjc/agent/secrets.yml`). Use `type: plain` for workspace-contained configuration.\n\n```yaml\n# Obfuscate any AWS-style key\n- type: regex\n content: \"AKIA[0-9A-Z]{16}\"\n\n# Case-insensitive match with explicit flags\n- type: regex\n content: \"api[_-]?key\\\\s*=\\\\s*\\\\w+\"\n flags: \"i\"\n\n# Regex literal syntax (pattern and flags in one string)\n- type: regex\n content: \"/bearer\\\\s+[a-zA-Z0-9._~+\\\\/=-]+/i\"\n```\n\nRegex entries always scan globally (the `g` flag is enforced automatically). The regex literal syntax `/pattern/flags` is supported as an alternative to separate `content` + `flags` fields. Escaped slashes within the pattern (`\\\\/`) are handled correctly. The sticky `y` flag is rejected because it would prevent global scanning.\n\n#### Replace mode with regex\n\n```yaml\n# One-way replace connection strings (not reversible)\n- type: regex\n content: \"postgres://[^\\\\s]+\"\n mode: replace\n replacement: \"postgres://***\"\n```\n\n## Interaction with env var detection\n\nEnvironment variables are collected first, then file-defined entries are appended. File entries can cover secrets that don't live in env vars (config files, hardcoded values, etc.). If the same plain value appears in both env and file entries, the env entry's obfuscate-mode mapping is used first.\n\n## Key files\n\n- `packages/coding-agent/src/secrets/index.ts` -- loading, merging, env var collection\n- `packages/coding-agent/src/secrets/obfuscator.ts` -- `SecretObfuscator` class, placeholder generation, message obfuscation\n- `packages/coding-agent/src/secrets/regex.ts` -- regex literal parsing and compilation\n- `packages/coding-agent/src/config/settings-schema.ts` -- `secrets.enabled` setting definition\n\n## See also\n\n- [`auth-broker-gateway.md`](./auth-broker-gateway.md) -- remote credential vault and forward-proxy that keep provider OAuth refresh tokens and access tokens off developer hosts entirely (complementary to in-process obfuscation).\n",
|
|
97
97
|
"session-import.md": "# Session Import (Codex and Claude)\n\nGJC can import an external **Codex** or **Claude** session transcript into a new\nGJC session so you can continue the conversation with reconstructed context\ninstead of copying a long session by hand.\n\n## CLI surface\n\n```\n/import-session <transcript-file> [--provider codex|claude]\n```\n\n- Available on Linux in the interactive TUI and trusted local startup command path. It is\n deliberately excluded from ACP and remote-control transports because it reads\n an operator-selected local file.\n- `<transcript-file>` is an explicit, user-selected export/transcript file\n (absolute or cwd-relative; quote paths containing spaces).\n- `--provider` narrows detection to one provider and fails closed on a\n mismatch. Without it, the format is detected deterministically from the file\n content.\n- On success, GJC creates a **new** resumable session containing the reconstructed\n context and reports its id. The active session is not switched automatically;\n use `/resume` to select the import.\n\nThere is intentionally no flag-less provider-directory mode: import never\nenumerates `~/.codex` / `~/.claude` or reads live provider process state. You\nalways name the exact file to import.\n\n## Supported source formats\n\nDetection is content-based and provider-neutral; unknown shapes fail with an\nactionable diagnostic instead of a speculative parse.\n\n| Format | Provider | File shape |\n| --- | --- | --- |\n| `codex-rollout-jsonl` | Codex | Codex CLI rollout transcript (`rollout-*.jsonl`): `session_meta`, `response_item` (`message`, `function_call`, `function_call_output`, `custom_tool_call*`, `local_shell_call`, `web_search_call`, `reasoning`), `event_msg`, `turn_context` records, one JSON object per line. |\n| `claude-code-jsonl` | Claude | Claude Code session transcript (`~/.claude/projects/<slug>/<uuid>.jsonl` copied out explicitly): `user` / `assistant` / `summary` / `system` records with `uuid`/`sessionId` envelopes. |\n| `claude-export-json` | Claude | claude.ai data export: one conversation object (or an array of conversations) with `uuid`, `name`, and `chat_messages[]` (`sender: \"human\" \\| \"assistant\"`, `content[]` text blocks). |\n\nNormalization rules:\n\n- User/assistant text is preserved; consecutive assistant text is merged.\n- Tool calls and results become bounded, single-line **tool/file evidence**\n entries (`$ <tool> <summary>` / `→ <output>`) attached to the preceding\n assistant message. Tool payloads are never executable in the new session.\n- Model-internal content (Codex `reasoning`, Claude `thinking`) and provider\n bookkeeping (`turn_context`, `system`, `isMeta` rows, file-history snapshots)\n are recognized and skipped by design.\n- Records that fail to parse, lack required fields, or use unknown record types\n are **quarantined** — counted and digested (record number, byte length,\n SHA-256) in provenance and the import summary, never silently dropped.\n- A native GJC session file is rejected with `unsupported_format` (use\n resume/fork for those).\n\n## Provenance and identity\n\nEvery imported session persists a `custom` entry with\n`customType: \"session-import\"` recording: provider, format, source file\nbasename, source session id and title (when the format carries them), SHA-256\nand byte size of the exact imported source bytes, converter/sanitizer versions,\nmapped/quarantined/redacted/omitted counts, truncation flag, target session id,\nand import timestamp. The reconstructed context lives in a separate\n`custom_message` entry (same `customType`, `display: true`) so imported context\nis visually and structurally distinct from native GJC history, while still\nparticipating in LLM context like a handoff document.\n\nRe-importing the same file is allowed and creates another distinct session;\nthe provenance record (source hash + import timestamp) is what distinguishes\nthem. The source file is only ever read — verify with the recorded digest.\n\n## Redaction\n\nBefore any imported text enters the new session it passes a fail-closed\nsanitizer (`sanitizerVersion` in provenance): Anthropic/OpenAI/GitHub/Slack/\nGoogle/AWS keys, JWTs, bearer tokens, PEM private keys, long hex secrets,\nURL-embedded credentials, and `NAME=value` / `key: value` assignments whose\nname contains a sensitive word (key/secret/token/password/credential/…).\nMatches are replaced with `[REDACTED]`; the redaction count and rule kinds are\nreported in the summary and provenance. False positives are preferred over\nfalse negatives: a benign look-alike loses one span, a missed credential would\nleak into a durable transcript.\n\n## Bounds and diagnostics\n\n| Bound | Value | Over-limit behavior |\n| --- | --- | --- |\n| Source file size | 64 MiB | `content_too_large` with limit/observed bytes |\n| Normalized messages | 5,000 | `content_too_large` with observed count |\n| Rendered continuation context | 120,000 chars | deterministic head+tail bounding with an explicit elision marker (`omitted` count in provenance) |\n| Single message text | 16,000 chars | truncated with an ellipsis marker |\n| Tool evidence per message | 100 lines / 8 KiB | marked truncated |\n| Quarantine records | 512 retained | count kept, `quarantine.truncated` flag set |\n\nError codes are deterministic and actionable: `invalid_request`,\n`source_not_found`, `source_unreadable`, `source_changed` (retryable),\n`unsupported_format`, `format_mismatch`, `malformed_input`, `content_too_large`,\n`destination_conflict`, `io_failed`. If persistence or post-write verification\nfails, the partially created session file is removed best-effort and the\ncurrent session is never modified.\n\n## Non-goals\n\n- **No model-internal state cloning.** Reasoning traces, token telemetry,\n provider session caches, and tool-execution state are not imported; this is\n context reconstruction, not process migration.\n- **No live provider scraping.** Import reads only the file you name.\n- **No silent drops.** Every source record is mapped, recognized-and-skipped,\n or quarantined with a digest; totals are reported.\n- **No mid-stream import.** `/import-session` refuses to run while a response\n is streaming.\n",
|
|
98
|
-
"session-operations-export-share-fork-resume.md": "# Session Operations: export, dump, share, fork, resume/continue\n\nThis document describes operator-visible behavior for session export/share/fork/resume operations as currently implemented.\n\n## Implementation files\n\n- [`../src/modes/controllers/command-controller.ts`](../packages/coding-agent/src/modes/controllers/command-controller.ts)\n- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)\n- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)\n- [`../src/session-import/`](../packages/coding-agent/src/session-import/)\n- [`../src/export/html/index.ts`](../packages/coding-agent/src/export/html/index.ts)\n- [`../src/export/custom-share.ts`](../packages/coding-agent/src/export/custom-share.ts)\n- [`../src/main.ts`](../packages/coding-agent/src/main.ts)\n\n## Operation matrix\n\n| Operation | Entry path | Session mutation | Session file creation/switch | Output artifact |\n| --------------------------------------- | ------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---- |\n| `/dump` | Interactive slash command | No | No | Clipboard text |\n| `/export [path]` | Interactive slash command | No | No | HTML file |\n| `--export <session.jsonl> [outputPath]` | CLI startup fast-path | No runtime session mutation | No active session; reads target file | HTML file |\n| `/share` | Interactive slash command | No | No | Temp HTML + share URL/gist |\n| `/fork` | Interactive slash command | Yes (active session identity changes) | Creates new session file and switches current session to it (persistent mode only) | Copies artifact directory to new session namespace when present |\n| `--fork <id | path>` | CLI startup | Yes after session creation | Creates a new session fork from the selected source into current cwd/session dir | None |\n| `/import-session <transcript-file> [--provider codex\\|claude]` | Interactive or trusted local startup command | No active-session mutation | Creates one independently resumable native session file | Bounded quarantine digest proof and provenance |\n| `/resume` | Interactive slash command | Yes (active in-memory state replaced) | Switches to selected existing session file | None |\n| `--resume` | CLI startup (picker) | Yes after session creation | Opens selected existing session file | None |\n| `--resume <id | path>` | CLI startup | Yes after session creation | Opens existing session; cross-project case can fork into current project | None |\n| `--continue` | CLI startup | Yes after session creation | Opens terminal breadcrumb or most-recent session; creates new one if none exists | None |\n\n## Import external sessions\n\n`/import-session <transcript-file> [--provider codex|claude]` imports one explicit Codex CLI rollout transcript, Claude Code transcript, or claude.ai conversation export. Format detection is content-based; `--provider` narrows detection and fails closed on a mismatch. The command never scans private live process state or provider history directories.\n\nThe importer reconstructs user/assistant context and bounded tool evidence in a fresh native session. Unsupported or malformed records are never silently dropped: aggregate counts and at most 512 full-record SHA-256 quarantine proofs are retained in provenance. The source basename, provider/format, source/session identifier when available, exact source digest and byte count, converter/sanitizer versions, mapping/redaction counts, and bounded-context state are persisted. Raw provider archives are never copied into the session store.\n\nThe source is opened once with no-follow semantics and read through that retained descriptor; device, inode, link count, size, mtime, and ctime must remain exact through the complete read. Regular hard-linked exports are accepted without weakening managed-session storage, whose files remain single-linked. Secret-bearing values, Authorization/Cookie headers, terminal escapes, C0/C1 controls, bidi/zero-width controls, tool labels, IDs, titles, cwd metadata, and source diagnostics are sanitized before display or persistence.\n\nEach invocation creates one new session, verifies that it reopens with the same captured destination authority and reconstructs continuable history, then releases that authority. Imports are refused while the current session is streaming. Imported sessions do not replace the active session automatically; select the new session with `/resume`.\n\nThe command is available only on Linux in the interactive TUI and trusted local startup command path. It is neither advertised nor dispatched over ACP or remote-control transports.\n## Export and dump\n\n### `/export [outputPath]` (interactive)\n\nFlow:\n\n1. `InputController` routes `/export...` to `CommandController.handleExportCommand`.\n2. The command splits on whitespace and uses only the first argument after `/export` as `outputPath`.\n3. `AgentSession.exportToHtml()` calls `exportSessionToHtml(sessionManager, state, { outputPath, themeName })`.\n4. On success, UI shows path and opens the file in browser.\n\nBehavior details:\n\n- `--copy`, `clipboard`, and `copy` arguments are explicitly rejected with a warning to use `/dump`.\n- Export embeds session header/entries/leaf plus current `systemPrompt` and tool descriptions from agent state.\n- No session entries are appended during export.\n\nCaveat:\n\n- Argument parsing is whitespace-based (`text.split(/\\s+/)`), so quoted paths with spaces are not preserved as a single path by this command path.\n\n### `--export <inputSessionFile> [outputPath]` (CLI)\n\nFlow in `main.ts`:\n\n1. Handled early (before interactive/session startup).\n2. Calls `exportFromFile(inputPath, outputPath?)`.\n3. `SessionManager.open(inputPath)` loads entries, then HTML is generated and written.\n4. Process prints `Exported to: ...` and exits.\n\nBehavior details:\n\n- Missing input file surfaces as `File not found: <path>`.\n- This path does not create an `AgentSession` and does not mutate any running session.\n\n### `/dump` (interactive clipboard export)\n\nFlow:\n\n1. `CommandController.handleDumpCommand()` calls `session.formatSessionAsText()`.\n2. If empty string, reports `No messages to dump yet.`\n3. Otherwise copies to clipboard via native `copyToClipboard`.\n\nDump content includes:\n\n- System prompt\n- Active model/thinking level\n- Tool definitions + parameters\n- User/assistant messages\n- Thinking blocks and tool calls\n- Tool results and execution blocks (except `excludeFromContext` bash/python entries)\n- Custom/hook/file mention/branch summary/compaction summary entries\n\nNo session persistence changes are made by dumping.\n\n## Share\n\n`/share` is interactive-only and always starts by exporting current session to a temp HTML file.\n\n### Phase 1: temp export\n\n- Temp file path: `${os.tmpdir()}/${Snowflake.next()}.html`\n- Uses `session.exportToHtml(tmpFile)`\n- If export fails (notably in-memory sessions), share ends with error.\n\n### Phase 2: custom share handler (if present)\n\n`loadCustomShare()` checks `~/.gjc/agent` for first existing candidate:\n\n- `share.ts`\n- `share.js`\n- `share.mjs`\n\nRequirements:\n\n- Module must default-export a function `(htmlPath) => Promise<CustomShareResult | string | undefined>`.\n\nIf present and valid:\n\n- UI enters `Sharing...` loader state.\n- Handler result interpretation:\n - string => treated as URL, shown and opened\n - object => `url` and/or `message` shown; `url` opened\n - `undefined`/falsy => generic `Session shared`\n- Temp file is removed after completion.\n\nCritical fallback behavior:\n\n- If custom handler exists but loading fails, command errors and returns.\n- If custom handler executes and throws, command errors and returns.\n- In both failure cases, it **does not** fall back to GitHub gist.\n- Gist fallback happens only when no custom share script exists.\n\n### Phase 3: default gist fallback\n\nOnly when no custom share handler is found:\n\n1. Validates `gh auth status`.\n2. Shows `Creating gist...` loader.\n3. Runs `gh gist create --public=false <tmpFile>`.\n4. Parses gist URL, derives gist id, builds preview URL `https://gistpreview.github.io/?<id>`.\n5. Shows both preview and gist URLs; opens preview.\n\nCancellation/abort semantics in share:\n\n- Loader has `onAbort` hook that restores editor UI and reports `Share cancelled`.\n- The underlying `gh gist create` command is not passed an abort signal in this code path; cancellation is UI-level and checked after command returns.\n\n## Fork\n\nInteractive `/fork` creates a new session from the current one and switches the active session identity.\n\n### Preconditions and immediate guards\n\n- If agent is streaming, `/fork` is rejected with warning.\n- UI status/loading indicators are cleared before operation.\n\n### Session-level flow\n\n`AgentSession.fork()`:\n\n1. Emits `session_before_switch` with `reason: \"fork\"` (cancellable).\n2. Flushes pending writes.\n3. Calls `SessionManager.fork()`.\n4. Copies artifacts directory from old session namespace to new namespace (best-effort; non-ENOENT copy failures are logged, not fatal).\n5. Updates `agent.sessionId`.\n6. Emits `session_switch` with `reason: \"fork\"`.\n\n`SessionManager.fork()` behavior:\n\n- Requires persistent mode and existing session file.\n- Creates new session id and new JSONL file path.\n- Rewrites header with:\n - new `id`\n - new timestamp\n - `cwd` unchanged\n - `parentSession` set to previous session id\n- Keeps all non-header entries unchanged in the new file.\n\n### Non-persistent behavior\n\n- In-memory session manager returns `undefined` from `fork()`.\n- `AgentSession.fork()` returns `false`.\n- UI reports `Fork failed (session not persisted or cancelled)`.\n\n### CLI `--fork <id|path>`\n\nStartup `--fork` is resolved before normal session creation:\n\n1. `--fork` is rejected with `--no-session`.\n2. Path-like values (`/`, `\\`, or `.jsonl`) call `SessionManager.forkFrom(path, cwd, sessionDir)`.\n3. Other values resolve like resumable session ids via current scope and then global search when allowed.\n4. The forked file is created in the current cwd/session-dir scope and becomes the active session manager for startup.\n\n### Managed directory migration during session operations\n\nDefault persistent creates and forks write only to the managed v2 workspace scope. A resume/list operation may surface a validated legacy candidate for the same canonical workspace identity; with `session.directoryMigration: \"copy-retain\"`, the migration path copies it into v2 and retains the source. It never replaces an existing destination, and a migration tombstone prevents completed/retired legacy work from being retried as fresh work. `disabled` leaves legacy data in place.\n\nThe migration path does not delete legacy sessions or artifacts automatically. It fails closed on conflicting bindings, changed source identity, unsafe artifact trees, or unavailable owner-only path security; it does not claim authentication or protection against hostile concurrent filesystem races. Explicit `--session-dir` remains an operator-selected override.\n\n## Resume and continue\n\n## Interactive `/resume`\n\nFlow:\n\n1. Opens session selector populated via `SessionManager.list(currentCwd, currentSessionDir)`.\n2. On selection, `SelectorController.handleResumeSession(sessionPath)` calls `session.switchSession(sessionPath)`.\n3. UI clears/rebuilds chat and todos, then reports `Resumed session`.\n\nNotes:\n\n- This picker only lists sessions in the current session directory scope.\n- It does not use global cross-project search.\n\n## CLI `--resume`\n\n### `--resume` (no value)\n\n- `main.ts` lists sessions for current cwd/sessionDir and opens picker.\n- Selected path is opened with `SessionManager.open(selectedPath)` before session creation.\n\n### `--resume <value>`\n\n`createSessionManager()` resolution order:\n\n1. If value looks like path (`/`, `\\`, or `.jsonl`), open directly.\n2. Else treat as id prefix:\n - search current scope (`SessionManager.list(cwd, sessionDir)`)\n - if not found and no explicit `sessionDir`, search global (`SessionManager.listAll()`)\n\nCross-project id match behavior:\n\n- If matched session cwd differs from current cwd, CLI asks:\n - `Session found in different project ... Fork into current directory? [y/N]`\n- On yes: `SessionManager.forkFrom(match.path, cwd, sessionDir)` creates a new local forked file.\n- On no/non-TTY default: command errors.\n\n## CLI `--continue`\n\n`SessionManager.continueRecent(cwd, sessionDir)`:\n\n1. Resolves session dir for current cwd.\n2. Reads terminal-scoped breadcrumb first.\n3. Falls back to most recently modified session file.\n4. Opens found session; if none exists, creates new session.\n\nThis is startup-only behavior; there is no interactive `/continue` slash command.\n\n## How session switching actually mutates runtime state\n\n`AgentSession.switchSession(sessionPath)` does the runtime transition used by resume-like operations:\n\n1. Emit `session_before_switch` with `reason: \"resume\"` and `targetSessionFile` (cancellable).\n2. Disconnect agent event subscription and abort in-flight work.\n3. Clear queued steering/follow-up/next-turn messages.\n4. Flush current session manager writes.\n5. `sessionManager.setSessionFile(sessionPath)` and update `agent.sessionId`.\n6. Build session context from loaded entries.\n7. Emit `session_switch` with `reason: \"resume\"`.\n8. Replace agent messages from context.\n9. Restore model (if available in current registry).\n10. Restore or initialize thinking level.\n11. Reconnect agent event subscription.\n\nNo new session file is created by `switchSession()` itself.\n\n## Event emissions and cancellation points\n\n### Switch/fork lifecycle hooks\n\nFor `newSession`, `fork`, and `switchSession`:\n\n- Before event: `session_before_switch`\n - reasons: `new`, `fork`, `resume`\n - cancellable by returning `{ cancel: true }`\n- After event: `session_switch`\n - same reason set\n - includes `previousSessionFile`\n\n`ExtensionRunner.emit()` returns early on the first cancelling before-event result.\n\n### Custom tool `onSession` behavior\n\nSDK bridges extension session events to custom tool `onSession` callbacks:\n\n- `session_switch` -> `onSession({ reason: \"switch\", previousSessionFile })`\n- `session_branch` -> `reason: \"branch\"`\n- `session_start` -> `reason: \"start\"`\n- `session_tree` -> `reason: \"tree\"`\n- `session_shutdown` -> `reason: \"shutdown\"`\n\nThese callbacks are observational; they do not cancel switch/fork.\n\n### Other cancellation surfaces relevant to this doc\n\n- `/fork` is blocked while streaming (user must wait/abort current response first).\n- `/resume` selector can be cancelled by user closing selector.\n- Cross-project `--resume <id>` can be cancelled by declining fork prompt.\n- `/share` has UI abort path (`Share cancelled`) for gist flow; it does not wire process-kill semantics for `gh gist create` in this code path.\n\n## Non-persistent (in-memory) session behavior\n\nWhen session manager is created with `SessionManager.inMemory()` (`--no-session`):\n\n- Session file path is absent.\n- `/export` and `/share` fail with `Cannot export in-memory session to HTML` (propagated to command error UI).\n- `/fork` fails because `SessionManager.fork()` requires persistence.\n- `/dump` still works because it serializes in-memory agent state.\n- CLI resume/continue semantics are bypassed if `--no-session` is set, because manager creation returns in-memory immediately.\n\n## Known implementation caveats (as of current code)\n\n- `SelectorController.handleResumeSession()` does not check the boolean result from `session.switchSession(...)`; a hook-cancelled switch can still proceed through UI \"Resumed session\" repaint/status path.\n- `/share` custom-share failures do not degrade to default gist fallback; they terminate the command with error.\n- `/export` argument tokenization is simplistic and does not preserve quoted paths with spaces.\n",
|
|
99
|
-
"session-switching-and-recent-listing.md": "# Session switching and recent session listing\n\nThis document describes how coding-agent discovers recent sessions, resolves `--resume` targets, presents session pickers, and switches the active runtime session.\n\nIt focuses on current implementation behavior, including fallback paths and caveats.\n\n## Implementation files\n\n- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)\n- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)\n- [`../src/cli/session-picker.ts`](../packages/coding-agent/src/cli/session-picker.ts)\n- [`../src/modes/components/session-selector.ts`](../packages/coding-agent/src/modes/components/session-selector.ts)\n- [`../src/modes/controllers/selector-controller.ts`](../packages/coding-agent/src/modes/controllers/selector-controller.ts)\n- [`../src/main.ts`](../packages/coding-agent/src/main.ts)\n- [`../src/sdk/session.ts`](../packages/coding-agent/src/sdk/session.ts)\n- [`../src/modes/interactive-mode.ts`](../packages/coding-agent/src/modes/interactive-mode.ts)\n- [`../src/modes/utils/ui-helpers.ts`](../packages/coding-agent/src/modes/utils/ui-helpers.ts)\n\n## Recent-session discovery\n\n### Directory scope\n\nThe default managed scope is `~/.gjc/agent/sessions/v2-<identity-digest>/`, where the digest is derived from the native canonical workspace identity rather than a path-string substitution. It is collision-resistant, but the digest is not a public injective identity or an authentication credential. POSIX aliases and supported Windows local aliases for the same directory resolve to the same scope; UNC/network workspaces are rejected as unsupported.\n\n`SessionManager.list(cwd, sessionDir?)` reads the selected directory unless an explicit `sessionDir` is provided. The public readonly SDK API is `resolveManagedSessionScope()` followed by `listManagedSessionCandidates()` from `@gajae-code/coding-agent/sdk`; both are versioned by `SESSION_DIRECTORY_API_VERSION` (currently `1`). The resolver/listing API creates, migrates, and deletes nothing. Listing reports validated v2 and legacy candidates, invalid candidates, and a foreign count instead of treating arbitrary files as owned sessions.\n\nDefault writes are v2-only. Legacy discovery/migration is lazy, validates identity before use, and follows `session.directoryMigration` (`copy-retain` by default; `disabled` to opt out); no automatic legacy cleanup occurs.\n\n### Two listing paths with different payloads\n\nThere are two different listing pipelines:\n\n1. `getRecentSessions(sessionDir, limit)` (welcome/summary view)\n - Reads a bounded 4KB prefix plus reverse-scanned v4/v5 header patches from each file.\n - Parses header metadata, applicable trailing patches, and the earliest user text preview.\n - Returns lightweight `RecentSessionInfo` with lazy `name` and `timeAgo` getters.\n - Sorts by file `mtime` descending.\n\n2. `SessionManager.list(...)` / `SessionManager.listAll()` (resume pickers and ID matching)\n - Reads a bounded 4KB prefix, then reverse-scans for the latest strict `header_patch` values (cwd/title) in 64KB chunks, stopping once both fields resolve.\n - Recent patches near EOF stay cheap; when a field is still missing the scan continues past the historical 16KB window so a buried but still-canonical title remains listable without a full sequential JSONL parse of multi-MB message bodies.\n - A transcript that never emitted a `header_patch` cannot be recognized as such without reaching BOF, so its whole file is walked. The scan therefore uses its own 64KB buffer rather than the 4KB prefix buffer: chunk size sets how many `read` syscalls a listing costs per transcript byte, and listings run on every resume, continue, and picker open.\n - Builds `SessionInfo` objects from that projection plus prefix preview extraction.\n - Drops sessions with zero `message` entries and sorts by `modified` descending.\n\n### Metadata fallback behavior\n\nFor recent summaries (`RecentSessionInfo`):\n\n- display name preference: `header.title` -> first user prompt -> `header.id` -> filename\n- name is truncated to 40 chars for compact displays\n- control characters/newlines are stripped/sanitized from title-derived names\n\nFor `SessionInfo` list entries:\n\n- `title` is `header.title` or latest compaction `shortSummary`\n- `firstMessage` is first user message text or `\"(no messages)\"`\n\n## `--continue` resolution and terminal breadcrumb preference\n\n`SessionManager.continueRecent(cwd, sessionDir?)` resolves the target in this order:\n\n1. Read terminal-scoped breadcrumb (`~/.gjc/agent/terminal-sessions/<terminal-id>`)\n2. Validate breadcrumb:\n - current terminal can be identified\n - breadcrumb cwd matches current cwd (resolved path compare)\n - referenced file still exists\n3. If breadcrumb is invalid/missing, fall back to newest file by mtime in the session dir (`findMostRecentSession`)\n4. If none found, create a new session\n\nTerminal ID derivation prefers TTY path and falls back to env-based identifiers (`KITTY_WINDOW_ID`, `TMUX_PANE`, `TERM_SESSION_ID`, `WT_SESSION`).\n\nBreadcrumb writes are best-effort and non-fatal.\n\n## Startup-time resume target resolution (`main.ts`)\n\n### `--resume <value>`\n\n`createSessionManager(...)` handles string-valued `--resume` in two modes:\n\n1. Path-like value (contains `/`, `\\\\`, or ends with `.jsonl`)\n - direct `SessionManager.open(sessionArg, parsed.sessionDir)`\n\n2. ID prefix value\n - find match in `SessionManager.list(cwd, sessionDir)` by `id.startsWith(sessionArg)`\n - if no local match and `sessionDir` is not forced, try `SessionManager.listAll()`\n - first match is used (no ambiguity prompt)\n\nCross-project match behavior:\n\n- if matched session cwd differs from current cwd, CLI prompts whether to fork into current project\n- yes -> `SessionManager.forkFrom(...)`\n- no -> throws error (`Session \"...\" is in another project (...)`)\n\nNo match -> throws error (`Session \"...\" not found.`).\n\n### `--resume` (no value)\n\nHandled after initial session-manager construction:\n\n1. list local candidates through the bounded read-only resume-picker path\n2. if empty: print `No sessions found` and exit early\n3. open the TUI picker; cancellation returns silently and exits without writes\n4. inspect the selected transcript read-only and confirm resumable tail state when required\n5. strictly open the approved identity, rechecking ownership before any replay-sanitization persistence\n6. publish the terminal breadcrumb only after strict-open sanitation succeeds, then continue startup from the opened manager\n### `--continue`\n\nUses `SessionManager.continueRecent(...)` directly (breadcrumb-first behavior above).\n\n## Picker-based selection internals\n\n## CLI picker (`src/cli/session-picker.ts`)\n\n`selectSession(sessions)` creates a standalone TUI with `SessionSelectorComponent` and resolves exactly once:\n\n- selection -> resolves selected path\n- cancel (Esc) -> resolves `null`\n- hard exit (Ctrl+C path) -> stops TUI and `process.exit(0)`\n\n## Interactive in-session picker (`SelectorController.showSessionSelector`)\n\nFlow:\n\n1. fetch sessions from the current session directory via `SessionManager.listForResumePickerReadOnly(currentCwd, currentSessionDir)`\n2. mount `SessionSelectorComponent` in editor area using `showSelector(...)`\n3. callbacks:\n - select -> close selector and call `handleResumeSession(sessionPath)`\n - cancel -> restore editor and rerender\n - exit -> `ctx.shutdown()`\n\n## Session selector component behavior\n\n`SessionList` supports:\n\n- arrow/page navigation\n- Enter to select\n- Esc to cancel\n- Ctrl+C to exit\n- fuzzy search across session id/title/cwd/first message/all messages/path\n\nEmpty-list render behavior:\n\n- renders a message instead of crashing\n- Enter on empty does nothing (no callback)\n- Esc/Ctrl+C still work\n\nCaveat: UI text says `Press Tab to view all`, but this component currently has no Tab handler and current wiring only lists current-scope sessions.\n\n## Runtime switch execution (`AgentSession.switchSession`)\n\n`switchSession(sessionPath)` is the core in-process switch path.\n\nLifecycle/state transition:\n\n1. capture `previousSessionFile`\n2. emit `session_before_switch` hook event (`reason: \"resume\"`, cancellable)\n3. if canceled -> return `false` with no switch\n4. disconnect from current agent event stream\n5. abort active generation/tool flow\n6. clear queued steering/follow-up/next-turn message buffers\n7. flush session writer (`sessionManager.flush()`) to persist pending writes\n8. `sessionManager.setSessionFile(sessionPath)`\n - updates session file pointer\n - writes terminal breadcrumb\n - loads entries / migrates / blob-resolves / reindexes\n - if missing/invalid file data: initializes a new session at that path and rewrites header\n9. update `agent.sessionId`\n10. rebuild display context via `buildDisplaySessionContext()`\n11. restore persisted/discovered MCP tool selections and rebuild active tools/system prompt when discovery is enabled\n12. emit `session_switch` hook event (`reason: \"resume\"`, `previousSessionFile`)\n13. replace agent messages with rebuilt context and sync todos\n14. close provider sessions when switching to a different session or when same-session reload changed replay messages\n15. restore default model from `sessionContext.models.default` if available and present in model registry\n16. restore thinking level and service tier:\n - thinking uses persisted `thinking_level_change`, otherwise the configured default clamped to model capability\n - service tier uses persisted `service_tier_change`, otherwise the configured `serviceTier` setting (`\"none\"` becomes unset)\n17. reconnect agent listeners and return `true`\n\n## UI state rebuild after interactive switch\n\n`SelectorController.handleResumeSession` performs UI reset around `switchSession`:\n\n- stop loading animation\n- clear status container\n- clear pending-message UI and pending tool map\n- reset streaming component/message references\n- call `session.switchSession(...)`\n- clear chat container and rerender from session context (`renderInitialMessages`)\n- reload todos from new session artifacts\n- show `Resumed session`\n\nSo visible conversation/todo state is rebuilt from the new session file.\n\n## Startup resume vs in-session switch\n\n### Startup resume (`--continue`, `--resume`, direct open)\n\n- Session file is chosen before `createAgentSession(...)`.\n- `sdk.ts` builds `existingSession = sessionManager.buildSessionContext()`.\n- Agent messages are restored once during session creation.\n- Model/thinking are selected during creation (including restore/fallback logic).\n- Interactive mode then runs `#restoreModeFromSession()` to re-enter persisted mode state (currently plan/plan_paused).\n\n### In-session switch (`/resume`-style selector path)\n\n- Uses `AgentSession.switchSession(...)` on an already-running `AgentSession`.\n- Messages/model/thinking are rebuilt immediately in place.\n- Hook `session_before_switch`/`session_switch` events are emitted.\n- UI chat/todos are refreshed.\n- No dedicated post-switch mode restore call is made in selector flow; mode re-entry behavior is not symmetric with startup `#restoreModeFromSession()`.\n\n## Failure and edge-case behavior\n\n### Cancellation paths\n\n- CLI picker cancel -> returns `null`; bare resume exits silently without writes.\n- Interactive picker cancel -> editor restored, no session change.\n- Hook cancellation (`session_before_switch`) -> `switchSession()` returns `false`.\n\n### Empty list paths\n\n- CLI `--resume` (no value): empty list prints `No sessions found` and exits.\n- Interactive selector: empty list renders message and remains cancellable.\n\n### Missing/invalid target session file\n\nWhen opening/switching to a specific path (`setSessionFile`):\n\n- ENOENT -> treated as empty -> new session initialized at that exact path and persisted.\n- malformed/invalid header (or effectively unreadable parsed entries) -> treated as empty -> new session initialized and persisted.\n\nThis is recovery behavior, not hard failure.\n\n### Hard failures\n\nSwitch/open can still throw on true I/O failures (permission errors, rewrite failures, etc.), which propagate to callers.\n\n### ID prefix matching caveats\n\n- ID matching uses `startsWith` and takes first match in sorted list.\n- No ambiguity UI if multiple sessions share prefix.\n- `SessionManager.list(...)` excludes sessions with zero messages, so those sessions are not resumable via ID match/list picker.\n",
|
|
98
|
+
"session-operations-export-share-fork-resume.md": "# Session Operations: export, dump, share, fork, resume/continue\n\nThis document describes operator-visible behavior for session export/share/fork/resume operations as currently implemented.\n\n## Implementation files\n\n- [`../src/modes/controllers/command-controller.ts`](../packages/coding-agent/src/modes/controllers/command-controller.ts)\n- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)\n- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)\n- [`../src/session-import/`](../packages/coding-agent/src/session-import/)\n- [`../src/export/html/index.ts`](../packages/coding-agent/src/export/html/index.ts)\n- [`../src/export/custom-share.ts`](../packages/coding-agent/src/export/custom-share.ts)\n- [`../src/main.ts`](../packages/coding-agent/src/main.ts)\n\n## Operation matrix\n\n| Operation | Entry path | Session mutation | Session file creation/switch | Output artifact |\n| --------------------------------------- | ------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---- |\n| `/dump` | Interactive slash command | No | No | Clipboard text |\n| `/export [path]` | Interactive slash command | No | No | HTML file |\n| `--export <session.jsonl> [outputPath]` | CLI startup fast-path | No runtime session mutation | No active session; reads target file | HTML file |\n| `/share` | Interactive slash command | No | No | Temp HTML + share URL/gist |\n| `/star` | Interactive/headless slash command | Yes (append-only header metadata) | No; marks only the active session | None |\n| `/unstar` | Interactive/headless slash command | Yes (append-only header metadata) | No; clears the active session's star | None |\n| `/fork` | Interactive slash command | Yes (active session identity changes) | Creates new session file and switches current session to it (persistent mode only) | Copies artifact directory to new session namespace when present |\n| `--fork <id | path>` | CLI startup | Yes after session creation | Creates a new session fork from the selected source into current cwd/session dir | None |\n| `/import-session <transcript-file> [--provider codex\\|claude]` | Interactive or trusted local startup command | No active-session mutation | Creates one independently resumable native session file | Bounded quarantine digest proof and provenance |\n| `/resume` | Interactive slash command | Yes (active in-memory state replaced) | Switches to selected existing session file | None |\n| `--resume` | CLI startup (picker) | Yes after session creation | Opens selected existing session file | None |\n| `--resume <id | path>` | CLI startup | Yes after session creation | Opens existing session; cross-project case can fork into current project | None |\n| `--continue` | CLI startup | Yes after session creation | Opens terminal breadcrumb or most-recent session; creates new one if none exists | None |\n\nStars are per-transcript discovery metadata and require a persistent session; `/star` and `/unstar` reject `--no-session` runtimes. New sessions and forks start unstarred even when their parent is starred. Stars affect resume-picker and read-only dashboard ordering only; `--continue`, retention pruning, and explicit deletion keep their existing behavior.\n\n## Import external sessions\n\n`/import-session <transcript-file> [--provider codex|claude]` imports one explicit Codex CLI rollout transcript, Claude Code transcript, or claude.ai conversation export. Format detection is content-based; `--provider` narrows detection and fails closed on a mismatch. The command never scans private live process state or provider history directories.\n\nThe importer reconstructs user/assistant context and bounded tool evidence in a fresh native session. Unsupported or malformed records are never silently dropped: aggregate counts and at most 512 full-record SHA-256 quarantine proofs are retained in provenance. The source basename, provider/format, source/session identifier when available, exact source digest and byte count, converter/sanitizer versions, mapping/redaction counts, and bounded-context state are persisted. Raw provider archives are never copied into the session store.\n\nThe source is opened once with no-follow semantics and read through that retained descriptor; device, inode, link count, size, mtime, and ctime must remain exact through the complete read. Regular hard-linked exports are accepted without weakening managed-session storage, whose files remain single-linked. Secret-bearing values, Authorization/Cookie headers, terminal escapes, C0/C1 controls, bidi/zero-width controls, tool labels, IDs, titles, cwd metadata, and source diagnostics are sanitized before display or persistence.\n\nEach invocation creates one new session, verifies that it reopens with the same captured destination authority and reconstructs continuable history, then releases that authority. Imports are refused while the current session is streaming. Imported sessions do not replace the active session automatically; select the new session with `/resume`.\n\nThe command is available only on Linux in the interactive TUI and trusted local startup command path. It is neither advertised nor dispatched over ACP or remote-control transports.\n## Export and dump\n\n### `/export [outputPath]` (interactive)\n\nFlow:\n\n1. `InputController` routes `/export...` to `CommandController.handleExportCommand`.\n2. The command splits on whitespace and uses only the first argument after `/export` as `outputPath`.\n3. `AgentSession.exportToHtml()` calls `exportSessionToHtml(sessionManager, state, { outputPath, themeName })`.\n4. On success, UI shows path and opens the file in browser.\n\nBehavior details:\n\n- `--copy`, `clipboard`, and `copy` arguments are explicitly rejected with a warning to use `/dump`.\n- Export embeds session header/entries/leaf plus current `systemPrompt` and tool descriptions from agent state.\n- No session entries are appended during export.\n\nCaveat:\n\n- Argument parsing is whitespace-based (`text.split(/\\s+/)`), so quoted paths with spaces are not preserved as a single path by this command path.\n\n### `--export <inputSessionFile> [outputPath]` (CLI)\n\nFlow in `main.ts`:\n\n1. Handled early (before interactive/session startup).\n2. Calls `exportFromFile(inputPath, outputPath?)`.\n3. `SessionManager.open(inputPath)` loads entries, then HTML is generated and written.\n4. Process prints `Exported to: ...` and exits.\n\nBehavior details:\n\n- Missing input file surfaces as `File not found: <path>`.\n- This path does not create an `AgentSession` and does not mutate any running session.\n\n### `/dump` (interactive clipboard export)\n\nFlow:\n\n1. `CommandController.handleDumpCommand()` calls `session.formatSessionAsText()`.\n2. If empty string, reports `No messages to dump yet.`\n3. Otherwise copies to clipboard via native `copyToClipboard`.\n\nDump content includes:\n\n- System prompt\n- Active model/thinking level\n- Tool definitions + parameters\n- User/assistant messages\n- Thinking blocks and tool calls\n- Tool results and execution blocks (except `excludeFromContext` bash/python entries)\n- Custom/hook/file mention/branch summary/compaction summary entries\n\nNo session persistence changes are made by dumping.\n\n## Share\n\n`/share` is interactive-only and always starts by exporting current session to a temp HTML file.\n\n### Phase 1: temp export\n\n- Temp file path: `${os.tmpdir()}/${Snowflake.next()}.html`\n- Uses `session.exportToHtml(tmpFile)`\n- If export fails (notably in-memory sessions), share ends with error.\n\n### Phase 2: custom share handler (if present)\n\n`loadCustomShare()` checks `~/.gjc/agent` for first existing candidate:\n\n- `share.ts`\n- `share.js`\n- `share.mjs`\n\nRequirements:\n\n- Module must default-export a function `(htmlPath) => Promise<CustomShareResult | string | undefined>`.\n\nIf present and valid:\n\n- UI enters `Sharing...` loader state.\n- Handler result interpretation:\n - string => treated as URL, shown and opened\n - object => `url` and/or `message` shown; `url` opened\n - `undefined`/falsy => generic `Session shared`\n- Temp file is removed after completion.\n\nCritical fallback behavior:\n\n- If custom handler exists but loading fails, command errors and returns.\n- If custom handler executes and throws, command errors and returns.\n- In both failure cases, it **does not** fall back to GitHub gist.\n- Gist fallback happens only when no custom share script exists.\n\n### Phase 3: default gist fallback\n\nOnly when no custom share handler is found:\n\n1. Validates `gh auth status`.\n2. Shows `Creating gist...` loader.\n3. Runs `gh gist create --public=false <tmpFile>`.\n4. Parses gist URL, derives gist id, builds preview URL `https://gistpreview.github.io/?<id>`.\n5. Shows both preview and gist URLs; opens preview.\n\nCancellation/abort semantics in share:\n\n- Loader has `onAbort` hook that restores editor UI and reports `Share cancelled`.\n- The underlying `gh gist create` command is not passed an abort signal in this code path; cancellation is UI-level and checked after command returns.\n\n## Fork\n\nInteractive `/fork` creates a new session from the current one and switches the active session identity.\n\n### Preconditions and immediate guards\n\n- If agent is streaming, `/fork` is rejected with warning.\n- UI status/loading indicators are cleared before operation.\n\n### Session-level flow\n\n`AgentSession.fork()`:\n\n1. Emits `session_before_switch` with `reason: \"fork\"` (cancellable).\n2. Flushes pending writes.\n3. Calls `SessionManager.fork()`.\n4. Copies artifacts directory from old session namespace to new namespace (best-effort; non-ENOENT copy failures are logged, not fatal).\n5. Updates `agent.sessionId`.\n6. Emits `session_switch` with `reason: \"fork\"`.\n\n`SessionManager.fork()` behavior:\n\n- Requires persistent mode and existing session file.\n- Creates new session id and new JSONL file path.\n- Rewrites header with:\n - new `id`\n - new timestamp\n - `cwd` unchanged\n - `parentSession` set to previous session id\n- Keeps all non-header entries unchanged in the new file.\n\n### Non-persistent behavior\n\n- In-memory session manager returns `undefined` from `fork()`.\n- `AgentSession.fork()` returns `false`.\n- UI reports `Fork failed (session not persisted or cancelled)`.\n\n### CLI `--fork <id|path>`\n\nStartup `--fork` is resolved before normal session creation:\n\n1. `--fork` is rejected with `--no-session`.\n2. Path-like values (`/`, `\\`, or `.jsonl`) call `SessionManager.forkFrom(path, cwd, sessionDir)`.\n3. Other values resolve like resumable session ids via current scope and then global search when allowed.\n4. The forked file is created in the current cwd/session-dir scope and becomes the active session manager for startup.\n\n### Managed directory migration during session operations\n\nDefault persistent creates and forks write only to the managed v2 workspace scope. A resume/list operation may surface a validated legacy candidate for the same canonical workspace identity; with `session.directoryMigration: \"copy-retain\"`, the migration path copies it into v2 and retains the source. It never replaces an existing destination, and a migration tombstone prevents completed/retired legacy work from being retried as fresh work. `disabled` leaves legacy data in place.\n\nThe migration path does not delete legacy sessions or artifacts automatically. It fails closed on conflicting bindings, changed source identity, unsafe artifact trees, or unavailable owner-only path security; it does not claim authentication or protection against hostile concurrent filesystem races. Explicit `--session-dir` remains an operator-selected override.\n\n## Resume and continue\n\n## Interactive `/resume`\n\nFlow:\n\n1. Opens session selector populated via `SessionManager.list(currentCwd, currentSessionDir)`.\n2. On selection, `SelectorController.handleResumeSession(sessionPath)` calls `session.switchSession(sessionPath)`.\n3. UI clears/rebuilds chat and todos, then reports `Resumed session`.\n\nNotes:\n\n- This picker only lists sessions in the current session directory scope.\n- It does not use global cross-project search.\n\n## CLI `--resume`\n\n### `--resume` (no value)\n\n- `main.ts` lists sessions for current cwd/sessionDir and opens picker.\n- Selected path is opened with `SessionManager.open(selectedPath)` before session creation.\n\n### `--resume <value>`\n\n`createSessionManager()` resolution order:\n\n1. If value looks like path (`/`, `\\`, or `.jsonl`), open directly.\n2. Else treat as id prefix:\n - search current scope (`SessionManager.list(cwd, sessionDir)`)\n - if not found and no explicit `sessionDir`, search global (`SessionManager.listAll()`)\n\nCross-project id match behavior:\n\n- If matched session cwd differs from current cwd, CLI asks:\n - `Session found in different project ... Fork into current directory? [y/N]`\n- On yes: `SessionManager.forkFrom(match.path, cwd, sessionDir)` creates a new local forked file.\n- On no/non-TTY default: command errors.\n\n## CLI `--continue`\n\n`SessionManager.continueRecent(cwd, sessionDir)`:\n\n1. Resolves session dir for current cwd.\n2. Reads terminal-scoped breadcrumb first.\n3. Falls back to most recently modified session file.\n4. Opens found session; if none exists, creates new session.\n\nThis is startup-only behavior; there is no interactive `/continue` slash command.\n\n## How session switching actually mutates runtime state\n\n`AgentSession.switchSession(sessionPath)` does the runtime transition used by resume-like operations:\n\n1. Emit `session_before_switch` with `reason: \"resume\"` and `targetSessionFile` (cancellable).\n2. Disconnect agent event subscription and abort in-flight work.\n3. Clear queued steering/follow-up/next-turn messages.\n4. Flush current session manager writes.\n5. `sessionManager.setSessionFile(sessionPath)` and update `agent.sessionId`.\n6. Build session context from loaded entries.\n7. Emit `session_switch` with `reason: \"resume\"`.\n8. Replace agent messages from context.\n9. Restore model (if available in current registry).\n10. Restore or initialize thinking level.\n11. Reconnect agent event subscription.\n\nNo new session file is created by `switchSession()` itself.\n\n## Event emissions and cancellation points\n\n### Switch/fork lifecycle hooks\n\nFor `newSession`, `fork`, and `switchSession`:\n\n- Before event: `session_before_switch`\n - reasons: `new`, `fork`, `resume`\n - cancellable by returning `{ cancel: true }`\n- After event: `session_switch`\n - same reason set\n - includes `previousSessionFile`\n\n`ExtensionRunner.emit()` returns early on the first cancelling before-event result.\n\n### Custom tool `onSession` behavior\n\nSDK bridges extension session events to custom tool `onSession` callbacks:\n\n- `session_switch` -> `onSession({ reason: \"switch\", previousSessionFile })`\n- `session_branch` -> `reason: \"branch\"`\n- `session_start` -> `reason: \"start\"`\n- `session_tree` -> `reason: \"tree\"`\n- `session_shutdown` -> `reason: \"shutdown\"`\n\nThese callbacks are observational; they do not cancel switch/fork.\n\n### Other cancellation surfaces relevant to this doc\n\n- `/fork` is blocked while streaming (user must wait/abort current response first).\n- `/resume` selector can be cancelled by user closing selector.\n- Cross-project `--resume <id>` can be cancelled by declining fork prompt.\n- `/share` has UI abort path (`Share cancelled`) for gist flow; it does not wire process-kill semantics for `gh gist create` in this code path.\n\n## Non-persistent (in-memory) session behavior\n\nWhen session manager is created with `SessionManager.inMemory()` (`--no-session`):\n\n- Session file path is absent.\n- `/export` and `/share` fail with `Cannot export in-memory session to HTML` (propagated to command error UI).\n- `/fork` fails because `SessionManager.fork()` requires persistence.\n- `/dump` still works because it serializes in-memory agent state.\n- CLI resume/continue semantics are bypassed if `--no-session` is set, because manager creation returns in-memory immediately.\n\n## Known implementation caveats (as of current code)\n\n- `SelectorController.handleResumeSession()` does not check the boolean result from `session.switchSession(...)`; a hook-cancelled switch can still proceed through UI \"Resumed session\" repaint/status path.\n- `/share` custom-share failures do not degrade to default gist fallback; they terminate the command with error.\n- `/export` argument tokenization is simplistic and does not preserve quoted paths with spaces.\n",
|
|
99
|
+
"session-switching-and-recent-listing.md": "# Session switching and recent session listing\n\nThis document describes how coding-agent discovers recent sessions, resolves `--resume` targets, presents session pickers, and switches the active runtime session.\n\nIt focuses on current implementation behavior, including fallback paths and caveats.\n\n## Implementation files\n\n- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)\n- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)\n- [`../src/cli/session-picker.ts`](../packages/coding-agent/src/cli/session-picker.ts)\n- [`../src/modes/components/session-selector.ts`](../packages/coding-agent/src/modes/components/session-selector.ts)\n- [`../src/modes/controllers/selector-controller.ts`](../packages/coding-agent/src/modes/controllers/selector-controller.ts)\n- [`../src/main.ts`](../packages/coding-agent/src/main.ts)\n- [`../src/sdk/session.ts`](../packages/coding-agent/src/sdk/session.ts)\n- [`../src/modes/interactive-mode.ts`](../packages/coding-agent/src/modes/interactive-mode.ts)\n- [`../src/modes/utils/ui-helpers.ts`](../packages/coding-agent/src/modes/utils/ui-helpers.ts)\n\n## Recent-session discovery\n\n### Directory scope\n\nThe default managed scope is `~/.gjc/agent/sessions/v2-<identity-digest>/`, where the digest is derived from the native canonical workspace identity rather than a path-string substitution. It is collision-resistant, but the digest is not a public injective identity or an authentication credential. POSIX aliases and supported Windows local aliases for the same directory resolve to the same scope; UNC/network workspaces are rejected as unsupported.\n\n`SessionManager.list(cwd, sessionDir?)` reads the selected directory unless an explicit `sessionDir` is provided. The public readonly SDK API is `resolveManagedSessionScope()` followed by `listManagedSessionCandidates()` from `@gajae-code/coding-agent/sdk`; both are versioned by `SESSION_DIRECTORY_API_VERSION` (currently `1`). The resolver/listing API creates, migrates, and deletes nothing. Listing reports validated v2 and legacy candidates, invalid candidates, and a foreign count instead of treating arbitrary files as owned sessions.\n\nDefault writes are v2-only. Legacy discovery/migration is lazy, validates identity before use, and follows `session.directoryMigration` (`copy-retain` by default; `disabled` to opt out); no automatic legacy cleanup occurs.\n\n### Two listing paths with different payloads\n\nThere are two different listing pipelines:\n\n1. `getRecentSessions(sessionDir, limit)` (welcome/summary view)\n - Reads a bounded 4KB prefix plus reverse-scanned v4/v5 header patches from each file.\n - Parses header metadata, applicable trailing patches, and the earliest user text preview.\n - Returns lightweight `RecentSessionInfo` with lazy `name` and `timeAgo` getters.\n - Sorts by file `mtime` descending.\n\n2. `SessionManager.list(...)` / `SessionManager.listAll()` (resume pickers and ID matching)\n - Reads a bounded 4KB prefix, then reverse-scans strict `header_patch` records in 64KB chunks. Legacy transcripts request cwd/title only; `starredPatchVersion: 1` transcripts also request the latest star state.\n - Recent patches near EOF stay cheap. When a requested field is still missing, the scan continues past the historical 16KB window so buried canonical metadata remains listable without a full sequential JSONL parse of multi-MB message bodies.\n - A transcript that never emitted a requested `header_patch` cannot be recognized as such without reaching BOF, so its whole file is walked. Legacy rename/move patches stop once cwd/title resolve. Star-capable writers checkpoint current star state beside later cwd/title patches, keeping those metadata groups bounded without making read-only discovery mutate transcripts.\n - Builds `SessionInfo` objects from that projection plus prefix preview extraction.\n - Drops sessions with zero `message` entries and sorts by `modified` descending; resume selectors and the sessions dashboard then stable-partition starred sessions ahead of unstarred sessions without changing ID-prefix resolution order.\n\n### Metadata fallback behavior\n\nFor recent summaries (`RecentSessionInfo`):\n\n- display name preference: `header.title` -> first user prompt -> `header.id` -> filename\n- name is truncated to 40 chars for compact displays\n- control characters/newlines are stripped/sanitized from title-derived names\n\nFor `SessionInfo` list entries:\n\n- `title` is `header.title` or latest compaction `shortSummary`\n- `starred` defaults to `false` for sessions without star metadata\n- `firstMessage` is first user message text or `\"(no messages)\"`\n\n## `--continue` resolution and terminal breadcrumb preference\n\n`SessionManager.continueRecent(cwd, sessionDir?)` resolves the target in this order:\n\n1. Read terminal-scoped breadcrumb (`~/.gjc/agent/terminal-sessions/<terminal-id>`)\n2. Validate breadcrumb:\n - current terminal can be identified\n - breadcrumb cwd matches current cwd (resolved path compare)\n - referenced file still exists\n3. If breadcrumb is invalid/missing, fall back to newest file by mtime in the session dir (`findMostRecentSession`)\n4. If none found, create a new session\n\nTerminal ID derivation prefers TTY path and falls back to env-based identifiers (`KITTY_WINDOW_ID`, `TMUX_PANE`, `TERM_SESSION_ID`, `WT_SESSION`).\n\nBreadcrumb writes are best-effort and non-fatal.\n\n## Startup-time resume target resolution (`main.ts`)\n\n### `--resume <value>`\n\n`createSessionManager(...)` handles string-valued `--resume` in two modes:\n\n1. Path-like value (contains `/`, `\\\\`, or ends with `.jsonl`)\n - direct `SessionManager.open(sessionArg, parsed.sessionDir)`\n\n2. ID prefix value\n - find match in `SessionManager.list(cwd, sessionDir)` by `id.startsWith(sessionArg)`\n - if no local match and `sessionDir` is not forced, try `SessionManager.listAll()`\n - first match is used (no ambiguity prompt)\n\nCross-project match behavior:\n\n- if matched session cwd differs from current cwd, CLI prompts whether to fork into current project\n- yes -> `SessionManager.forkFrom(...)`\n- no -> throws error (`Session \"...\" is in another project (...)`)\n\nNo match -> throws error (`Session \"...\" not found.`).\n\n### `--resume` (no value)\n\nHandled after initial session-manager construction:\n\n1. list local candidates through the bounded read-only resume-picker path\n2. if empty: print `No sessions found` and exit early\n3. open the TUI picker; cancellation returns silently and exits without writes\n4. inspect the selected transcript read-only and confirm resumable tail state when required\n5. strictly open the approved identity, rechecking ownership before any replay-sanitization persistence\n6. publish the terminal breadcrumb only after strict-open sanitation succeeds, then continue startup from the opened manager\n### `--continue`\n\nUses `SessionManager.continueRecent(...)` directly (breadcrumb-first behavior above).\n\n## Picker-based selection internals\n\n## CLI picker (`src/cli/session-picker.ts`)\n\n`selectSession(sessions)` creates a standalone TUI with `SessionSelectorComponent` and resolves exactly once:\n\n- selection -> resolves selected path\n- cancel (Esc) -> resolves `null`\n- hard exit (Ctrl+C path) -> stops TUI and `process.exit(0)`\n\n## Interactive in-session picker (`SelectorController.showSessionSelector`)\n\nFlow:\n\n1. fetch sessions from the current session directory via `SessionManager.listForResumePickerReadOnly(currentCwd, currentSessionDir)`\n2. mount `SessionSelectorComponent` in editor area using `showSelector(...)`\n3. callbacks:\n - select -> close selector and call `handleResumeSession(sessionPath)`\n - cancel -> restore editor and rerender\n - exit -> `ctx.shutdown()`\n\n## Session selector component behavior\n\n`SessionList` supports:\n\n- arrow/page navigation\n- Enter to select\n- Esc to cancel\n- Ctrl+C to exit\n- fuzzy search across session id/title/cwd/first message/all messages/path\n- starred sessions are marked with `★` and appear before unstarred sessions\n\nEmpty-list render behavior:\n\n- renders a message instead of crashing\n- Enter on empty does nothing (no callback)\n- Esc/Ctrl+C still work\n\nCaveat: UI text says `Press Tab to view all`, but this component currently has no Tab handler and current wiring only lists current-scope sessions.\n\n`/star` and `/unstar` update only the active session. The read-only `/sessions` dashboard displays the same star marker but never mutates session metadata. Starring is a discovery aid, not a retention or deletion guard.\n\n## Runtime switch execution (`AgentSession.switchSession`)\n\n`switchSession(sessionPath)` is the core in-process switch path.\n\nLifecycle/state transition:\n\n1. capture `previousSessionFile`\n2. emit `session_before_switch` hook event (`reason: \"resume\"`, cancellable)\n3. if canceled -> return `false` with no switch\n4. disconnect from current agent event stream\n5. abort active generation/tool flow\n6. clear queued steering/follow-up/next-turn message buffers\n7. flush session writer (`sessionManager.flush()`) to persist pending writes\n8. `sessionManager.setSessionFile(sessionPath)`\n - updates session file pointer\n - writes terminal breadcrumb\n - loads entries / migrates / blob-resolves / reindexes\n - if missing/invalid file data: initializes a new session at that path and rewrites header\n9. update `agent.sessionId`\n10. rebuild display context via `buildDisplaySessionContext()`\n11. restore persisted/discovered MCP tool selections and rebuild active tools/system prompt when discovery is enabled\n12. emit `session_switch` hook event (`reason: \"resume\"`, `previousSessionFile`)\n13. replace agent messages with rebuilt context and sync todos\n14. close provider sessions when switching to a different session or when same-session reload changed replay messages\n15. restore default model from `sessionContext.models.default` if available and present in model registry\n16. restore thinking level and service tier:\n - thinking uses persisted `thinking_level_change`, otherwise the configured default clamped to model capability\n - service tier uses persisted `service_tier_change`, otherwise the configured `serviceTier` setting (`\"none\"` becomes unset)\n17. reconnect agent listeners and return `true`\n\n## UI state rebuild after interactive switch\n\n`SelectorController.handleResumeSession` performs UI reset around `switchSession`:\n\n- stop loading animation\n- clear status container\n- clear pending-message UI and pending tool map\n- reset streaming component/message references\n- call `session.switchSession(...)`\n- clear chat container and rerender from session context (`renderInitialMessages`)\n- reload todos from new session artifacts\n- show `Resumed session`\n\nSo visible conversation/todo state is rebuilt from the new session file.\n\n## Startup resume vs in-session switch\n\n### Startup resume (`--continue`, `--resume`, direct open)\n\n- Session file is chosen before `createAgentSession(...)`.\n- `sdk.ts` builds `existingSession = sessionManager.buildSessionContext()`.\n- Agent messages are restored once during session creation.\n- Model/thinking are selected during creation (including restore/fallback logic).\n- Interactive mode then runs `#restoreModeFromSession()` to re-enter persisted mode state (currently plan/plan_paused).\n\n### In-session switch (`/resume`-style selector path)\n\n- Uses `AgentSession.switchSession(...)` on an already-running `AgentSession`.\n- Messages/model/thinking are rebuilt immediately in place.\n- Hook `session_before_switch`/`session_switch` events are emitted.\n- UI chat/todos are refreshed.\n- No dedicated post-switch mode restore call is made in selector flow; mode re-entry behavior is not symmetric with startup `#restoreModeFromSession()`.\n\n## Failure and edge-case behavior\n\n### Cancellation paths\n\n- CLI picker cancel -> returns `null`; bare resume exits silently without writes.\n- Interactive picker cancel -> editor restored, no session change.\n- Hook cancellation (`session_before_switch`) -> `switchSession()` returns `false`.\n\n### Empty list paths\n\n- CLI `--resume` (no value): empty list prints `No sessions found` and exits.\n- Interactive selector: empty list renders message and remains cancellable.\n\n### Missing/invalid target session file\n\nWhen opening/switching to a specific path (`setSessionFile`):\n\n- ENOENT -> treated as empty -> new session initialized at that exact path and persisted.\n- malformed/invalid header (or effectively unreadable parsed entries) -> treated as empty -> new session initialized and persisted.\n\nThis is recovery behavior, not hard failure.\n\n### Hard failures\n\nSwitch/open can still throw on true I/O failures (permission errors, rewrite failures, etc.), which propagate to callers.\n\n### ID prefix matching caveats\n\n- ID matching uses `startsWith` and takes first match in sorted list.\n- No ambiguity UI if multiple sessions share prefix.\n- `SessionManager.list(...)` excludes sessions with zero messages, so those sessions are not resumable via ID match/list picker.\n",
|
|
100
100
|
"session-tree-plan.md": "# Session tree architecture (current)\n\nReference: [session.md](../docs/session.md)\n\nThis document describes how session tree navigation works today: in-memory tree model, leaf movement rules, branching behavior, and extension/event integration.\n\n## What this subsystem is\n\nThe session is stored as an append-only entry log, but runtime behavior is tree-based:\n\n- Every non-header entry has `id` and `parentId`.\n- The active position is `leafId` in `SessionManager`.\n- Appending an entry always creates a child of the current leaf.\n- Branching does **not** rewrite history; it only changes where the leaf points before the next append.\n\nKey files:\n\n- `src/session/session-manager.ts` — tree data model, traversal, leaf movement, branch/session extraction\n- `src/session/agent-session.ts` — `/tree` navigation flow, summarization, hook/event emission\n- `src/modes/components/tree-selector.ts` — interactive tree UI behavior and filtering\n- `src/modes/controllers/selector-controller.ts` — selector orchestration for `/tree` and `/branch`\n- `src/modes/controllers/input-controller.ts` — command routing (`/tree`, `/branch`, double-escape behavior)\n- `src/session/messages.ts` — conversion of `branch_summary`, `compaction`, and `custom_message` entries into LLM context messages\n\n## Tree data model in `SessionManager`\n\nRuntime indices:\n\n- `#byId: Map<string, SessionEntry>` — fast lookup for any entry\n- `#leafId: string | null` — current position in the tree\n- `#labelsById: Map<string, string>` — resolved labels by target entry id\n\nTree APIs:\n\n- `getBranch(fromId?)` walks parent links to root and returns root→node path\n- `getTree()` returns `SessionTreeNode[]` (`entry`, `children`, `label`)\n - parent links become children arrays\n - entries with missing parents are treated as roots\n - children are sorted oldest→newest by timestamp\n- `getChildren(parentId)` returns direct children\n- `getLabel(id)` resolves current label from `labelsById`\n\n`getTree()` is a runtime projection; persistence remains append-only JSONL entries.\n\n## Leaf movement semantics\n\nThere are three leaf movement primitives:\n\n1. `branch(entryId)`\n - Validates entry exists\n - Sets `leafId = entryId`\n - No new entry is written\n\n2. `resetLeaf()`\n - Sets `leafId = null`\n - Next append creates a new root entry (`parentId = null`)\n\n3. `branchWithSummary(branchFromId, summary, details?, fromExtension?)`\n - Accepts `branchFromId: string | null`\n - Sets `leafId = branchFromId`\n - Appends a `branch_summary` entry as child of that leaf\n - When `branchFromId` is `null`, `fromId` is persisted as `\"root\"`\n\n## `/tree` navigation behavior (same session file)\n\n`AgentSession.navigateTree()` is navigation, not file forking.\n\nFlow:\n\n1. Validate target and compute abandoned path (`collectEntriesForBranchSummary`)\n2. Emit `session_before_tree` with `TreePreparation`\n3. Optionally summarize abandoned entries (hook-provided summary or built-in summarizer)\n4. Compute new leaf target:\n - selecting a **user** message: leaf moves to its parent, and message text is returned for editor prefill\n - selecting a **custom_message**: same rule as user message (leaf = parent, text prefills editor)\n - selecting any other entry: leaf = selected entry id\n5. Apply leaf move:\n - with summary: `branchWithSummary(newLeafId, ...)`\n - without summary and `newLeafId === null`: `resetLeaf()`\n - otherwise: `branch(newLeafId)`\n6. Rebuild agent context from new leaf and emit `session_tree`\n\nImportant: summary entries are attached at the **new navigation position**, not on the abandoned branch tail.\n\n## `/branch` behavior (new session file)\n\n`/branch` and `/tree` are intentionally different:\n\n- `/tree` navigates within the current session file.\n- `/branch` creates a new session branch file (or in-memory replacement for non-persistent mode).\n\nUser-facing `/branch` flow (`SelectorController.showUserMessageSelector` → `AgentSession.branch`):\n\n- Branch source must be a **user message**.\n- Selected user text is extracted for editor prefill.\n- If selected user message is root (`parentId === null`): start a new session via `newSession({ parentSession: previousSessionFile })`.\n- Otherwise: `createBranchedSession(selectedEntry.parentId)` to fork history up to the selected prompt boundary.\n\n`SessionManager.createBranchedSession(leafId)` specifics:\n\n- Builds root→leaf path via `getBranch(leafId)`; throws if missing.\n- Excludes existing `label` entries from copied path.\n- Rebuilds fresh label entries from resolved `labelsById` for entries that remain in path.\n- Persistent mode: writes new JSONL file and switches manager to it; returns new file path.\n- In-memory mode: replaces in-memory entries; returns `undefined`.\n\n## Context reconstruction and summary/custom integration\n\n`buildSessionContext()` (in `session-manager.ts`) resolves the active root→leaf path and builds effective LLM context state:\n\n- Tracks latest thinking/model/service-tier/mode/TTSR/MCP-selection state on path.\n- Handles latest compaction on path:\n - emits compaction summary first\n - replays kept messages from `firstKeptEntryId` to compaction point\n - then replays post-compaction messages\n- Includes `branch_summary` and `custom_message` entries as `AgentMessage` objects.\n\n`session/messages.ts` then maps these message types for model input:\n\n- `branchSummary` and `compactionSummary` become user-role templated context messages\n- `custom`/`hookMessage` become user-role content messages\n\nSo tree movement changes context by changing the active leaf path, not by mutating old entries.\n\n## Labels and tree UI behavior\n\nLabel persistence:\n\n- `appendLabelChange(targetId, label?)` writes `label` entries on the current leaf chain.\n- `labelsById` is updated immediately (set or delete).\n- `getTree()` resolves current label onto each returned node.\n\nTree selector behavior (`tree-selector.ts`):\n\n- Flattens tree for navigation, keeps active-path highlighting, and prioritizes displaying the active branch first.\n- Supports filter modes: `default`, `no-tools`, `user-only`, `labeled-only`, `all`.\n- Supports free-text search over rendered semantic content.\n- `Shift+L` opens inline label editing and writes via `appendLabelChange`.\n\nCommand routing:\n\n- `/tree` always opens tree selector.\n- `/branch` opens user-message selector unless `doubleEscapeAction=tree`, in which case it also uses tree selector UX.\n\n## Extension and hook touchpoints for tree operations\n\nCommand-time extension API (`ExtensionCommandContext`):\n\n- `branch(entryId)` — create branched session file\n- `navigateTree(targetId, { summarize? })` — move within current tree/file\n\nEvents around tree navigation:\n\n- `session_before_tree`\n - receives `TreePreparation`:\n - `targetId`\n - `oldLeafId`\n - `commonAncestorId`\n - `entriesToSummarize`\n - `userWantsSummary`\n - may cancel navigation\n - may provide summary payload used instead of built-in summarizer\n - receives abort `signal` (Escape cancellation path)\n- `session_tree`\n - emits `newLeafId`, `oldLeafId`\n - includes `summaryEntry` when a summary was created\n - `fromExtension` indicates summary origin\n\nAdjacent but related lifecycle hooks:\n\n- `session_before_branch` / `session_branch` for `/branch` flow\n- `session_before_compact`, `session.compacting`, `session_compact` for compaction entries that later affect tree-context reconstruction\n\n## Real constraints and edge conditions\n\n- `branch()` cannot target `null`; use `resetLeaf()` for root-before-first-entry state.\n- `branchWithSummary()` supports `null` target and records `fromId: \"root\"`.\n- Selecting current leaf in tree selector is a no-op.\n- Summarization requires an active model; if absent, summarize navigation fails fast.\n- If summarization is aborted, navigation is cancelled and leaf is unchanged.\n- In-memory sessions never return a branch file path from `createBranchedSession`.\n- Tree context reconstruction includes service-tier and MCP tool-selection state, but those entries do not become LLM messages.\n\n## Plan approval session naming\n\nWhen a user approves a plan from plan mode (`InteractiveMode.#approvePlan`), the approval handler seeds the session name from the plan's title so the resulting (fresh or compacted) session does not stay unnamed.\n\nTrigger:\n\n- Plan approval reaches `#approvePlan(...)` with `options.title` populated from the plan-approval details.\n- This runs for every approval choice (`Approve and execute`, `Approve and compact context`, plain `Approve`); the synthetic `plan-approved` prompt is what otherwise bypasses the input-controller's title-generation path.\n\nNaming source:\n\n- The normalized plan title is humanized via `humanizePlanTitle(title)` (`packages/coding-agent/src/plan-mode/approved-plan.ts`):\n - replaces runs of `-`/`_` with a single space\n - trims whitespace\n - capitalizes the first character\n - returns `\"\"` for whitespace-only / separator-only input\n- The humanized name is applied with `sessionManager.setSessionName(name, \"auto\")`. Because `setSessionName` is a no-op when `titleSource === \"user\"`, the seeded name never overrides a name the user already chose (e.g. on the `preserveContext` path where the session continues with prior naming).\n- On successful apply, the terminal title (`setSessionTerminalTitle`) and the editor border color are refreshed to reflect the new name.\n\nExamples (from `humanizePlanTitle`):\n\n- `migrate-mcp-loader` → `Migrate mcp loader`\n- `fix_session_naming` → `Fix session naming`\n- `foo--bar__baz` → `Foo bar baz`\n- `RefactorRouter` → `RefactorRouter` (no separators to expand)\n- `\"\"` / `\"---\"` → `\"\"` (no name applied)\n\n## Legacy compatibility still present\n\nSession migrations still run on load:\n\n- v1→v2 adds `id`/`parentId` and converts compaction index anchor to id anchor\n- v2→v3 migrates legacy `hookMessage` role to `custom`\n\nCurrent runtime behavior is version-3 tree semantics after migration.\n",
|
|
101
|
-
"session.md": "# Session Storage and Entry Model\n\nThis document is the source of truth for how coding-agent sessions are represented, persisted, migrated, and reconstructed at runtime.\n\n## Scope\n\nCovers:\n\n- Session JSONL format and versioning\n- Entry taxonomy and tree semantics (`id`/`parentId` + leaf pointer)\n- Migration/compatibility behavior when loading old or malformed files\n- Context reconstruction (`buildSessionContext`)\n- Persistence guarantees, failure behavior, truncation/blob externalization\n- Storage abstractions (`FileSessionStorage`, `MemorySessionStorage`) and related utilities\n\nDoes not cover `/tree` UI rendering behavior beyond semantics that affect session data.\n\n## Implementation Files\n\n- [`src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)\n- [`src/session/messages.ts`](../packages/coding-agent/src/session/messages.ts)\n- [`src/session/session-storage.ts`](../packages/coding-agent/src/session/session-storage.ts)\n- [`src/session/history-storage.ts`](../packages/coding-agent/src/session/history-storage.ts)\n- [`src/session/blob-store.ts`](../packages/coding-agent/src/session/blob-store.ts)\n\n## On-Disk Layout\n\nDefault managed session file location:\n\n```text\n~/.gjc/agent/sessions/v2-<52-char-base32-sha256>/<timestamp>_<sessionId>.jsonl\n```\n\nThe `v2-…` component is a fixed-width SHA-256/base32 digest of the native canonical workspace identity (identity version 1); it is **not** a reversible or injective user-facing encoding. The binding file `.gjc-managed-session-scope.v2.json` records the canonical identity and digest. Existing bindings must be regular, canonically encoded files that agree with the resolved identity; a mismatch or unsafe path fails closed.\n\nIdentity is platform-specific:\n\n- POSIX paths and supported local aliases that resolve to the same native directory identity share the same v2 scope.\n- On Windows, equivalent supported local path spellings (including drive-letter/case aliases) resolve through the native identity API before the scope is derived.\n- UNC/network workspaces are unsupported and return a `network_unsupported` resolution result; no SMB share is needed or assumed by this design.\n\nThe default managed writer creates new data only in v2 scopes. It never writes new legacy-layout data. `--session-dir` is an explicit storage/lookup override and is not a request to derive the default managed scope.\n\n### Legacy migration and retention\n\nLegacy encoded directories are discovered only after validating each candidate's header and workspace identity. With `session.directoryMigration: \"copy-retain\"` (the default), an eligible legacy session is copied into the v2 scope without replacing an existing destination; the legacy source is retained. Set `session.directoryMigration: \"disabled\"` to leave legacy candidates unmigrated. Migration is lazy and guarded by a managed lock, binding checks, no-follow/owner-only path checks, and source identity validation; conflicts, unsafe artifacts, or changed sources fail rather than guessing.\n\nMigration does not automatically clean up legacy files, copied files, locks, artifacts, or abandoned data. A migration tombstone records a completed/retired source so repeated scans do not reinterpret it as a new migration request; it is not evidence that the old data was deleted. Artifact copying is bounded and rejects symlinks, hard links, excessive depth, file count, or size.\n\n### Security boundary\n\nManaged storage enforces owner-only directory/file security and refuses unsafe symlinks or malformed bindings on the paths it verifies. This is a local storage-integrity boundary, not authentication, authorization, encryption, or a guarantee against a hostile concurrent local actor/race outside the verified operations. Callers must still protect the agent directory and session contents.\n\nOn Linux filesystems where the exact POSIX ACL xattr operation returns `ENOTSUP`/`EOPNOTSUPP`, GJC treats that result only as proof that the filesystem cannot store that ACL attribute. The ACL gate still requires the same opened object to pass effective-owner, exact `0700` directory or `0600` file mode, safe-type, no-follow traversal, and identity/replacement checks. Permission denial, I/O errors, present or malformed ACL data, and unknown results remain failures. Managed descriptors use close-on-exec and are not delegated as authority to subprocesses. This compatibility rule does not change explicit `--session-dir`, macOS ACL, or Windows DACL policy.\n\nBlob store location:\n\n```text\n~/.gjc/agent/blobs/<sha256>\n```\n\nTerminal breadcrumb files are written under:\n\n```text\n~/.gjc/agent/terminal-sessions/<terminal-id>\n```\n\nBreadcrumb content is two lines: original cwd, then session file path. `continueRecent()` prefers this terminal-scoped pointer before scanning most-recent mtime.\n\n## File Format\n\nSession files are JSONL: one JSON object per line.\n\n- Line 1 is always the session header (`type: \"session\"`).\n- Remaining lines are `SessionEntry` values or v4/v5 append-only patch records. `header_patch` records update header metadata and `entry_patch` records replace a message payload when replay metadata is sanitized.\n- Entries and patch records are append-only at runtime; branch navigation moves a pointer (`leafId`) rather than mutating existing entries.\n\n### Header (`SessionHeader`)\n\n```json\n{\n \"type\": \"session\",\n \"version\": 5,\n \"id\": \"1f9d2a6b9c0d1234\",\n \"timestamp\": \"2026-02-16T10:20:30.000Z\",\n \"cwd\": \"/work/pi\",\n \"title\": \"optional session title\",\n \"titleSource\": \"auto\",\n \"parentSession\": \"optional lineage marker\"\n}\n```\n\nNotes:\n\n- `version` is optional in v1 files; absence means v1.\n- `parentSession` is an opaque lineage string. Current code writes either a session id or a session path depending on flow (`fork`, `forkFrom`, `createBranchedSession`, or explicit `newSession({ parentSession })`). Treat as metadata, not a typed foreign key.\n\n### Entry Base (`SessionEntryBase`)\n\nAll non-header entries include:\n\n```json\n{\n \"type\": \"...\",\n \"id\": \"8-char-id\",\n \"parentId\": \"previous-or-branch-parent\",\n \"timestamp\": \"2026-02-16T10:20:30.000Z\"\n}\n```\n\n`parentId` can be `null` for a root entry (first append, or after `resetLeaf()`).\n\n## Entry Taxonomy\n\n`SessionEntry` is the union of:\n\n- `message`\n- `thinking_level_change`\n- `service_tier_change`\n- `compaction`\n- `branch_summary`\n- `custom`\n- `custom_message`\n- `label`\n- `ttsr_injection`\n- `session_init`\n- `mode_change`\n- `mcp_tool_selection`\n- `discovered_builtin_tool_selection`\n\n### `message`\n\nStores an `AgentMessage` directly.\n\n```json\n{\n \"type\": \"message\",\n \"id\": \"a1b2c3d4\",\n \"parentId\": null,\n \"timestamp\": \"2026-02-16T10:21:00.000Z\",\n \"message\": {\n \"role\": \"assistant\",\n \"provider\": \"anthropic\",\n \"model\": \"anthropic-model-sonnet-4-5\",\n \"content\": [{ \"type\": \"text\", \"text\": \"Done.\" }],\n \"usage\": {\n \"input\": 100,\n \"output\": 20,\n \"cacheRead\": 0,\n \"cacheWrite\": 0,\n \"cost\": {\n \"input\": 0,\n \"output\": 0,\n \"cacheRead\": 0,\n \"cacheWrite\": 0,\n \"total\": 0\n }\n },\n \"timestamp\": 1760000000000\n }\n}\n```\n\n### `model_change`\n\n```json\n{\n \"type\": \"model_change\",\n \"id\": \"b1c2d3e4\",\n \"parentId\": \"a1b2c3d4\",\n \"timestamp\": \"2026-02-16T10:21:30.000Z\",\n \"model\": \"openai/gpt-4o\",\n \"role\": \"default\"\n}\n```\n\n`role` is optional; missing is treated as `default` in context reconstruction.\n\n### `service_tier_change`\n\n```json\n{\n \"type\": \"service_tier_change\",\n \"id\": \"c1d2e3f4\",\n \"parentId\": \"b1c2d3e4\",\n \"timestamp\": \"2026-02-16T10:21:45.000Z\",\n \"serviceTier\": \"flex\"\n}\n```\n\n`serviceTier` can also be `null`.\n\n### `thinking_level_change`\n\n```json\n{\n \"type\": \"thinking_level_change\",\n \"id\": \"c1d2e3f4\",\n \"parentId\": \"b1c2d3e4\",\n \"timestamp\": \"2026-02-16T10:22:00.000Z\",\n \"thinkingLevel\": \"high\"\n}\n```\n\n### `compaction`\n\n```json\n{\n \"type\": \"compaction\",\n \"id\": \"d1e2f3a4\",\n \"parentId\": \"c1d2e3f4\",\n \"timestamp\": \"2026-02-16T10:23:00.000Z\",\n \"summary\": \"Conversation summary\",\n \"shortSummary\": \"Short recap\",\n \"firstKeptEntryId\": \"a1b2c3d4\",\n \"tokensBefore\": 42000,\n \"details\": { \"readFiles\": [\"src/a.ts\"] },\n \"preserveData\": { \"hookState\": true },\n \"fromExtension\": false\n}\n```\n\n### `branch_summary`\n\n```json\n{\n \"type\": \"branch_summary\",\n \"id\": \"e1f2a3b4\",\n \"parentId\": \"a1b2c3d4\",\n \"timestamp\": \"2026-02-16T10:24:00.000Z\",\n \"fromId\": \"a1b2c3d4\",\n \"summary\": \"Summary of abandoned path\",\n \"details\": { \"note\": \"optional\" },\n \"fromExtension\": true\n}\n```\n\nIf branching from root (`branchFromId === null`), `fromId` is the literal string `\"root\"`.\n\n### `custom`\n\nExtension state persistence; ignored by `buildSessionContext`.\n\n```json\n{\n \"type\": \"custom\",\n \"id\": \"f1a2b3c4\",\n \"parentId\": \"e1f2a3b4\",\n \"timestamp\": \"2026-02-16T10:25:00.000Z\",\n \"customType\": \"my-extension\",\n \"data\": { \"state\": 1 }\n}\n```\n\n### `custom_message`\n\nExtension-provided message that does participate in LLM context. `content` can be a string or text/image content blocks, and `attribution` records whether the user or agent initiated it.\n\n```json\n{\n \"type\": \"custom_message\",\n \"id\": \"a2b3c4d5\",\n \"parentId\": \"f1a2b3c4\",\n \"timestamp\": \"2026-02-16T10:26:00.000Z\",\n \"customType\": \"my-extension\",\n \"content\": \"Injected context\",\n \"display\": true,\n \"details\": { \"debug\": false },\n \"attribution\": \"agent\"\n}\n```\n\n### `label`\n\n```json\n{\n \"type\": \"label\",\n \"id\": \"b2c3d4e5\",\n \"parentId\": \"a2b3c4d5\",\n \"timestamp\": \"2026-02-16T10:27:00.000Z\",\n \"targetId\": \"a1b2c3d4\",\n \"label\": \"checkpoint\"\n}\n```\n\n`label: undefined` clears a label for `targetId`.\n\n### `ttsr_injection`\n\n```json\n{\n \"type\": \"ttsr_injection\",\n \"id\": \"c2d3e4f5\",\n \"parentId\": \"b2c3d4e5\",\n \"timestamp\": \"2026-02-16T10:28:00.000Z\",\n \"injectedRules\": [\"ruleA\", \"ruleB\"]\n}\n```\n\n### `mcp_tool_selection`\n\n```json\n{\n \"type\": \"mcp_tool_selection\",\n \"id\": \"d2e3f4a5\",\n \"parentId\": \"c2d3e4f5\",\n \"timestamp\": \"2026-02-16T10:28:30.000Z\",\n \"selectedToolNames\": [\"server.tool\"]\n}\n```\n\n### `discovered_builtin_tool_selection`\n\n```json\n{\n \"type\": \"discovered_builtin_tool_selection\",\n \"id\": \"e2f3g4h5\",\n \"parentId\": \"d2e3f4a5\",\n \"timestamp\": \"2026-02-16T10:28:31.000Z\",\n \"selectedToolNames\": [\"search_tool_bm25\"],\n \"mutationCorrelationId\": \"4c2b9c60-20d7-4a18-8d2a-8edc1f892b89\"\n}\n```\n\n`selectedToolNames` is the explicit discovered built-in selection. `mutationCorrelationId` is optional and correlates adjacent MCP and discovered built-in selection records from one mutation.\n\n### `session_init`\n\n```json\n{\n \"type\": \"session_init\",\n \"id\": \"d2e3f4a5\",\n \"parentId\": \"c2d3e4f5\",\n \"timestamp\": \"2026-02-16T10:29:00.000Z\",\n \"systemPrompt\": \"...\",\n \"task\": \"...\",\n \"tools\": [\"read\", \"edit\"],\n \"outputSchema\": { \"type\": \"object\" }\n}\n```\n\n### `mode_change`\n\n```json\n{\n \"type\": \"mode_change\",\n \"id\": \"e2f3a4b5\",\n \"parentId\": \"d2e3f4a5\",\n \"timestamp\": \"2026-02-16T10:30:00.000Z\",\n \"mode\": \"plan\",\n \"data\": { \"planFile\": \"/tmp/plan.md\" }\n}\n```\n\n## Versioning and Migration\n\nCurrent session version: `5`.\n\n### v1 -> v2\n\nApplied when header `version` is missing or `< 2`:\n\n- Adds `id` and `parentId` to each non-header entry.\n- Reconstructs a linear parent chain using file order.\n- Migrates compaction field `firstKeptEntryIndex` -> `firstKeptEntryId` when present.\n- Sets header `version = 2`.\n\n### v2 -> v3\n\nApplied when header `version < 3`:\n\n- For `message` entries: rewrites legacy `message.role === \"hookMessage\"` to `\"custom\"`.\n- Sets header `version = 3`.\n\n### v3 -> v4\n\nApplied when header `version < 4`:\n\n- Sets header `version = 4`.\n- Introduces append-only `header_patch` and `entry_patch` records.\n\n### v4 -> v5\n\nApplied when header `version < 5`:\n\n- Sets header `version = 5`.\n- Separates MCP (`mcp_tool_selection`) and discovered built-in (`discovered_builtin_tool_selection`) selection authority. The legacy v4 combined built-in field remains readable.\n- Patch records replay for v4 and v5 transcripts. Headers with a version greater than 5 are rejected before replay.\n\n### Migration Trigger and Persistence\n\n- v1-v4 transcripts remain readable without mutation during read-only inspection and strict resume selection. Patch records replay for v4 and v5 transcripts; headers with a version greater than 5 are rejected before replay.\n- Mutable loads migrate v1-v4 entries in memory but do not rewrite on read. Migration and the complete v5 rewrite are deferred until the first authorized persistence.\n- v5 sessions load without a migration rewrite. Once v5 data exists, do not roll back to a v4 writer: v4 writers cannot preserve v5 selection authority.\n\n### Discovery selection authority\n\nMCP and discovered built-in authority are independent. Constructor `toolNames` establishes authority only for the domain it names; currently essential built-ins remain baseline policy and never become discovered-built-in authority. A list containing only non-essential built-ins does not suppress configured or exact-config MCP defaults, and a list containing only MCP tools does not suppress built-in baselines. An explicit empty list clears both applicable domains. Explicit new-session names and empty clears are persisted as separate domain entries; omitted selections, essential baselines, and configured/exact baselines are not authoritative and are not persisted. Resume reconstructs state without appending authority entries.\n\nA combined activation appends an MCP entry first and a discovered-built-in entry second. Both entries carry the same optional `mutationCorrelationId`; older entries without this field remain valid.\n## Load and Compatibility Behavior\n\n`loadEntriesFromFile(path)` behavior:\n\n- Missing file (`ENOENT`) -> returns `[]`.\n- Non-parseable lines are handled by lenient JSONL parser (`parseJsonlLenient`).\n- If first parsed entry is not a valid session header (`type !== \"session\"` or missing string `id`) -> returns `[]`.\n\n`SessionManager.setSessionFile()` behavior:\n\n- `[]` from loader is treated as empty/nonexistent session and replaced with a new initialized session file at that path.\n- Valid files are loaded, migrated if needed, blob refs resolved, then indexed.\n\n## Tree and Leaf Semantics\n\nThe underlying model is append-only tree + mutable leaf pointer:\n\n- Every append method creates exactly one new entry whose `parentId` is current `leafId`.\n- The new entry becomes the new `leafId`.\n- `branch(entryId)` moves only `leafId`; existing entries remain unchanged.\n- `resetLeaf()` sets `leafId = null`; next append creates a new root entry (`parentId: null`).\n- `branchWithSummary()` sets leaf to branch target and appends a `branch_summary` entry.\n\n`getEntries()` returns all non-header entries in insertion order. Existing entries are not deleted in normal operation; rewrites preserve logical history while updating representation (migrations, move, targeted rewrite helpers).\n\n## Context Reconstruction (`buildSessionContext`)\n\n`buildSessionContext(entries, leafId, byId?)` resolves what is sent to the model.\n\nAlgorithm:\n\n1. Determine leaf:\n - `leafId === null` -> return empty context.\n - explicit `leafId` -> use that entry if found.\n - otherwise fallback to last entry.\n2. Walk `parentId` chain from leaf to root and reverse to root->leaf path.\n3. Derive runtime state across path:\n - `thinkingLevel` from latest `thinking_level_change` (default `\"off\"`)\n - `serviceTier` from latest `service_tier_change`\n - model map from `model_change` entries (`role ?? \"default\"`)\n - fallback `models.default` from assistant message provider/model if no explicit model change\n - deduplicated `injectedTtsrRules` from all `ttsr_injection` entries\n - selected MCP discovery tools from latest `mcp_tool_selection`\n - mode/modeData from latest `mode_change` (default mode `\"none\"`)\n4. Build message list:\n - `message` entries pass through\n - `custom_message` entries become `custom` AgentMessages via `createCustomMessage`\n - `branch_summary` entries become `branchSummary` AgentMessages via `createBranchSummaryMessage`\n - if a `compaction` exists on path:\n - emit compaction summary first (`createCompactionSummaryMessage`)\n - emit path entries starting at `firstKeptEntryId` up to the compaction boundary\n - emit entries after the compaction boundary\n\n`custom`, `session_init`, `service_tier_change`, `mcp_tool_selection`, and `ttsr_injection` entries do not inject model context directly.\n\n## Persistence Guarantees and Failure Model\n\n### Persist vs in-memory\n\n- `SessionManager.create/open/continueRecent/forkFrom` -> persistent mode (`persist = true`).\n- `SessionManager.inMemory` -> non-persistent mode (`persist = false`) with `MemorySessionStorage`.\n\n### Write pipeline\n\nWrites are serialized through an internal promise chain (`#persistChain`) and `NdjsonFileWriter`.\n\n- `append*` updates in-memory state immediately.\n- Persistence is deferred until at least one assistant message exists.\n - Before first assistant: entries are retained in memory; no file append occurs.\n - When first assistant exists: full in-memory session is flushed to file.\n - Afterwards: new entries append incrementally.\n\nRationale in code: avoid persisting sessions that never produced an assistant response.\n\n### Durability operations\n\n- `flush()` flushes writer and calls `fsync()`.\n- Atomic full rewrites (`#rewriteFile`) write to temp file, flush+fsync, close, then rename over target.\n- Used for migrations, `setSessionName`, `rewriteEntries`, move operations, and tool-call arg rewrites.\n\n### Error behavior\n\n- Persistence errors are latched (`#persistError`) and rethrown on subsequent operations.\n- First error is logged once with session file context.\n- Writer close is best-effort but propagates the first meaningful error.\n\n## Data Size Controls and Blob Externalization\n\nBefore persisting entries:\n\n- Large strings are truncated to `MAX_PERSIST_CHARS` (500,000 chars) with notice:\n - `\"[Session persistence truncated large content]\"`\n- Transient fields `partialJson` and `jsonlEvents` are removed.\n- If object has both `content` and `lineCount`, line count is recomputed after truncation.\n- Image blocks in `content` arrays with base64 length >= 1024 are externalized to blob refs:\n - stored as `blob:sha256:<hash>`\n - raw bytes written to blob store (`BlobStore.put`)\n\nOn load, blob refs are resolved back to base64 for message/custom_message image blocks.\n\n## Storage Abstractions\n\n`SessionStorage` interface provides all filesystem operations used by `SessionManager`:\n\n- sync: `ensureDirSync`, `existsSync`, `writeTextSync`, `statSync`, `listFilesSync`\n- async: `exists`, `readText`, `readTextPrefix`, `writeText`, `rename`, `unlink`, `openWriter`\n\nImplementations:\n\n- `FileSessionStorage`: real filesystem (Bun + node fs)\n- `MemorySessionStorage`: map-backed in-memory implementation for tests/non-persistent sessions\n\n`SessionStorageWriter` exposes `writeLine`, `flush`, `fsync`, `close`, `getError`.\n\n## Session Discovery Utilities\n\nDefined in `session-manager.ts`:\n\n- `getRecentSessions(sessionDir, limit)` -> lightweight metadata for UI/session picker\n- `findMostRecentSession(sessionDir)` -> newest by mtime\n- `list(cwd, sessionDir?)` -> sessions in one project scope\n- `listAll()` -> sessions across all project scopes under `~/.gjc/agent/sessions`\n\nMetadata extraction reads only a prefix (`readTextPrefix(..., 4096)`) where possible.\n\n## Related but Distinct: Prompt History Storage\n\n`HistoryStorage` (`history-storage.ts`) is a separate SQLite subsystem for prompt recall/search, not session replay.\n\n- DB: `~/.gjc/agent/history.db`\n- Table: `history(id, prompt, created_at, cwd)`\n- FTS5 index: `history_fts` with trigger-maintained sync\n- Deduplicates consecutive identical prompts using in-memory last-prompt cache\n- Async insertion (`setImmediate`) so prompt capture does not block turn execution\n\nUse session files for conversation graph/state replay; use `HistoryStorage` for prompt history UX.\n",
|
|
101
|
+
"session.md": "# Session Storage and Entry Model\n\nThis document is the source of truth for how coding-agent sessions are represented, persisted, migrated, and reconstructed at runtime.\n\n## Scope\n\nCovers:\n\n- Session JSONL format and versioning\n- Entry taxonomy and tree semantics (`id`/`parentId` + leaf pointer)\n- Migration/compatibility behavior when loading old or malformed files\n- Context reconstruction (`buildSessionContext`)\n- Persistence guarantees, failure behavior, truncation/blob externalization\n- Storage abstractions (`FileSessionStorage`, `MemorySessionStorage`) and related utilities\n\nDoes not cover `/tree` UI rendering behavior beyond semantics that affect session data.\n\n## Implementation Files\n\n- [`src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)\n- [`src/session/messages.ts`](../packages/coding-agent/src/session/messages.ts)\n- [`src/session/session-storage.ts`](../packages/coding-agent/src/session/session-storage.ts)\n- [`src/session/history-storage.ts`](../packages/coding-agent/src/session/history-storage.ts)\n- [`src/session/blob-store.ts`](../packages/coding-agent/src/session/blob-store.ts)\n\n## On-Disk Layout\n\nDefault managed session file location:\n\n```text\n~/.gjc/agent/sessions/v2-<52-char-base32-sha256>/<timestamp>_<sessionId>.jsonl\n```\n\nThe `v2-…` component is a fixed-width SHA-256/base32 digest of the native canonical workspace identity (identity version 1); it is **not** a reversible or injective user-facing encoding. The binding file `.gjc-managed-session-scope.v2.json` records the canonical identity and digest. Existing bindings must be regular, canonically encoded files that agree with the resolved identity; a mismatch or unsafe path fails closed.\n\nIdentity is platform-specific:\n\n- POSIX paths and supported local aliases that resolve to the same native directory identity share the same v2 scope.\n- On Windows, equivalent supported local path spellings (including drive-letter/case aliases) resolve through the native identity API before the scope is derived.\n- UNC/network workspaces are unsupported and return a `network_unsupported` resolution result; no SMB share is needed or assumed by this design.\n\nThe default managed writer creates new data only in v2 scopes. It never writes new legacy-layout data. `--session-dir` is an explicit storage/lookup override and is not a request to derive the default managed scope.\n\n### Legacy migration and retention\n\nLegacy encoded directories are discovered only after validating each candidate's header and workspace identity. With `session.directoryMigration: \"copy-retain\"` (the default), an eligible legacy session is copied into the v2 scope without replacing an existing destination; the legacy source is retained. Set `session.directoryMigration: \"disabled\"` to leave legacy candidates unmigrated. Migration is lazy and guarded by a managed lock, binding checks, no-follow/owner-only path checks, and source identity validation; conflicts, unsafe artifacts, or changed sources fail rather than guessing.\n\nMigration does not automatically clean up legacy files, copied files, locks, artifacts, or abandoned data. A migration tombstone records a completed/retired source so repeated scans do not reinterpret it as a new migration request; it is not evidence that the old data was deleted. Artifact copying is bounded and rejects symlinks, hard links, excessive depth, file count, or size.\n\n### Security boundary\n\nManaged storage enforces owner-only directory/file security and refuses unsafe symlinks or malformed bindings on the paths it verifies. This is a local storage-integrity boundary, not authentication, authorization, encryption, or a guarantee against a hostile concurrent local actor/race outside the verified operations. Callers must still protect the agent directory and session contents.\n\nOn Linux filesystems where the exact POSIX ACL xattr operation returns `ENOTSUP`/`EOPNOTSUPP`, GJC treats that result only as proof that the filesystem cannot store that ACL attribute. The ACL gate still requires the same opened object to pass effective-owner, exact `0700` directory or `0600` file mode, safe-type, no-follow traversal, and identity/replacement checks. Permission denial, I/O errors, present or malformed ACL data, and unknown results remain failures. Managed descriptors use close-on-exec and are not delegated as authority to subprocesses. This compatibility rule does not change explicit `--session-dir`, macOS ACL, or Windows DACL policy.\n\nBlob store location:\n\n```text\n~/.gjc/agent/blobs/<sha256>\n```\n\nTerminal breadcrumb files are written under:\n\n```text\n~/.gjc/agent/terminal-sessions/<terminal-id>\n```\n\nBreadcrumb content is two lines: original cwd, then session file path. `continueRecent()` prefers this terminal-scoped pointer before scanning most-recent mtime.\n\n## File Format\n\nSession files are JSONL: one JSON object per line.\n\n- Line 1 is always the session header (`type: \"session\"`).\n- Remaining lines are `SessionEntry` values or v4/v5 append-only patch records. `header_patch` records update header metadata and `entry_patch` records replace a message payload when replay metadata is sanitized.\n- Entries and patch records are append-only at runtime; branch navigation moves a pointer (`leafId`) rather than mutating existing entries.\n\n### Header (`SessionHeader`)\n\n```json\n{\n \"type\": \"session\",\n \"version\": 5,\n \"id\": \"1f9d2a6b9c0d1234\",\n \"timestamp\": \"2026-02-16T10:20:30.000Z\",\n \"cwd\": \"/work/pi\",\n \"title\": \"optional session title\",\n \"titleSource\": \"auto\",\n \"starredPatchVersion\": 1,\n \"starred\": true,\n \"parentSession\": \"optional lineage marker\"\n}\n```\n\nNotes:\n\n- `version` is optional in v1 files; absence means v1.\n- `starredPatchVersion: 1` declares that the transcript can contain standalone star patches. It lets discovery skip star scans for older v5 transcripts while keeping their existing title/cwd patch lookup bounded.\n- `starred` is optional and defaults to `false`. `/star` and `/unstar` persist changes through append-only `header_patch` records; writers checkpoint the current star state beside later title/cwd patches so reverse discovery remains bounded. Starring adds no star-aware branch to ID resolution, `--continue`, deletion, or retention pruning and never protects a session. Like every transcript append, the metadata write updates the file's ordinary modification time, which those existing policies may already consume.\n- `parentSession` is an opaque lineage string. Current code writes either a session id or a session path depending on flow (`fork`, `forkFrom`, `createBranchedSession`, or explicit `newSession({ parentSession })`). Treat as metadata, not a typed foreign key.\n\n### Entry Base (`SessionEntryBase`)\n\nAll non-header entries include:\n\n```json\n{\n \"type\": \"...\",\n \"id\": \"8-char-id\",\n \"parentId\": \"previous-or-branch-parent\",\n \"timestamp\": \"2026-02-16T10:20:30.000Z\"\n}\n```\n\n`parentId` can be `null` for a root entry (first append, or after `resetLeaf()`).\n\n## Entry Taxonomy\n\n`SessionEntry` is the union of:\n\n- `message`\n- `thinking_level_change`\n- `service_tier_change`\n- `compaction`\n- `branch_summary`\n- `custom`\n- `custom_message`\n- `label`\n- `ttsr_injection`\n- `session_init`\n- `mode_change`\n- `mcp_tool_selection`\n- `discovered_builtin_tool_selection`\n\n### `message`\n\nStores an `AgentMessage` directly.\n\n```json\n{\n \"type\": \"message\",\n \"id\": \"a1b2c3d4\",\n \"parentId\": null,\n \"timestamp\": \"2026-02-16T10:21:00.000Z\",\n \"message\": {\n \"role\": \"assistant\",\n \"provider\": \"anthropic\",\n \"model\": \"anthropic-model-sonnet-4-5\",\n \"content\": [{ \"type\": \"text\", \"text\": \"Done.\" }],\n \"usage\": {\n \"input\": 100,\n \"output\": 20,\n \"cacheRead\": 0,\n \"cacheWrite\": 0,\n \"cost\": {\n \"input\": 0,\n \"output\": 0,\n \"cacheRead\": 0,\n \"cacheWrite\": 0,\n \"total\": 0\n }\n },\n \"timestamp\": 1760000000000\n }\n}\n```\n\n### `model_change`\n\n```json\n{\n \"type\": \"model_change\",\n \"id\": \"b1c2d3e4\",\n \"parentId\": \"a1b2c3d4\",\n \"timestamp\": \"2026-02-16T10:21:30.000Z\",\n \"model\": \"openai/gpt-4o\",\n \"role\": \"default\"\n}\n```\n\n`role` is optional; missing is treated as `default` in context reconstruction.\n\n### `service_tier_change`\n\n```json\n{\n \"type\": \"service_tier_change\",\n \"id\": \"c1d2e3f4\",\n \"parentId\": \"b1c2d3e4\",\n \"timestamp\": \"2026-02-16T10:21:45.000Z\",\n \"serviceTier\": \"flex\"\n}\n```\n\n`serviceTier` can also be `null`.\n\n### `thinking_level_change`\n\n```json\n{\n \"type\": \"thinking_level_change\",\n \"id\": \"c1d2e3f4\",\n \"parentId\": \"b1c2d3e4\",\n \"timestamp\": \"2026-02-16T10:22:00.000Z\",\n \"thinkingLevel\": \"high\"\n}\n```\n\n### `compaction`\n\n```json\n{\n \"type\": \"compaction\",\n \"id\": \"d1e2f3a4\",\n \"parentId\": \"c1d2e3f4\",\n \"timestamp\": \"2026-02-16T10:23:00.000Z\",\n \"summary\": \"Conversation summary\",\n \"shortSummary\": \"Short recap\",\n \"firstKeptEntryId\": \"a1b2c3d4\",\n \"tokensBefore\": 42000,\n \"details\": { \"readFiles\": [\"src/a.ts\"] },\n \"preserveData\": { \"hookState\": true },\n \"fromExtension\": false\n}\n```\n\n### `branch_summary`\n\n```json\n{\n \"type\": \"branch_summary\",\n \"id\": \"e1f2a3b4\",\n \"parentId\": \"a1b2c3d4\",\n \"timestamp\": \"2026-02-16T10:24:00.000Z\",\n \"fromId\": \"a1b2c3d4\",\n \"summary\": \"Summary of abandoned path\",\n \"details\": { \"note\": \"optional\" },\n \"fromExtension\": true\n}\n```\n\nIf branching from root (`branchFromId === null`), `fromId` is the literal string `\"root\"`.\n\n### `custom`\n\nExtension state persistence; ignored by `buildSessionContext`.\n\n```json\n{\n \"type\": \"custom\",\n \"id\": \"f1a2b3c4\",\n \"parentId\": \"e1f2a3b4\",\n \"timestamp\": \"2026-02-16T10:25:00.000Z\",\n \"customType\": \"my-extension\",\n \"data\": { \"state\": 1 }\n}\n```\n\n### `custom_message`\n\nExtension-provided message that does participate in LLM context. `content` can be a string or text/image content blocks, and `attribution` records whether the user or agent initiated it.\n\n```json\n{\n \"type\": \"custom_message\",\n \"id\": \"a2b3c4d5\",\n \"parentId\": \"f1a2b3c4\",\n \"timestamp\": \"2026-02-16T10:26:00.000Z\",\n \"customType\": \"my-extension\",\n \"content\": \"Injected context\",\n \"display\": true,\n \"details\": { \"debug\": false },\n \"attribution\": \"agent\"\n}\n```\n\n### `label`\n\n```json\n{\n \"type\": \"label\",\n \"id\": \"b2c3d4e5\",\n \"parentId\": \"a2b3c4d5\",\n \"timestamp\": \"2026-02-16T10:27:00.000Z\",\n \"targetId\": \"a1b2c3d4\",\n \"label\": \"checkpoint\"\n}\n```\n\n`label: undefined` clears a label for `targetId`.\n\n### `ttsr_injection`\n\n```json\n{\n \"type\": \"ttsr_injection\",\n \"id\": \"c2d3e4f5\",\n \"parentId\": \"b2c3d4e5\",\n \"timestamp\": \"2026-02-16T10:28:00.000Z\",\n \"injectedRules\": [\"ruleA\", \"ruleB\"]\n}\n```\n\n### `mcp_tool_selection`\n\n```json\n{\n \"type\": \"mcp_tool_selection\",\n \"id\": \"d2e3f4a5\",\n \"parentId\": \"c2d3e4f5\",\n \"timestamp\": \"2026-02-16T10:28:30.000Z\",\n \"selectedToolNames\": [\"server.tool\"]\n}\n```\n\n### `discovered_builtin_tool_selection`\n\n```json\n{\n \"type\": \"discovered_builtin_tool_selection\",\n \"id\": \"e2f3g4h5\",\n \"parentId\": \"d2e3f4a5\",\n \"timestamp\": \"2026-02-16T10:28:31.000Z\",\n \"selectedToolNames\": [\"search_tool_bm25\"],\n \"mutationCorrelationId\": \"4c2b9c60-20d7-4a18-8d2a-8edc1f892b89\"\n}\n```\n\n`selectedToolNames` is the explicit discovered built-in selection. `mutationCorrelationId` is optional and correlates adjacent MCP and discovered built-in selection records from one mutation.\n\n### `session_init`\n\n```json\n{\n \"type\": \"session_init\",\n \"id\": \"d2e3f4a5\",\n \"parentId\": \"c2d3e4f5\",\n \"timestamp\": \"2026-02-16T10:29:00.000Z\",\n \"systemPrompt\": \"...\",\n \"task\": \"...\",\n \"tools\": [\"read\", \"edit\"],\n \"outputSchema\": { \"type\": \"object\" }\n}\n```\n\n### `mode_change`\n\n```json\n{\n \"type\": \"mode_change\",\n \"id\": \"e2f3a4b5\",\n \"parentId\": \"d2e3f4a5\",\n \"timestamp\": \"2026-02-16T10:30:00.000Z\",\n \"mode\": \"plan\",\n \"data\": { \"planFile\": \"/tmp/plan.md\" }\n}\n```\n\n## Versioning and Migration\n\nCurrent session version: `5`.\n\n### v1 -> v2\n\nApplied when header `version` is missing or `< 2`:\n\n- Adds `id` and `parentId` to each non-header entry.\n- Reconstructs a linear parent chain using file order.\n- Migrates compaction field `firstKeptEntryIndex` -> `firstKeptEntryId` when present.\n- Sets header `version = 2`.\n\n### v2 -> v3\n\nApplied when header `version < 3`:\n\n- For `message` entries: rewrites legacy `message.role === \"hookMessage\"` to `\"custom\"`.\n- Sets header `version = 3`.\n\n### v3 -> v4\n\nApplied when header `version < 4`:\n\n- Sets header `version = 4`.\n- Introduces append-only `header_patch` and `entry_patch` records.\n\n### v4 -> v5\n\nApplied when header `version < 5`:\n\n- Sets header `version = 5`.\n- Separates MCP (`mcp_tool_selection`) and discovered built-in (`discovered_builtin_tool_selection`) selection authority. The legacy v4 combined built-in field remains readable.\n- Patch records replay for v4 and v5 transcripts. Headers with a version greater than 5 are rejected before replay.\n\n### Migration Trigger and Persistence\n\n- v1-v4 transcripts remain readable without mutation during read-only inspection and strict resume selection. Patch records replay for v4 and v5 transcripts; headers with a version greater than 5 are rejected before replay.\n- Mutable loads migrate v1-v4 entries in memory but do not rewrite on read. Migration and the complete v5 rewrite are deferred until the first authorized persistence.\n- v5 sessions load without a migration rewrite. Once v5 data exists, do not roll back to a v4 writer: v4 writers cannot preserve v5 selection authority.\n\n### Discovery selection authority\n\nMCP and discovered built-in authority are independent. Constructor `toolNames` establishes authority only for the domain it names; currently essential built-ins remain baseline policy and never become discovered-built-in authority. A list containing only non-essential built-ins does not suppress configured or exact-config MCP defaults, and a list containing only MCP tools does not suppress built-in baselines. An explicit empty list clears both applicable domains. Explicit new-session names and empty clears are persisted as separate domain entries; omitted selections, essential baselines, and configured/exact baselines are not authoritative and are not persisted. Resume reconstructs state without appending authority entries.\n\nA combined activation appends an MCP entry first and a discovered-built-in entry second. Both entries carry the same optional `mutationCorrelationId`; older entries without this field remain valid.\n## Load and Compatibility Behavior\n\n`loadEntriesFromFile(path)` behavior:\n\n- Missing file (`ENOENT`) -> returns `[]`.\n- Non-parseable lines are handled by lenient JSONL parser (`parseJsonlLenient`).\n- If first parsed entry is not a valid session header (`type !== \"session\"` or missing string `id`) -> returns `[]`.\n\n`SessionManager.setSessionFile()` behavior:\n\n- `[]` from loader is treated as empty/nonexistent session and replaced with a new initialized session file at that path.\n- Valid files are loaded, migrated if needed, blob refs resolved, then indexed.\n\n## Tree and Leaf Semantics\n\nThe underlying model is append-only tree + mutable leaf pointer:\n\n- Every append method creates exactly one new entry whose `parentId` is current `leafId`.\n- The new entry becomes the new `leafId`.\n- `branch(entryId)` moves only `leafId`; existing entries remain unchanged.\n- `resetLeaf()` sets `leafId = null`; next append creates a new root entry (`parentId: null`).\n- `branchWithSummary()` sets leaf to branch target and appends a `branch_summary` entry.\n\n`getEntries()` returns all non-header entries in insertion order. Existing entries are not deleted in normal operation; rewrites preserve logical history while updating representation (migrations, move, targeted rewrite helpers).\n\n## Context Reconstruction (`buildSessionContext`)\n\n`buildSessionContext(entries, leafId, byId?)` resolves what is sent to the model.\n\nAlgorithm:\n\n1. Determine leaf:\n - `leafId === null` -> return empty context.\n - explicit `leafId` -> use that entry if found.\n - otherwise fallback to last entry.\n2. Walk `parentId` chain from leaf to root and reverse to root->leaf path.\n3. Derive runtime state across path:\n - `thinkingLevel` from latest `thinking_level_change` (default `\"off\"`)\n - `serviceTier` from latest `service_tier_change`\n - model map from `model_change` entries (`role ?? \"default\"`)\n - fallback `models.default` from assistant message provider/model if no explicit model change\n - deduplicated `injectedTtsrRules` from all `ttsr_injection` entries\n - selected MCP discovery tools from latest `mcp_tool_selection`\n - mode/modeData from latest `mode_change` (default mode `\"none\"`)\n4. Build message list:\n - `message` entries pass through\n - `custom_message` entries become `custom` AgentMessages via `createCustomMessage`\n - `branch_summary` entries become `branchSummary` AgentMessages via `createBranchSummaryMessage`\n - if a `compaction` exists on path:\n - emit compaction summary first (`createCompactionSummaryMessage`)\n - emit path entries starting at `firstKeptEntryId` up to the compaction boundary\n - emit entries after the compaction boundary\n\n`custom`, `session_init`, `service_tier_change`, `mcp_tool_selection`, and `ttsr_injection` entries do not inject model context directly.\n\n## Persistence Guarantees and Failure Model\n\n### Persist vs in-memory\n\n- `SessionManager.create/open/continueRecent/forkFrom` -> persistent mode (`persist = true`).\n- `SessionManager.inMemory` -> non-persistent mode (`persist = false`) with `MemorySessionStorage`.\n\n### Write pipeline\n\nWrites are serialized through an internal promise chain (`#persistChain`) and `NdjsonFileWriter`.\n\n- `append*` updates in-memory state immediately.\n- Persistence is deferred until at least one assistant message exists.\n - Before first assistant: entries are retained in memory; no file append occurs.\n - When first assistant exists: full in-memory session is flushed to file.\n - Afterwards: new entries append incrementally.\n\nRationale in code: avoid persisting sessions that never produced an assistant response.\n\n### Durability operations\n\n- `flush()` flushes writer and calls `fsync()`.\n- Atomic full rewrites (`#rewriteFile`) write to temp file, flush+fsync, close, then rename over target.\n- Used for migrations, `setSessionName`, `rewriteEntries`, move operations, and tool-call arg rewrites.\n\n### Error behavior\n\n- Persistence errors are latched (`#persistError`) and rethrown on subsequent operations.\n- First error is logged once with session file context.\n- Writer close is best-effort but propagates the first meaningful error.\n\n## Data Size Controls and Blob Externalization\n\nBefore persisting entries:\n\n- Large strings are truncated to `MAX_PERSIST_CHARS` (500,000 chars) with notice:\n - `\"[Session persistence truncated large content]\"`\n- Transient fields `partialJson` and `jsonlEvents` are removed.\n- If object has both `content` and `lineCount`, line count is recomputed after truncation.\n- Image blocks in `content` arrays with base64 length >= 1024 are externalized to blob refs:\n - stored as `blob:sha256:<hash>`\n - raw bytes written to blob store (`BlobStore.put`)\n\nOn load, blob refs are resolved back to base64 for message/custom_message image blocks.\n\n## Storage Abstractions\n\n`SessionStorage` interface provides all filesystem operations used by `SessionManager`:\n\n- sync: `ensureDirSync`, `existsSync`, `writeTextSync`, `statSync`, `listFilesSync`\n- async: `exists`, `readText`, `readTextPrefix`, `writeText`, `rename`, `unlink`, `openWriter`\n\nImplementations:\n\n- `FileSessionStorage`: real filesystem (Bun + node fs)\n- `MemorySessionStorage`: map-backed in-memory implementation for tests/non-persistent sessions\n\n`SessionStorageWriter` exposes `writeLine`, `flush`, `fsync`, `close`, `getError`.\n\n## Session Discovery Utilities\n\nDefined in `session-manager.ts`:\n\n- `getRecentSessions(sessionDir, limit)` -> lightweight metadata for UI/session picker\n- `findMostRecentSession(sessionDir)` -> newest by mtime\n- `list(cwd, sessionDir?)` -> sessions in one project scope\n- `listAll()` -> sessions across all project scopes under `~/.gjc/agent/sessions`\n\nMetadata extraction reads only a prefix (`readTextPrefix(..., 4096)`) where possible.\n\n## Related but Distinct: Prompt History Storage\n\n`HistoryStorage` (`history-storage.ts`) is a separate SQLite subsystem for prompt recall/search, not session replay.\n\n- DB: `~/.gjc/agent/history.db`\n- Table: `history(id, prompt, created_at, cwd)`\n- FTS5 index: `history_fts` with trigger-maintained sync\n- Deduplicates consecutive identical prompts using in-memory last-prompt cache\n- Async insertion (`setImmediate`) so prompt capture does not block turn execution\n\nUse session files for conversation graph/state replay; use `HistoryStorage` for prompt history UX.\n",
|
|
102
102
|
"skills.md": "# Skills\n\nGJC supports custom `SKILL.md` skills that live as plain files on disk, following\nthe same file convention as Claude Code and OpenAI Codex. Filesystem skill\ndiscovery is **on by default** — a valid skill placed in a canonical `.gjc`\nlocation is advertised in a normal session (listed in the session's `<skills>`\ncatalog and invokable via `/skill:<name>`) with no configuration ceremony.\n\nThe four bundled GJC workflow skills — `autoresearch`, `deep-interview`, `ralplan`, and\n`ultragoal` — are always available and can never be replaced by a filesystem\nskill with the same name.\n\n## Canonical locations (loaded directly)\n\nProject scope (trusted from the repository you open):\n\n| Location | Scope notes |\n|---|---|\n| `<project>/.gjc/skills/<name>/SKILL.md` | Native GJC location; discovered from every ancestor of `cwd` up to the repo root (closest first) |\n\nUser scope (installed once, available in every project):\n\n| Location | Scope notes |\n|---|---|\n| `~/.gjc/agent/skills/<name>/SKILL.md` | Canonical GJC user location |\n| `<config>/skills/<name>/SKILL.md` | Configured legacy root (`<config>` is the home-relative directory from `GJC_CONFIG_DIR`, then `PI_CONFIG_DIR`, then `.gjc`) |\n| `~/.gjc/skills/<name>/SKILL.md` | Historical legacy user location (still honored) |\n\n## Claude Code / Codex layouts (explicit import sources)\n\nGJC recognizes the Claude Code and Codex skill layouts but never loads them\ndirectly — `.gjc` is the only runtime authority, so a session never silently\nexecutes content owned by another host's configuration:\n\n| Location | Convention |\n|---|---|\n| `<project>/.claude/skills/<name>/SKILL.md` | Claude Code project skills |\n| `<project>/.codex/skills/<name>/SKILL.md` | OpenAI Codex project skills |\n| `~/.claude/skills/<name>/SKILL.md` | Claude Code user skills |\n| `~/.codex/skills/<name>/SKILL.md` | Codex user skills |\n\nThese are **import sources**: the `skill_discovery` tool and\n`gjc skills discover` surface them as diagnostics naming the exact copy command\nthat enables each skill, so a skill placed in a documented convention location\nis discoverable in a normal session. Foreign user-home layouts are enumerated\nfor import only and are never loaded into sessions.\n\nImporting is a plain file copy into a canonical location:\n\n```sh\n# import one Claude Code project skill into the current repository\nmkdir -p .gjc/skills/my-skill\ncp .claude/skills/my-skill/SKILL.md .gjc/skills/my-skill/SKILL.md\n\n# import one Codex user skill into your user-wide GJC skills\nmkdir -p ~/.gjc/agent/skills/my-skill\ncp ~/.codex/skills/my-skill/SKILL.md ~/.gjc/agent/skills/my-skill/SKILL.md\n```\n\n## Installing a skill\n\nCopy a skill directory (its `SKILL.md` must start with YAML frontmatter that\nincludes `name` and `description`):\n\n```sh\n# project-local, per repository\nmkdir -p .gjc/skills/my-skill\ncp my-skill/SKILL.md .gjc/skills/my-skill/SKILL.md\n\n# user-wide, available in every project\nmkdir -p ~/.gjc/agent/skills/my-skill\ncp my-skill/SKILL.md ~/.gjc/agent/skills/my-skill/SKILL.md\n```\n\nStart a new session and invoke the skill with `/skill:my-skill`, or let the\nmodel discover it with the `skill_discovery` tool.\n\n## Trust and disable\n\nSkill discovery is controlled by three settings, all on by default:\n\n| Setting | Effect |\n|---|---|\n| `skills.enabled` | Master switch for all filesystem skill discovery |\n| `skills.trustProjectSkills` | Load project-scoped `.gjc/skills` and surface project `.claude`/`.codex` import candidates |\n| `skills.trustUserSkills` | Load user-scoped skills (`~/.gjc/agent/skills` and legacy roots) and surface user-home import candidates |\n\n```sh\ngjc config set skills.trustProjectSkills false # ignore repo-controlled skills only\ngjc config set skills.trustUserSkills false # ignore personal skills only\ngjc config set skills.enabled false # disable all filesystem skill discovery\n```\n\nThe deprecated `skills.enablePiProject` / `skills.enablePiUser` settings remain\nsupported as aliases: an explicitly configured legacy value is honored unless\nthe corresponding trust setting is also configured. `gjc config set` accepts\neither name.\n\nBundled workflow skills are never affected by these switches — they remain\navailable even with discovery fully disabled.\n\n## Precedence\n\nDuplicate names resolve deterministically, first location wins:\n\n1. project scope beats user scope;\n2. within project scope, the `.gjc/skills` directory nearest to `cwd` wins\n (ancestors are walked from `cwd` up to the repo root, closest first);\n3. within user scope: `<config>/agent/skills` > legacy `<config>/skills` >\n legacy `~/.gjc/skills`.\n\nShadowed duplicates are diagnosed rather than silent. Bundled workflow skill\nnames are reserved: a project skill named `autoresearch`, `deep-interview`, `ralplan`,\nor `ultragoal` produces a protected-name collision warning, and the bundled\ndefinition always wins in sessions.\n\n## Diagnostics\n\nInvalid skills and policy filters produce actionable diagnostics instead of\nsilent skips:\n\n- a `SKILL.md` without a leading YAML frontmatter block;\n- a skill without a `description` in its frontmatter;\n- a skill shadowed by a higher-precedence location;\n- a skill filtered by `skills.ignoredSkills` / `skills.includeSkills` /\n `disabledExtensions`;\n- a protected-name collision with a bundled workflow skill;\n- a Claude Code / Codex import candidate (with the copy command that enables it);\n- disabled discovery scopes (the `skill_discovery` tool returns a `notice`\n explaining which setting blocked an otherwise-empty result).\n\nInspect what is discoverable and why from the CLI:\n\n```sh\ngjc skills discover # project + user skills with diagnostics\ngjc skills discover --source project --json\n```\n\n## Custom directories\n\n`skills.customDirectories` adds extra user-scope scan roots; tilde expansion is\nsupported. Skills loaded from custom directories follow the same filters\n(include/ignore/disabled) as discovered skills.\n",
|
|
103
|
-
"slack-onboarding.md": "# Slack notification onboarding\n\nThis is the managed Slack Socket Mode notification adapter. It is an SDK client:\nlocal GJC sessions continue to own loopback SDK endpoints, and Slack provides a\nper-session message thread for notifications and replies.\n\n## Prerequisites\n\nCreate a Slack app in the target workspace, enable Socket Mode, and create an\napp-level token with the Socket Mode connection scope. Install the app in the\nworkspace and invite it to the selected channel. Configure only the scopes and\nevent subscriptions the adapter needs:\n\n- `chat:write` to post session roots, replies, and closure markers\n- `channels:history` for a public channel, or the corresponding history scope\n for the channel type in use\n- the message event subscription for the selected channel type\n- Socket Mode enabled for Events API delivery\n\nKeep the selected channel private to people authorized to see local session\nmetadata. Do not add broad workspace scopes or use an app token for ordinary Web\nAPI calls.\n\n## Configure the adapter\n\n`gjc notify setup slack` is non-interactive. It requires these flags:\n\n- `--slack-bot-token`\n- `--slack-app-token`\n- `--slack-workspace-id`\n- `--slack-channel-id`\n- `--slack-authorized-user-id` for the single Slack user authorized to submit replies and `/sdk` commands\n\nWithout `--slack-authorized-user-id`, the adapter remains outbound-only: every inbound envelope is acknowledged but denied before it can create a durable claim or reach an SDK endpoint. The user ID is an identifier, not a secret. It also accepts `--redact`. Provide secret values from an approved local secret mechanism, not shell history, committed configuration, tickets, screenshots, or chat. Setup writes:\n\n- `notifications.enabled = true`\n- `notifications.slack.enabled = true` (durable desired intent)\n- `notifications.slack.botToken`\n- `notifications.slack.appToken`\n- `notifications.slack.workspaceId`\n- `notifications.slack.channelId`\n- `notifications.slack.authorizedUserId` when configured\n- `notifications.redact = true` when requested\n\n`gjc notify status` reports Slack completeness, repair/quarantine state, desired intent, effective enablement, destination identifiers, and masked token values. It is status output, not a credential recovery mechanism. A successful durable save is not rolled back when later daemon activation fails; the command reports the saved-but-runtime-degraded outcome and exits nonzero. In `/settings`, bot/app secret edits are explicit `keep`, `replace`, or `remove`; removing either required token turns Slack desired intent off without changing Telegram, Discord, or the global master.\n\n## Socket Mode, threads, and resume\n\nThe daemon validates the configured workspace, channel, and paired user before durably claiming an inbound effect or sending its Socket Mode acknowledgement. The durable claim records the paired actor identity, replay identity, protected-effect reference, and captured endpoint generation; it never records Socket Mode cursors, endpoint tokens, or message bodies. Rejected, bot-authored, unauthorized, and already-claimed envelopes are acknowledged without an SDK endpoint call.\n\nAcknowledgement latency is therefore bounded by local durable-claim work rather\nthan SDK availability or command execution. After the ACK, the worker dispatches\nthe claimed effect asynchronously; a restart can replay the claim, and a retry\ncannot create a second injection. Do not treat an ACK as confirmation that the SDK\noperation completed.\n\nEach session starts with one root message. Root creation uses a caller-generated\nclient message ID and reconciliation lookup, preventing a duplicate root after\nan uncertain post. When a session closes, the daemon posts a closure marker. A\nresume starts a new immutable root, so replies to the old root are rejected and\ncannot steer the resumed session.\n\nEvents, retried deliveries, event contexts, and interaction/message identifiers\nare deduplicated in the durable claim before a reply is injected into the captured\ncurrent endpoint generation. After a Socket Mode reconnect, Slack may redeliver an\nenvelope; the new delivery is acknowledged after its claim is recognized and\ncannot cause a second injection.\n\n## Adopting an existing thread\n\nStock startup publishes a session's readiness immediately, so the daemon\nsurfaces the session and creates its own root before an operator could name an\nexisting one. Adopting an existing root therefore has an explicit, opt-in\nthree-phase lifecycle. Configuration is never part of it: the workspace and\nchannel come from `gjc notify setup` alone, and the operator supplies only a\nsession id and a thread timestamp.\n\n```text\nprepare session authority → bind the existing root through the live daemon → activate readiness\n```\n\n1. **Prepare.** Start the session prepared. A manually started session opts in\n with `GJC_NOTIFY_BIND_EXISTING_THREAD=1` in its environment; a broker\n lifecycle-managed session is prepared by its launch request instead (see\n below). Either way the session publishes its endpoint and registers with the\n broker exactly as usual, so its id and endpoint generation are discoverable\n authority, but it withholds the replayable `session_ready` signal. An\n attached daemon has nothing to surface, so no root is posted.\n2. **Bind.** `gjc notify bind-thread --session-id <id> --thread-ts <root>`\n adopts the existing root through the running daemon owner, exactly as it does\n for any live session. The CLI never writes the mapping store itself: it\n proves the configured target and the exact current owner, then submits the\n mutation over the per-request chat-daemon command channel that owner serves\n in place. A reported success is accepted only after this process observes the\n exact mapping in the durable conversation store, so a stale or forged\n `status:\"ok\"` answer is reported as `binding_outcome_unknown` rather than as a\n success.\n3. **Activate.** `gjc notify activate-thread --session-id <id>` asks\n `SessionRouter` to validate the exact indexed endpoint generation and invoke\n the session host internally. The host authorizes readiness against the\n provider-owned mapping through a redacted `{sessionId, endpointGeneration}`\n gate: activation before the binding is applied is refused (`not_bound`) with\n no grace period, and activation is idempotent, so an exact retry answers\n `already` rather than publishing a second readiness signal. The CLI never\n reads an endpoint URL/token or constructs an SDK client. When readiness is\n published, Slack adopts the bound root and posts zero replacement roots.\n\nThe opt-in is per session and explicit: only the exact value `1` prepares a\nsession, and a session without it keeps the stock immediate-ready root. The\nexisting global (`notifications.enabled`, `GJC_NOTIFICATIONS=0`) and per-session\nopt-outs are unchanged and still authoritative.\n\nPreparation has exactly two authorities and they never overlap. A manually\nstarted session uses the environment opt-in above. A broker lifecycle-managed\nsession is prepared only by the session-scoped `readiness: \"deferred\"` intent on\nits own `session.create` request: the child then publishes a distinct\n`session_prepared` signal, the lifecycle wait completes on that instead of\nreadiness, and the create receipt reports `readiness: \"prepared\"`. The\nenvironment opt-in is refused for lifecycle-managed sessions, so an inherited\nprocess-global flag can never silently defer a broker-created session.\n\nEither authority additionally requires a configured, session-enabled Slack\ntarget, because the activation gate is the existing-thread presentation\nmapping. The mapping stays provider-owned, while `SessionRouter` alone proves\nthe current endpoint generation and performs activation. A preparation request\nthat cannot build the configured mapping gate fails closed — the lifecycle child\nsettles a startup failure and the environment opt-in throws — rather than\ndegrading to ordinary immediate readiness or handing back a prepared session\nthat could activate with no binding at all.\n\nThrough the Coordinator MCP surface the same three phases are\n`gjc_coordinator_start_session` with `prepare_existing_thread: true` (which\nrejects an initial prompt and returns the session at state `prepared`), the\nunchanged `gjc notify bind-thread` command, and\n`gjc_coordinator_activate_session`. The Coordinator never writes the mapping\nstore: it proves exact endpoint authority and delegates to the same activation\nexchange the CLI uses, and durable session state only becomes ready once the\nsession itself proves `activated`/`already`. A prepared session refuses\n`gjc_coordinator_send_prompt` until it is activated.\n\n### Trust boundary\n\nThe command channel proves *correlation*, never authorship: every field a\nresponse echoes is copied verbatim out of the plaintext request published beside\nit in the daemon's own owner-only command directory. GJC trusts same-UID local\nprocesses, so nothing here defends against a hostile process running as the same\nuser; what it does guarantee is that a stale or forged answer with no matching\ndurable mapping never becomes a reported success.\n\n## Operational safety\n\nTreat rate limits, permission failures, and Socket Mode disconnects as transport\nfailures. Let the managed daemon reconnect or reconcile; do not run a competing\nSocket Mode consumer against the same app/state, manually modify conversation\nstate, persist delivery cursors, expose loopback endpoints, or use Slack as a\ngeneral remote shell.\n\nThe adapter only sends notifications and routes SDK replies. It does not support\nprovider registration, retaining endpoint credentials, or arbitrary remote\ncontrol.\n\n## Verification boundary\n\nAcceptance coverage uses an injectable fake Slack provider plus a production\nSession SDK host boundary proof. It covers durable-claim-before-acknowledgement\nfor accepted, rejected, duplicate, and reconnect-redelivered envelopes; root-post\nreconciliation; event/retry/context/interaction dedupe; generation and restart\nisolation; rate-limit/permission/disconnect failures; and the prohibition on\npersisted Socket Mode cursors. No live Slack credentials or workspace is required.\n",
|
|
103
|
+
"slack-onboarding.md": "# Slack notification onboarding\n\nThis is the managed Slack Socket Mode notification adapter. It is an SDK client:\nlocal GJC sessions continue to own loopback SDK endpoints, and Slack provides a\nper-session message thread for notifications, pending-action replies, and new\nuser turns.\n\n## Prerequisites\n\nCreate a Slack app in the target workspace, enable Socket Mode, and create an\napp-level token with the Socket Mode connection scope. Install the app in the\nworkspace and invite it to the selected channel. Configure only the scopes and\nevent subscriptions the adapter needs:\n\n- `chat:write` to post session roots, replies, and closure markers\n- `channels:history` for a public channel, or the corresponding history scope\n for the channel type in use\n- the message event subscription for the selected channel type\n- Socket Mode enabled for Events API delivery\n\nKeep the selected channel private to people authorized to see local session\nmetadata. Do not add broad workspace scopes or use an app token for ordinary Web\nAPI calls.\n\n## Configure the adapter\n\n`gjc notify setup slack` is non-interactive. It requires these flags:\n\n- `--slack-bot-token`\n- `--slack-app-token`\n- `--slack-workspace-id`\n- `--slack-channel-id`\n- `--slack-authorized-user-id` for the single Slack user authorized to submit thread messages and `/sdk` commands\n\nWithout `--slack-authorized-user-id`, the adapter remains outbound-only: every inbound envelope is acknowledged but denied before it can create a durable claim or reach an SDK endpoint. The user ID is an identifier, not a secret. It also accepts `--redact`. Provide secret values from an approved local secret mechanism, not shell history, committed configuration, tickets, screenshots, or chat. Setup writes:\n\n- `notifications.enabled = true`\n- `notifications.slack.enabled = true` (durable desired intent)\n- `notifications.slack.botToken`\n- `notifications.slack.appToken`\n- `notifications.slack.workspaceId`\n- `notifications.slack.channelId`\n- `notifications.slack.authorizedUserId` when configured\n- `notifications.redact = true` when requested\n\n`gjc notify status` reports Slack completeness, repair/quarantine state, desired intent, effective enablement, destination identifiers, and masked token values. It is status output, not a credential recovery mechanism. A successful durable save is not rolled back when later daemon activation fails; the command reports the saved-but-runtime-degraded outcome and exits nonzero. In `/settings`, bot/app secret edits are explicit `keep`, `replace`, or `remove`; removing either required token turns Slack desired intent off without changing Telegram, Discord, or the global master.\n\n## Socket Mode, threads, and resume\n\nThe daemon validates the configured workspace, channel, and paired user before durably claiming an inbound effect or sending its Socket Mode acknowledgement. The durable claim records the paired actor identity, replay identity, protected-effect reference, and captured endpoint generation; it never records Socket Mode cursors, endpoint tokens, or message bodies. Rejected, bot-authored, unauthorized, and already-claimed envelopes are acknowledged without an SDK endpoint call.\n\nWithin an active mapped thread, plain text answers the latest pending ask when\none exists; otherwise it starts a new `user_message` turn. `/sdk` prefixes retain\ntheir command behavior. Idle notifications are status-only and never become\nreplyable actions. Messages outside the active root thread remain ignored, so a\nshared channel does not route unrelated conversation or other bots into GJC.\n\nAcknowledgement latency is therefore bounded by local durable-claim work rather\nthan SDK availability or command execution. After the ACK, the worker dispatches\nthe claimed effect asynchronously; a restart can replay the claim, and a retry\ncannot create a second injection. Do not treat an ACK as confirmation that the SDK\noperation completed.\n\nEach session starts with one root message. Root creation uses a caller-generated\nclient message ID scoped to the configured workspace and channel, plus\nreconciliation lookup, preventing both a duplicate root after an uncertain post\nand accidental reuse of a root from a previous home channel. When a session\ncloses, the daemon posts a closure marker. A resume starts a new immutable root,\nso replies to the old root are rejected and cannot steer the resumed session.\n\nEvent IDs, retried deliveries, and interaction/message identifiers are\ndeduplicated in the durable claim before input is injected into the captured\ncurrent endpoint generation. `event_context` is retained as routing metadata but\nis not a dedupe key because Slack can reuse it across distinct messages in the\nsame channel context. After a Socket Mode reconnect, Slack may redeliver an\nenvelope; the new delivery is acknowledged after its claim is recognized and\ncannot cause a second injection.\n\n## Adopting an existing thread\n\nStock startup publishes a session's readiness immediately, so the daemon\nsurfaces the session and creates its own root before an operator could name an\nexisting one. Adopting an existing root therefore has an explicit, opt-in\nthree-phase lifecycle. Configuration is never part of it: the workspace and\nchannel come from `gjc notify setup` alone, and the operator supplies only a\nsession id and a thread timestamp.\n\n```text\nprepare session authority → bind the existing root through the live daemon → activate readiness\n```\n\n1. **Prepare.** Start the session prepared. A manually started session opts in\n with `GJC_NOTIFY_BIND_EXISTING_THREAD=1` in its environment; a broker\n lifecycle-managed session is prepared by its launch request instead (see\n below). Either way the session publishes its endpoint and registers with the\n broker exactly as usual, so its id and endpoint generation are discoverable\n authority, but it withholds the replayable `session_ready` signal. An\n attached daemon has nothing to surface, so no root is posted.\n2. **Bind.** `gjc notify bind-thread --session-id <id> --thread-ts <root>`\n adopts the existing root through the running daemon owner, exactly as it does\n for any live session. The CLI never writes the mapping store itself: it\n proves the configured target and the exact current owner, then submits the\n mutation over the per-request chat-daemon command channel that owner serves\n in place. A reported success is accepted only after this process observes the\n exact mapping in the durable conversation store, so a stale or forged\n `status:\"ok\"` answer is reported as `binding_outcome_unknown` rather than as a\n success.\n3. **Activate.** `gjc notify activate-thread --session-id <id>` asks\n `SessionRouter` to validate the exact indexed endpoint generation and invoke\n the session host internally. The host authorizes readiness against the\n provider-owned mapping through a redacted `{sessionId, endpointGeneration}`\n gate: activation before the binding is applied is refused (`not_bound`) with\n no grace period, and activation is idempotent, so an exact retry answers\n `already` rather than publishing a second readiness signal. The CLI never\n reads an endpoint URL/token or constructs an SDK client. When readiness is\n published, Slack adopts the bound root and posts zero replacement roots.\n\nThe opt-in is per session and explicit: only the exact value `1` prepares a\nsession, and a session without it keeps the stock immediate-ready root. The\nexisting global (`notifications.enabled`, `GJC_NOTIFICATIONS=0`) and per-session\nopt-outs are unchanged and still authoritative.\n\nPreparation has exactly two authorities and they never overlap. A manually\nstarted session uses the environment opt-in above. A broker lifecycle-managed\nsession is prepared only by the session-scoped `readiness: \"deferred\"` intent on\nits own `session.create` request: the child then publishes a distinct\n`session_prepared` signal, the lifecycle wait completes on that instead of\nreadiness, and the create receipt reports `readiness: \"prepared\"`. The\nenvironment opt-in is refused for lifecycle-managed sessions, so an inherited\nprocess-global flag can never silently defer a broker-created session.\n\nEither authority additionally requires a configured, session-enabled Slack\ntarget, because the activation gate is the existing-thread presentation\nmapping. The mapping stays provider-owned, while `SessionRouter` alone proves\nthe current endpoint generation and performs activation. A preparation request\nthat cannot build the configured mapping gate fails closed — the lifecycle child\nsettles a startup failure and the environment opt-in throws — rather than\ndegrading to ordinary immediate readiness or handing back a prepared session\nthat could activate with no binding at all.\n\nThrough the Coordinator MCP surface the same three phases are\n`gjc_coordinator_start_session` with `prepare_existing_thread: true` (which\nrejects an initial prompt and returns the session at state `prepared`), the\nunchanged `gjc notify bind-thread` command, and\n`gjc_coordinator_activate_session`. The Coordinator never writes the mapping\nstore: it proves exact endpoint authority and delegates to the same activation\nexchange the CLI uses, and durable session state only becomes ready once the\nsession itself proves `activated`/`already`. A prepared session refuses\n`gjc_coordinator_send_prompt` until it is activated.\n\n### Trust boundary\n\nThe command channel proves *correlation*, never authorship: every field a\nresponse echoes is copied verbatim out of the plaintext request published beside\nit in the daemon's own owner-only command directory. GJC trusts same-UID local\nprocesses, so nothing here defends against a hostile process running as the same\nuser; what it does guarantee is that a stale or forged answer with no matching\ndurable mapping never becomes a reported success.\n\n## Operational safety\n\nTreat rate limits, permission failures, and Socket Mode disconnects as transport\nfailures. Let the managed daemon reconnect or reconcile; do not run a competing\nSocket Mode consumer against the same app/state, manually modify conversation\nstate, persist delivery cursors, expose loopback endpoints, or use Slack as a\ngeneral remote shell.\n\nThe adapter only sends notifications and routes SDK replies. It does not support\nprovider registration, retaining endpoint credentials, or arbitrary remote\ncontrol.\n\n## Verification boundary\n\nAcceptance coverage uses an injectable fake Slack provider plus a production\nSession SDK host boundary proof. It covers durable-claim-before-acknowledgement\nfor accepted, rejected, duplicate, and reconnect-redelivered envelopes; root-post\nreconciliation; event/retry/context/interaction dedupe; generation and restart\nisolation; rate-limit/permission/disconnect failures; and the prohibition on\npersisted Socket Mode cursors. No live Slack credentials or workspace is required.\n",
|
|
104
104
|
"speech-to-text.md": "# Speech-to-text\n\nGajae-Code can record microphone audio, transcribe it locally with OpenAI Whisper, and insert the result into the interactive composer. It does not submit the transcription automatically, so you can edit it before sending.\n\n## Quick start\n\n1. Install or check the local dependencies:\n\n ```sh\n gjc setup stt\n gjc setup stt --check\n ```\n\n2. Enable speech-to-text:\n\n ```sh\n gjc config set stt.enabled true\n ```\n\n Alternatively, open `/settings` in an interactive session, select **Interaction**, and enable **Speech-to-Text**. The first `false` → `true` transition checks the recorder, Python, and Whisper installation immediately. Missing Whisper dependencies are installed with progress in the status line; if setup fails, GJC disables STT again and shows the actionable error.\n\n3. In the composer, press **Alt+H** once to start recording. Press **Alt+H** again to stop and transcribe.\n\n4. Review the text inserted into the composer, then press **Return** to send it.\n\nRun `/hotkeys` to see the active shortcut after user remaps or extensions are loaded.\n\nIf Alt/Option is not reaching GJC, press **Ctrl+P**, select **Toggle speech-to-text**, and repeat the action to stop and transcribe. This command-palette path does not depend on an Alt key sequence.\n\n## macOS keyboard and permissions\n\nOn macOS, **Alt+H** means **Option+H** (`⌥H`). The terminal must forward Option as Meta/Esc or use an enhanced keyboard protocol. In Apple Terminal, enable **Settings > Profiles > Keyboard > Use Option as Meta key** for the active profile.\n\nIn Ghostty, add this to `~/.config/ghostty/config`, then reload the configuration or restart Ghostty:\n\n```ini\nmacos-option-as-alt = true\n```\n\nWithout that setting, Option+H may arrive as the composed Unicode character `˙`, which GJC correctly treats as text rather than an Alt+H shortcut.\n\nThe first recording may cause macOS to request microphone access for the terminal application. Grant access under **System Settings > Privacy & Security > Microphone**. Restart the terminal after changing the permission.\n\nIf no recorder is installed, use Homebrew:\n\n```sh\nbrew install sox\n# or\nbrew install ffmpeg\n```\n\n## Linux and Windows recorders\n\nOn Debian or Ubuntu, install either supported recorder:\n\n```sh\nsudo apt install sox\n# or\nsudo apt install ffmpeg\n```\n\nWindows has a PowerShell recording fallback. SoX or FFmpeg can provide better recording support when the fallback is unsuitable.\n\n## Models and language\n\nThe default model is `base.en`, configured for English. Change **Speech Model** in `/settings` when you need a different speed/accuracy tradeoff. Multilingual models omit the `.en` suffix.\n\nThe first transcription with a model may take longer while Whisper downloads that model. Later transcriptions reuse the local model cache.\n\n## Troubleshooting\n\n- **The shortcut does nothing:** confirm `stt.enabled` is on, run `/hotkeys`, and verify the terminal forwards Alt/Option.\n- **Dependency check fails:** run `gjc setup stt --check` and follow the platform-specific recorder, Python, or Whisper diagnostic.\n- **No speech detected or the recording is empty:** check the operating-system microphone permission and the selected/default input device.\n- **Transcription is slow:** select a smaller Whisper model such as `tiny.en` or `base.en`.\n- **Wrong language:** set `stt.language` and choose a multilingual model such as `base`, `small`, or `medium`.\n\n## Remap the shortcut\n\nUser keybindings live at `~/.gjc/agent/keybindings.json`. For example:\n\n```json\n{\n \"app.stt.toggle\": \"f6\"\n}\n```\n\nOn compact Mac keyboards, the physical chord may be **Fn+F6**. A function key avoids Option composed-character behavior and control-code collisions such as Ctrl+H.\n\nSee [Keybindings](./keybindings.md) for chord syntax and terminal-specific behavior.\n",
|
|
105
105
|
"standalone-mcp.md": "# Standalone MCP configuration\n\n`gjc mcp add` writes the definition supplied on that invocation to GJC's own MCP config (`~/.gjc/agent/mcp.json` by default, or `./.gjc/mcp.json` with `--project`). `gjc mcp list` and `gjc mcp remove` print redacted definitions with source scope and runtime status. Enabled registrations are consumed by ordinary standalone sessions at startup (conventional autoload).\n\n## Conventional autoload\n\nOrdinary top-level standalone sessions (`gjc`, `gjc --tmux`, print/text/json modes) discover and connect MCP servers from GJC's own native config scopes only:\n\n| Source | Scope | Notes |\n| --- | --- | --- |\n| `.gjc/mcp.json`, `.gjc/.mcp.json` | project | Native GJC config; written by `gjc mcp add --project`. |\n| `~/.gjc/agent/mcp.json`, `~/.gjc/agent/.mcp.json` | user | Native GJC config; written by `gjc mcp add`. |\n\nUser scope is the agent directory, not a fixed home path: an agent-directory profile (`GJC_CODING_AGENT_DIR`, an SDK session's `agentDir`) moves discovery, `gjc mcp add`, and the `disabledServers` denylist together, so a profile always autoloads its own registrations and never the default profile's.\n\nPrecedence per server name is deterministic: the native project scope wins over the native user scope on a name collision. Plugin-bundle MCP servers (from installed GJC plugins) override conventional servers with the same name; they are a validated, always-on product surface.\n\nClaude Code and Codex MCP files (project `.claude/mcp.json` / `.claude/.mcp.json`, `.codex/config.toml` `[mcp_servers.*]`, and their user-global counterparts) are **import sources, not runtime authorities**: sessions never load them at startup. A bounded compatibility layer normalizes them into the same internal MCP contract, and an explicit import transaction writes the normalized definitions into the chosen `.gjc` scope (the `/extensions` import surface). `~/.claude`, `~/.codex`, and other foreign user-home configs are never read.\n\n### Which servers load\n\nA server is loaded at startup when all of the following hold:\n\n- the server is not marked `enabled: false`;\n- the server name is not in the `disabledServers` list of either native config scope (`<agent dir>/mcp.json` or `./.gjc/mcp.json`);\n- the server is not marked `autoload: false` (autoload defaults to true; `autoload: false` keeps a server configured for on-demand `/mcp` connection);\n- project-scope servers load by default; setting `mcp.enableProjectConfig` explicitly to `false` in settings disables every project-scope source for that environment.\n\nMalformed or unparseable definitions are skipped fail-closed: they are never partially loaded, a warning is emitted, and the session continues with the remaining valid servers. A server that fails to connect reports an error entry and the session continues.\n\n### Opt out\n\nPass `--no-mcp` to skip conventional autoload for one session (plugin-bundle MCPs and exact-file `--mcp-config` remain governed by their own surfaces). `--no-mcp` and `--mcp-config` are mutually exclusive.\n\n### Subagents and lifecycle\n\nTop-level sessions own their MCP manager and clean up server processes on session end. Subagents inherit the parent session's manager facade: they never spawn duplicate server processes and never take ownership of cleanup.\n\n## Use an explicit config\n\nA caller can opt one top-level standalone session into one trusted config file instead of conventional autoload:\n\n```bash\ngjc --mcp-config /absolute/path/to/mcp.json\n```\n\nThe path must be absolute and identify a regular file directly; symbolic links and other indirection are rejected. GJC reads the file through one open handle and rejects it if the path, file identity, size, or modification metadata changes during the read. Exact-file mode **replaces** conventional autoload: it exposes only that file's MCP tools and does not overlay `.gjc/mcp.json` registrations from either scope. GJC owns the server processes for that session. It does not load server prompts, resources, instructions, sampling, or other config files. Expected read, parse, validation, and connection failures emit one sanitized warning and continue. Unexpected errors and final-catalog tool-name collisions clean up and abort startup.\n\nThere is no MCP config reload while the session runs except `/mcp reload` in sessions without plugin-bundle MCP servers, and no subagent inheritance of exact-file tools beyond the parent session's exposed catalog.\n\n## Supported integrations\n\n| Need | Use | Notes |\n| --- | --- | --- |\n| Register servers for every standalone session | `gjc mcp add <name> ...` | Conventional autoload in user scope; `--project` scopes to the current project. |\n| Trust one MCP config for one standalone session | `gjc --mcp-config /absolute/path/to/mcp.json` | Exact-file, top-level, tools-only opt-in; GJC owns cleanup; replaces autoload. |\n| Disable conventional autoload for one session | `gjc --no-mcp` | Skips native `.gjc` user/project discovery; plugin-bundle and exact-file surfaces are unaffected. |\n| External bot or multi-session controller | [Coordinator MCP](./hermes-mcp-bridge.md) | Coordinator MCP exposes GJC lifecycle and coordination tools. |\n| External session control | [SDK session CLI](./sdk-session-cli.md) or a managed adapter | Broker-bound controls and opaque Router attachments; no direct endpoint transport. |\n| Editor/ACP client owns MCP servers | ACP via `gjc --mode acp` or `gjc acp` | ACP remains a stdio editor protocol. |\n| Codex / Claude Code delegation plugin | [Canonical gajae-code plugin](./hermes-mcp-bridge.md) | Installs Coordinator MCP plus GJC delegation commands. |\n\n## Boundary\n\nStandalone GJC does not inherit user-home MCP configurations from Claude Code, Codex, OpenCode, or other tools (`~/.claude`, `~/.codex`, and similar user-global configs are never read). MCP servers often carry credentials, filesystem reach, browser state, approval semantics, and lifecycle that belong to the configuring host. Claude/Codex MCP files are normalized only through the bounded compatibility layer on explicit import, and the only MCP config read from the user's home directory at session startup is GJC's own `~/.gjc/agent/mcp.json` (or the active agent directory when a profile overrides it).\n\n`--mode rpc`, `--mode rpc-ui`, `--mode bridge`, and `gjc sdk serve` have been removed. Do not use the former RPC host-tool protocol to connect an MCP server; use Coordinator MCP, the [SDK session CLI](./sdk-session-cli.md), or a managed adapter for supported external control.\n\n## Related docs\n\n- [SDK machine interfaces](./sdk.md)\n- [Coordinator MCP bridge](./hermes-mcp-bridge.md)\n- [External control surface readiness](./external-control-readiness.md)\n",
|
|
106
106
|
"streamdeck-integration-guide-with-cmux.md": "# Stream Deck integration guide with cmux\n\nThis guide captures a production-style Elgato Stream Deck control surface for Gajae-Code (`gjc`) running inside [cmux](https://github.com/manaflow-ai/cmux). It is formatted as an installable AI skill template so an operator or coding agent can reproduce, audit, repair, or extend the integration without relying on undocumented UI automation.\n\nThe installable skill body starts at the first frontmatter marker. To install it as a user skill:\n\n```sh\nmkdir -p ~/.gjc/agent/skills/streamdeck-cmux\nsed -n '/^---$/,$p' docs/streamdeck-integration-guide-with-cmux.md \\\n > ~/.gjc/agent/skills/streamdeck-cmux/SKILL.md\n```\n\nFilesystem skill discovery is on by default, so no configuration is needed. Start a new GJC session and invoke `/skill:streamdeck-cmux`. To stop loading personal skills later, use `gjc config set skills.trustUserSkills false` (see [docs/skills.md](./skills.md)).\n\n---\nname: streamdeck-cmux\ndescription: Configure, operate, verify, or repair an Elgato Stream Deck integration for Gajae-Code sessions hosted in cmux.\nargument-hint: \"[install|audit|repair|extend]\"\nlevel: 2\n---\n\n# Gajae-Code Stream Deck + cmux operator skill\n\n## Purpose\n\nBuild a Stream Deck control surface that treats cmux as the terminal host, GJC as the interactive coding runtime, and the GJC SDK as the authoritative machine interface for pending questions.\n\nThe control surface should:\n\n- navigate cmux panes and surface tabs;\n- open fixed project and home-directory terminal tabs;\n- create worktree-scoped GJC sessions;\n- change the model profile of the focused GJC session;\n- invoke common GJC skills without submitting them prematurely;\n- send precise keyboard controls such as `Shift+Tab`, `Esc`, and `Enter`;\n- render and answer the focused session's SDK questions;\n- open and close native cmux terminal surfaces;\n- reuse existing Chrome or Safari tabs for ordinary web shortcuts;\n- use distinct, readable mascot artwork for each operation;\n- preserve the operator's original Stream Deck profile and unrelated repository work.\n\n## Do not use when\n\n- cmux is not the terminal host;\n- the Stream Deck application or hardware is unavailable;\n- the target GJC session has SDK hosting disabled with `GJC_SDK_DISABLE=1` and SDK question answering is required;\n- the requested action would overwrite a shared checkout containing unrelated work;\n- the operator expects generic UI automation instead of deterministic cmux and SDK commands.\n\n## Safety invariants\n\n1. Back up the full Stream Deck profile before every structural layout change.\n2. Preserve the original/default profile instead of reconstructing it manually.\n3. Never log or commit SDK tokens, provider API keys, browser credentials, or endpoint discovery files.\n4. Resolve the focused cmux surface with `cmux identify --no-caller`; do not infer focus only from tree decorations.\n5. Send GJC-only controls only when the focused surface title starts with `GJC:`.\n6. Use `action_needed.id` as the only authority for a generic SDK question reply.\n7. Do not answer stale, resolved, hidden, non-focused, free-text, or unsupported controlled questions from fixed answer keys.\n8. Send `Shift+Tab` as one atomic key event. Do not emulate it with separately delivered `Esc`-prefixed text.\n9. Do not create duplicate browser tabs when an existing Chrome or Safari tab matches.\n10. Reuse a focused non-GJC terminal when the operator explicitly wants an in-place worktree launch.\n11. Keep Stream Deck profiles, local plugin installations, generated artwork, and SDK state outside version control. Repository-local `.gjc/state/` is gitignored and is the authoritative SDK discovery location; do not move, delete, or copy it elsewhere.\n\n## Reference environment\n\nThe implementation described here was validated with:\n\n- Elgato Stream Deck application `7.5.1`;\n- Stream Deck device model `20GBA9901`;\n- Gajae-Code `0.12.21`;\n- cmux installed at `/Applications/cmux.app`;\n- cmux CLI at `/Applications/cmux.app/Contents/Resources/bin/cmux`;\n- GJC installed at `~/.local/bin/gjc`;\n- official mascot source at `assets/character.png`.\n\nTreat versions and absolute paths as environment inputs, not permanent product constants.\n\n## Architecture\n\n```text\nStream Deck hardware\n -> Elgato Stream Deck application\n -> native Stream Deck plugin\n -> cmux CLI / socket RPC\n -> GJC SDK WebSocket endpoints\n -> local launch helpers\n -> generated key images\n```\n\nUse a native Stream Deck plugin instead of a collection of shell-command actions. The plugin provides dynamic titles, per-key settings, hold/tap handling, SDK subscriptions, focused-session guards, question-state rendering, deterministic cmux routing, and success/error feedback.\n\nA representative local installation is:\n\n```text\n~/.local/share/gjc-streamdeck-plugin/\n manifest.json\n plugin.js\n bin/plugin\n images/*.png\n\n~/Library/Application Support/com.elgato.StreamDeck/Plugins/\n dev.gajae.streamdeck.sdPlugin/\n manifest.json\n plugin.js\n bin/plugin\n images/*.png\n```\n\nKeep one editable source copy and synchronize it to the installed plugin directory. Verify deployment with `cmp` before restarting Stream Deck.\n\n## Preserve and separate profiles\n\nMaintain separate profiles for separate concerns:\n\n- `Default Profile`: the restored original profile;\n- `Daily Control`: browser, cmux, and active-session controls;\n- an optional session inventory profile when dedicated session slots are useful.\n\nBefore changing a profile:\n\n```sh\nstamp=\"$(date +%Y%m%d-%H%M%S)\"\nbase=\"$HOME/Library/Application Support/com.elgato.StreamDeck\"\nmkdir -p \"$base/ManualBackups\"\nditto -c -k --sequesterRsrc --keepParent \\\n \"$base/ProfilesV3/<profile>.sdProfile\" \\\n \"$base/ManualBackups/streamdeck-before-change-$stamp.zip\"\n```\n\nRestore from a known backup instead of reverse-engineering a damaged default profile.\n\n## Three-page operating model\n\n### Page 1: daily web shortcuts\n\nUse ordinary daily shortcuts here. Browser actions should:\n\n1. search every Chrome window and tab;\n2. search every Safari window and tab;\n3. focus an existing matching tab;\n4. create a Chrome tab only when neither browser contains a match.\n\nCompiled AppleScript applications are suitable when Stream Deck's built-in website action cannot enforce tab reuse. Match stable URL fragments rather than volatile titles.\n\n### Page 2: cmux navigation and session entry\n\n```text\nTAB PREV | TAB NEXT | NEW SESSION | CLOSE TAB | GJC FOCUS\nPANE PREV | PANE NEXT | VOICE | STEER | ESC X2\nBACK | PROJECT 1 | PROJECT 2 | HOME | NEXT\n```\n\n#### Navigation controls\n\n- `PANE PREV` / `PANE NEXT`: select the previous or next pane in the current workspace.\n- `TAB PREV` / `TAB NEXT`: select the previous or next surface in the focused pane.\n- `GJC FOCUS`: keep the text-focused visual style; when pressed, submit `proceed` plus `Enter` only to a focused `GJC:` surface.\n\n#### Session and surface controls\n\n- `NEW SESSION`: create a terminal surface and ask for a worktree name; a blank answer starts a plain `gjc` session, while a name starts `gjc --worktree <name>`. Do not select a profile here.\n- `CLOSE TAB`: close the focused cmux surface.\n- `VOICE`: invoke GJC's local Whisper speech-to-text action with a user remap to `Ctrl+H` on the focused `GJC:` surface.\n- `STEER`: send `Esc`, wait 100 ms, then send `Enter`.\n- `ESC X2`: send `Esc`, wait 100 ms, then send `Esc` again.\n\nA session-only launcher can be implemented as:\n\n```zsh\n#!/bin/zsh\nset -u\n\nprintf 'GJC worktree name (blank = plain session): '\nIFS= read -r worktree_name\nargs=()\n[[ -n \"$worktree_name\" ]] && args+=(--worktree \"$worktree_name\")\nexec \"$HOME/.local/bin/gjc\" \"${args[@]}\"\n```\n\nDo not prompt for a model profile here. Apply the profile after the GJC session starts.\n\n#### Frequent GJC project controls\n\nBind the first two project keys from GJC session history, not operator-specific absolute paths. Merge `gjc sdk session list` with saved top-level session headers under the agent session store, canonicalize managed worktree paths such as `<repo>/.worktrees/<name>` (and legacy `<repo>.gajae-code-worktrees/<name>`) back to `<repo>`, discard non-existent and non-Git directories outside the user's home, count sessions per canonical repository, and display the top two repositories. The third key always opens `$HOME`.\n\nEach project key shows the repository basename and session count. Pressing it creates a terminal surface in that repository. The `HOME` key creates a terminal surface in the user's home directory. Leave the cmux tab name automatic so a later `gjc` launch can publish its authoritative `GJC:` title.\n\n### Bundled source and assets\n\nThe repository-owned implementation lives at `integrations/streamdeck-cmux/`:\n\n- `plugin/` contains the native Stream Deck plugin source, launcher, worktree helper, and required 144-by-144 PNG assets;\n- `profile/page-2` and `profile/page-3` contain portable page manifests and page-owned artwork;\n- `install.sh` installs the plugin and creates an importable `.streamDeckProfile` bundle on the Desktop.\n\nRuntime paths are derived from `$HOME`, `import.meta.dir`, `PATH`, and optional environment overrides (`GJC_STREAMDECK_GJC`, `GJC_STREAMDECK_CMUX`, `GJC_STREAMDECK_WORKTREE`, `GJC_AGENT_DIR`, `GJC_STREAMDECK_LOG`). Never commit local profile databases, SDK endpoint files, tokens, or user-specific absolute project paths.\n\n### Page 3: focused GJC operations\n\n```text\nSET FRONTIER | SET GPT | SET GLM DS | KIMI GPT | BTW EXPLAIN\nRESUME | EXIT | PR TO DEV | THINK LEVEL | CLEAR CTX\nBACK | DEEP INTERVIEW | RALPLAN | ULTRAGOAL | NEXT\n```\n\n#### Model profile controls\n\nModel profile keys submit commands to the focused GJC editor:\n\n```text\n/model gajae-code/frontier-heavy\n/model gajae-code/gpt-heavy\n/model gajae-code/glm-deepseek\n/model gajae-code/kimi-gpt\n```\n\nA profile must exist and be available to the current session. The names shown above (`frontier-heavy`, `gpt-heavy`, `glm-deepseek`, `kimi-gpt`) are operator-defined examples, not bundled defaults; none of them ships with GJC. Provide matching definitions in `~/.gjc/agent/models.yml` or replace them with bundled profile names before the keys will work.\n\n#### Session controls\n\n- `RESUME`: submit `/resume` and open the saved-session selector.\n- `EXIT`: submit `/exit` for a clean GJC shutdown.\n- `PR TO DEV`: submit the operator macro `make a PR targeting dev and make it LGTM` plus `Enter`.\n- `THINK LEVEL`: send atomic `Shift+Tab` through `cmux send-key`.\n- `CLEAR CTX`: submit `/clear`, preserving the session ID while clearing context.\n- `BTW EXPLAIN`: submit `/btw 설명해봐 이거` for an ephemeral side question.\n\nThe PR macro is an operator convenience, not a policy bypass. GJC must still inspect repository rules, run required verification, use an isolated branch or worktree when appropriate, create a focused commit, and open a PR against `dev` only when that branch exists and is the repository's intended integration branch.\n\n#### Skill controls\n\nSkill keys type but do not submit:\n\n```text\n/skill:deep-interview\n/skill:ralplan\n/skill:ultragoal\n```\n\nLeaving the command in the editor allows the operator to add arguments before pressing `Enter`.\n\n## cmux command patterns\n\nUse the installed cmux CLI directly:\n\n```sh\nCMUX=/Applications/cmux.app/Contents/Resources/bin/cmux\n\n$CMUX identify --no-caller\n$CMUX tree --all\n$CMUX focus-panel --panel surface:7 --workspace workspace:1 --window window:1\n$CMUX new-surface --type terminal --pane pane:1 --focus true\n$CMUX close-surface --surface surface:7 --workspace workspace:1 --window window:1\n```\n\nA new surface response contains a `surface:<n>` reference. Capture that exact reference and use it for subsequent send, rename, focus, read-screen, or close operations.\n\nDo not use selected/active decorations from `cmux tree --all` as the sole focus authority. `cmux identify --no-caller` returns the actual focused window, workspace, pane, and surface.\n\n## Keyboard delivery\n\n### Text and Enter\n\n```sh\ncmux send \\\n --surface surface:7 \\\n --workspace workspace:1 \\\n --window window:1 \\\n 'make a PR targeting dev and make it LGTM'\n\ncmux send-key \\\n --surface surface:7 \\\n --workspace workspace:1 \\\n --window window:1 \\\n enter\n```\n\n### Voice (`Ctrl+H`)\n\nRemap local Whisper speech-to-text in `~/.gjc/agent/keybindings.json`:\n\n```json\n{\n \"app.stt.toggle\": \"Ctrl+H\"\n}\n```\n\nThe Stream Deck plugin sends atomic `ctrl+h` through `cmux send-key`. New GJC sessions load the remap; already-running sessions keep the keybindings they started with and should not be modified in place.\n\n### Shift+Tab\n\nSend `Shift+Tab` atomically:\n\n```sh\ncmux send-key \\\n --surface surface:7 \\\n --workspace workspace:1 \\\n --window window:1 \\\n 'shift+tab'\n```\n\nThe expected terminal byte sequence is:\n\n```text\n[27, 91, 90]\n```\n\nDo not send `\\x1b[Z` through a text API when the TUI may consume the leading escape independently and abort the active operation.\n\n### Steer and abort\n\n```text\nSTEER: Esc -> wait 100 ms -> Enter\nABORT: Esc -> wait 100 ms -> Esc\n```\n\nKeep these as distinct controls. The abort control should not require a hold unless the operator explicitly requests one.\n\n## SDK question answer pad\n\nEvery top-level GJC session publishes a loopback SDK discovery file:\n\n```text\n<repo>/.gjc/state/sdk/<sessionId>.json\n```\n\nThe file contains the session WebSocket URL and token. Connect with the token as a query parameter and never persist or log it elsewhere.\n\nDo not assume repositories are only one directory below a fixed workspace root. Resolve each live `gjc` process PID to its TTY and current working directory, then inspect that exact `<cwd>/.gjc/state/sdk/` directory. This includes managed `.worktrees` sessions and legacy `.gajae-code-worktrees` sessions.\n\nWhen the focused session emits:\n\n```json\n{\n \"type\": \"action_needed\",\n \"id\": \"act_9e31\",\n \"kind\": \"ask\",\n \"sessionId\": \"sess-1\",\n \"question\": \"Choose a target\",\n \"options\": [\"A\", \"B\"],\n \"recommendedIndex\": 1\n}\n```\n\ntemporarily replace all five top-row controls—the four profile keys plus `BTW EXPLAIN`—with:\n\n```text\nANSWER 1 | ANSWER 2 | ANSWER 3 | ANSWER 4 | ANSWER 5\n```\n\nRender the real option labels with bounded wrapping. Highlight the valid recommended index, but never decorate or modify the submitted answer value.\n\nReply with the exact active presentation ID:\n\n```json\n{\n \"type\": \"reply\",\n \"id\": \"act_9e31\",\n \"answer\": 1,\n \"token\": \"<session token>\",\n \"idempotencyKey\": \"streamdeck-act_9e31-1\"\n}\n```\n\nReturn to the ordinary profile controls only when `action_resolved` arrives for the **same presentation ID currently displayed**; an `action_resolved` for a different session can arrive while another question's pad is still active, so match the frame `id` against the displayed presentation before clearing it. If `reply_rejected` arrives, show an error and do not guess from question text, option text, workflow IDs, or earlier presentations.\n\nFor checkbox questions, negotiate `ask_controls_v1` in the client `hello` / replay request and require both `selectedOptionIndices` and an enabled or disabled typed `navigation_forward` control. Support up to four checkbox options because the fifth top-row key is reserved for `Done` or `Next`:\n\n```text\n☐ OPTION 1 | ☑ OPTION 2 | ☐ OPTION 3 | NO OPTION | DONE\n```\n\nPressing an option sends its numeric index against the exact current `action_needed.id`. GJC resolves that presentation and reissues a fresh one with updated `selectedOptionIndices`; replace the displayed ID and selection state rather than reusing the old ID. Pressing the fifth key sends the typed control:\n\n```json\n{ \"type\": \"reply\", \"id\": \"<current action id>\", \"answer\": { \"controlId\": \"navigation_forward\" }, \"token\": \"<session token>\" }\n```\n\nDo not infer controls from labels such as `Done` or `Next`; only use the negotiated control object and honor its `enabled` field.\n\nOnly display the fixed answer pad when the question belongs to the focused GJC session, the PID/TTY mapping is exact, and the action is still active. Supported shapes are:\n\n- one to five scalar options with no negotiated controls;\n- one to four checkbox options with `selectedOptionIndices` and a typed `navigation_forward` control.\n\nLeave free-text, checkbox questions with five or more options, malformed/missing controls, and other controlled asks to the native GJC UI.\n\n## Mascot artwork\n\nUse `assets/character.png` as the identity reference. Generate a distinct pose, expression, prop, and task scene for every key. Optimize for a 144-by-144 display:\n\n- dark background;\n- strong silhouette;\n- high-contrast border;\n- large central action;\n- short bottom label;\n- no small decorative text;\n- dim artwork behind dynamic titles such as the focused session name or folder label.\n\nSuitable task scenes include pane dividers, tabs, browser windows, model cores, emergency controls, git branches, approval checks, interview notebooks, planning blueprints, and goal summits.\n\nGenerate artwork through a configured image provider without embedding credentials in commands, logs, documentation, or committed files. Environment variables should contain only operator-managed values; commit neither the values nor local shell configuration.\n\n## Manifest and plugin behavior\n\nRepresent each key with an action UUID and small settings payload. A generic control action can dispatch by `settings.type`:\n\n```json\n{ \"name\": \"new-website-tab\", \"type\": \"newWebsiteTab\" }\n{ \"name\": \"folder-gajae\", \"type\": \"fixedFolder\", \"path\": \"$HOME/src/gajae-code\", \"label\": \"gajae-code\" }\n{ \"name\": \"set-kimi-gpt\", \"type\": \"command\", \"value\": \"/model gajae-code/kimi-gpt\", \"submit\": true, \"answerSlot\": 3 }\n{ \"name\": \"thinking-level\", \"type\": \"key\", \"value\": \"shift+tab\" }\n```\n\nUse separate actions only when Stream Deck behavior differs materially, such as cmux navigation, focused status, skill typing, steer, or double-escape abort.\n\n## Verification protocol\n\nAfter each behavioral change, verify the narrow observable contract.\n\n### Build and installation\n\n```sh\nbun build ~/.local/share/gjc-streamdeck-plugin/plugin.js \\\n --target=bun \\\n --outfile=\"$HOME/tmp/gjc-streamdeck-plugin-verify.js\"\n\ncmp ~/.local/share/gjc-streamdeck-plugin/plugin.js \\\n \"$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/dev.gajae.streamdeck.sdPlugin/plugin.js\"\n```\n\n### Layout\n\n- Every referenced image exists.\n- Moved actions have new action IDs.\n- Page navigation keys still point in the intended direction.\n- Dynamic title keys have `ShowTitle: true`.\n- Question answer slots are zero-based and unique.\n- Removed controls do not remain in another page or plugin manifest.\n\n### cmux behavior\n\nUse temporary surfaces and restore the original focus after each test:\n\n- pane previous/next;\n- tab previous/next;\n- terminal creation in the requested pane;\n- voice sends atomic `Ctrl+H` to a session that loaded the remap without inserting text;\n- fixed-folder `cd` behavior;\n- focused tab closure;\n- same-tab worktree prompting when required;\n- exact `Shift+Tab` bytes;\n- exact `Esc`, delay, and `Esc` sequence;\n- exact macro text plus carriage return.\n\n### SDK behavior\n\nUse a temporary token-authenticated SDK WebSocket server and a temporary GJC-titled cmux surface to prove:\n\n- discovery;\n- focused session mapping;\n- `action_needed` rendering;\n- option wrapping;\n- recommended-option highlighting;\n- exact zero-based reply;\n- `action_resolved` restoration;\n- stale/rejected reply handling.\n\n### Stream Deck restart\n\nAfter synchronizing the plugin:\n\n```sh\npkill -TERM -f '^/Applications/Elgato Stream Deck.app/Contents/MacOS/Stream Deck$' || true\nsleep 2\nopen -a '/Applications/Elgato Stream Deck.app'\n```\n\nConfirm the plugin reconnects, contexts render, and the active profile remains correct.\n\n## Troubleshooting\n\n### A GJC control shows an error\n\nCheck the focused cmux surface title. GJC-only commands intentionally fail closed unless the raw title starts with `GJC:`.\n\n### Worktree prompting does not appear\n\nConfirm the helper is executable. A blank name must invoke plain `gjc`; a non-empty name must invoke `gjc --worktree <name>` from a Git repository. Do not pass a filesystem path as the worktree name.\n\n### Think level aborts the operation\n\nThe integration is probably sending escape-prefixed text. Replace it with atomic `cmux send-key ... shift+tab`.\n\n### Question options do not appear\n\nCheck:\n\n- SDK hosting is enabled;\n- the endpoint PID is alive;\n- the token-authenticated WebSocket connected;\n- the focused surface maps to the endpoint TTY;\n- the focused session was retained even when the session inventory is capped;\n- the question has no more than five scalar options, or no more than four checkbox options plus a negotiated `navigation_forward` control.\n\n### A browser shortcut creates duplicates\n\nSearch all Chrome and Safari windows before creating a tab. Do not limit the search to the frontmost window.\n\n### Artwork is unreadable\n\nRemove small details, enlarge the action, darken the dynamic-title background, shorten the label, and render a complete two-page contact sheet before deployment.\n\n## Completion report\n\nReport only verified facts:\n\n- profile IDs or names changed;\n- page and coordinate layout;\n- plugin source and installed locations;\n- backup path;\n- exact cmux and SDK checks run;\n- number of plugin keys rendered;\n- remaining environment-specific paths or optional profiles;\n- failures that could not be reproduced or verified.\n",
|
|
@@ -111,7 +111,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
111
111
|
"tools/ask.md": "# ask\n\n> Prompts the interactive user for one or more choices or free-form answers.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/ask.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/ask.md`\n- Key collaborators:\n - `packages/coding-agent/src/config/settings-schema.ts` — `ask.timeout` / `ask.notify` defaults\n - `packages/coding-agent/src/modes/theme/theme.ts` — checkbox and tree glyphs for TUI rendering\n - `packages/coding-agent/src/tui.ts` — status-line rendering\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `questions` | `Question[]` | Yes | One or more questions. Empty arrays are rejected by schema and also guarded at runtime. |\n\n### `Question`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `id` | `string` | Yes | Stable identifier used in multi-question results. |\n| `question` | `string` | Yes | Prompt text shown to the user. |\n| `options` | `{ label: string }[]` | Yes | Explicit options. The UI always appends `Other (type your own)`; callers must not include it. |\n| `multi` | `boolean` | No | Enables multi-select mode. Default: `false`. |\n| `recommended` | `number` | No | Zero-based recommended option index. In single-select mode the label gets ` (Recommended)` appended in the UI. |\n\n## Outputs\n- Single-shot result.\n- `content[0].text` is plain text:\n - single question: `User selected: ...` and/or `User provided custom input: ...`\n - multiple questions: `User answers:` followed by one line per `id`\n- `details`:\n - single question: `{ question, options, multi, selectedOptions, customInput? }`\n - multiple questions: `{ results: QuestionResult[] }`, where each item includes `id`, `question`, `options`, `multi`, `selectedOptions`, and optional `customInput`\n- Cancellation and headless cases throw instead of returning a structured success result.\n\n## Flow\n1. `AskTool.createIf()` only registers the tool when `session.hasUI` is true; headless sessions never get it.\n2. `execute()` requires `context.ui`; if missing it aborts the context and throws `ToolAbortError(\"Ask tool requires interactive mode\")`.\n3. It reads `ask.timeout` from settings, converts seconds to milliseconds, and disables timeout entirely while plan mode is enabled (`packages/coding-agent/src/tools/ask.ts`).\n4. If `ask.notify` is not `off`, it sends a terminal notification: `Waiting for input`.\n5. For each question, `askSingleQuestion()` drives either:\n - single-select list + optional editor for `Other`\n - multi-select checkbox loop + `Done selecting` sentinel + optional editor for `Other`\n6. In multi-question mode, left/right arrow handlers enable back/forward navigation between questions and preserve prior selections.\n7. If a timeout fires before any selection/custom input, the tool auto-selects the recommended option, or the first option when no valid `recommended` index exists.\n8. If the user cancels without timeout, `execute()` aborts the tool context and throws `ToolAbortError(\"Ask tool was cancelled by the user\")`.\n9. On success it formats human-readable text plus structured `details`; the TUI renderer uses `details` for rich display.\n\n## Modes / Variants\n- Single question: returns flattened `details` fields for one question.\n- Multiple questions: returns `details.results[]` and allows back/forward navigation across questions.\n- Single-select: one option or custom input.\n- Multi-select: toggled checkbox list, `Done selecting` sentinel only when forward navigation is not active.\n\n## Side Effects\n- User-visible prompts / interactive UI\n - Opens a selection dialog via `context.ui.select(...)`.\n - Opens a text editor dialog via `context.ui.editor(...)` for `Other`.\n - Sends a terminal notification unless `ask.notify=off`.\n- Session state\n - Reads plan-mode state to disable timeouts.\n - Calls `context.abort()` on headless use or user cancellation.\n- Background work / cancellation\n - Wraps UI waits in `untilAborted(...)` so abort signals interrupt pending dialogs.\n\n## Limits & Caps\n- `questions` must contain at least 1 item (`askSchema` in `packages/coding-agent/src/tools/ask.ts`).\n- `ask.timeout` default is `30` seconds; `0` disables timeout (`packages/coding-agent/src/config/settings-schema.ts`).\n- Prompt guidance says provide 2-5 options, but code does not enforce that (`packages/coding-agent/src/prompts/tools/ask.md`).\n- Timeout only applies to the option picker; once the user chooses `Other`, the editor has no timeout (`packages/coding-agent/src/prompts/tools/ask.md`).\n\n## Errors\n- Missing interactive UI: throws `ToolAbortError(\"Ask tool requires interactive mode\")`.\n- User cancels picker/editor without timeout: throws `ToolAbortError(\"Ask tool was cancelled by the user\")`.\n- Abort signal during input: converted to `ToolAbortError(\"Ask input was cancelled\")`.\n- Empty `questions` at runtime returns a text error payload instead of throwing: `Error: questions must not be empty`.\n\n## Notes\n- `recommended` is only a UI hint; invalid indexes are ignored.\n- In single-select mode the returned `selectedOptions` value strips the appended ` (Recommended)` suffix.\n- Multi-select results preserve selection order by `Set` insertion order, not original option order after arbitrary toggles.\n- Option labels and prompt text are returned verbatim in `details`; the tool does not interpret them beyond UI affordances like `Other` and ` (Recommended)`.\n",
|
|
112
112
|
"tools/ast-edit.md": "# ast_edit\n\n> Preview and apply structural rewrites over source files via native ast-grep.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/ast-edit.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/ast-edit.md`\n- Key collaborators:\n - `crates/pi-natives/src/ast.rs` — native rewrite planning and file mutation\n - `crates/pi-natives/src/language/mod.rs` — language aliases and extension inference\n - `packages/coding-agent/src/tools/path-utils.ts` — path/glob parsing and multi-path resolution\n - `packages/coding-agent/src/tools/resolve.ts` — preview/apply queueing\n - `packages/coding-agent/src/tools/render-utils.ts` — parse-error dedupe and display caps\n - `packages/coding-agent/src/utils/file-display-mode.ts` — hashline vs line-number diff references\n - `packages/coding-agent/src/hashline/hash.ts` — stable hashline diff anchors\n - `packages/natives/native/index.d.ts` — JS-visible native binding contract\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `ops` | `{ pat: string; out: string }[]` | Yes | One or more rewrite rules. `pat` must be non-empty. Duplicate `pat` values fail before native execution. Empty `out` deletes the matched node. |\n| `paths` | `string[]` | Yes | One or more files, directories, globs, or internal URLs with backing files. Empty entries are rejected. Globs are forbidden for internal URLs. |\n\nShared AST pattern grammar and language catalog: see [`ast_grep`](./ast-grep.md#inputs).\n\n- `ast_edit` uses the same `$NAME`, `$_`, `$$$NAME`, and `$$$` metavariable semantics.\n- The tool prompt adds rewrite-specific constraints:\n - metavariable names must be uppercase and must stand for whole AST nodes,\n - captures from `pat` are substituted into `out`,\n - each rewrite is a 1:1 structural substitution; one capture cannot expand into multiple sibling nodes unless the grammar itself permits that expansion at that position.\n\n## Outputs\n- Single-shot preview result from `ast_edit` itself.\n- Model-facing `content` is one text block showing proposed edits, grouped by file for directory/multi-file runs.\n - Each change renders as two lines: `-REF|before` and `+REF|after` in hashline mode, or `-LINE:COLUMN before` / `+LINE:COLUMN after` when hashlines are off.\n - Only the first line of each `before`/`after` snippet is shown, truncated to 120 characters in the wrapper.\n - `Limit reached; narrow paths.` and formatted parse issues are appended when applicable.\n- If no rewrites match, text is `No replacements made` plus formatted parse issues when present.\n- `details` includes aggregate preview metadata:\n - `totalReplacements`, `filesTouched`, `filesSearched`, `applied`, `limitReached`\n - optional `parseErrors`, `scopePath`, `files`, `fileReplacements`, `displayContent`, `meta`\n- The tool always previews first (`applied: false` in the direct result). Actual file writes happen only later through `resolve(action: \"apply\", ...)`.\n- When preview produced replacements, `ast_edit` also queues a pending `resolve` action. Successful apply returns a separate `resolve` result, not another `ast_edit` result.\n\n## Flow\n1. `AstEditTool.execute()` validates each op in `packages/coding-agent/src/tools/ast-edit.ts`:\n - empty `pat` fails,\n - at least one op is required,\n - duplicate `pat` values fail,\n - ops are converted to a `Record<pattern, replacement>`.\n2. The wrapper reads `GJC_MAX_AST_FILES` via `$envpos(..., 1000)` and uses that as the native `maxFiles` cap for both preview and apply.\n3. Path normalization, internal URL handling, missing-path partitioning, and multi-path resolution follow the same `path-utils.ts` flow as `ast_grep`.\n4. The wrapper stats the resolved base path to decide whether to render grouped directory output.\n5. `runAstEditOnce(...)` always runs native `astEdit(...)` with `dryRun: true` and `failOnParseError: false` on the first pass.\n6. Native `ast_edit` in `crates/pi-natives/src/ast.rs`:\n - normalizes the rewrite map and sorts rules by pattern string,\n - resolves strictness (`smart` by default),\n - collects candidate files from a file or gitignore-aware directory scan,\n - infers a single language for the whole call unless `lang` was supplied,\n - compiles every rewrite pattern for that language,\n - parses each file, skips files with syntax-error trees, collects `replace_by(...)` edits for every match, enforces replacement and file caps, and returns textual before/after slices plus source ranges.\n7. The TS wrapper deduplicates parse errors, groups changes by file, and renders preview diff lines.\n8. If preview found replacements and `applied` is false, `queueResolveHandler(...)` registers a forced `resolve` action and injects a `resolve-reminder` steering message.\n9. On `resolve(action: \"apply\")`, the queued callback reruns the same rewrite set with `dryRun: false`, recomputes counts, and rejects the apply as an error if the live result no longer matches the preview (`stalePreview`).\n10. On a non-stale apply, the callback returns `Applied N replacements in M files.`; on discard, `resolve` returns a discard message without mutating files.\n\n## Modes / Variants\n- Single file: preview or apply against one file.\n- Directory + optional glob: native scan walks the directory, then filters by compiled glob.\n- Multiple explicit paths/globs: wrapper unions them into one synthetic scope or runs per-target native calls when paths only meet at root.\n- Internal URL inputs: only supported when the router resolves them to a backing file path.\n- Preview mode: always the direct `ast_edit` tool result.\n- Apply mode: only reachable through the queued `resolve` callback after a preview.\n- Hashline output mode vs plain line/column mode: controlled by `resolveFileDisplayMode()`.\n\n## Side Effects\n- Filesystem\n - Preview reads files and scans directories.\n - Apply rewrites files in place with `std::fs::write(...)`, but only when the computed output differs from the original source.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Queues a one-shot forced `resolve` tool choice through `queueResolveHandler(...)`.\n - Adds a `resolve-reminder` steering message.\n- User-visible prompts / interactive UI\n - Direct `ast_edit` results are previews.\n - Follow-up apply/discard is exposed through the hidden `resolve` tool.\n- Background work / cancellation\n - Native preview/apply work runs on a blocking worker via `task::blocking(...)`.\n - Cancellation and optional native timeout are cooperative through `CancelToken::heartbeat()`.\n\n## Limits & Caps\n- File cap exposed by the wrapper: `GJC_MAX_AST_FILES`, default `1000`, in `packages/coding-agent/src/tools/ast-edit.ts`.\n- Native `maxFiles` and `maxReplacements` are both clamped to at least `1` when provided in `crates/pi-natives/src/ast.rs`.\n- The wrapper never sets `maxReplacements`; native behavior therefore defaults to effectively unbounded replacements for a run.\n- Parse issues are rendered with at most `PARSE_ERRORS_LIMIT = 20` lines in `packages/coding-agent/src/tools/render-utils.ts`; `details.parseErrors` is deduplicated but not capped.\n- Directory scans use `include_hidden: true`, `use_gitignore: true`, and skip `node_modules` unless the glob text explicitly mentions `node_modules` in `crates/pi-natives/src/ast.rs`.\n- No separate glob-expansion count cap exists. Candidate count is whatever the resolved path/glob expands to after gitignore filtering, then native `maxFiles` stops mutations after the configured number of touched files.\n- Preview text truncates each rendered `before` and `after` first line to 120 characters in `packages/coding-agent/src/tools/ast-edit.ts`.\n\n## Errors\n- TS wrapper throws `ToolError` for empty patterns, duplicate rewrite patterns, empty path entries, unsupported internal-URL globs, internal URLs without `sourcePath`, and missing paths.\n- Native code returns hard errors for:\n - inability to infer one language across all candidates when `lang` is absent,\n - unsupported explicit `lang`,\n - bad glob compilation or unreadable search roots,\n - overlapping computed edits (`Overlapping replacements detected; refine pattern to avoid ambiguous edits`),\n - out-of-bounds edit ranges or non-UTF-8 replacement text,\n - write failures during apply,\n - cancellation or timeout.\n- With `failOnParseError: false` (the wrapper always uses this), pattern compile failures and file parse failures become `parseErrors` instead of aborting the whole run.\n- If every rewrite pattern fails to compile, native `ast_edit` returns a successful zero-replacement result with `parseErrors` populated.\n- Files containing tree-sitter error nodes are skipped for rewriting; they do not get partial edits.\n- Apply can fail after a successful preview if the preview becomes stale. The resolve callback compares replacement totals and per-file counts and returns an error result rather than applying a mismatched preview silently.\n\n## Notes\n- `ast_edit` does not expose the native `lang`, `strictness`, `selector`, `maxReplacements`, `failOnParseError`, or `timeoutMs` fields to the model. The runtime fixes the call shape to a preview-first, smart-strictness, best-effort parse mode.\n- Because the wrapper does not expose `lang`, mixed-language rewrites only succeed when every candidate infers to the same canonical language. This is stricter than `ast_grep`.\n- Idempotency is not enforced syntactically. A rewrite like `foo($A) -> foo($A)` previews zero changes because output equals input; a rewrite that keeps matching its own output may still produce replacements on repeated calls.\n- Rewrites are accumulated per file, then applied from the end of the file backward after an overlap check. Independent matches can coexist; overlapping matches abort the run.\n- Native rewrite rule order is by pattern-string sort, not by the original `ops` array order, because `normalize_rewrite_map(...)` sorts the `(pattern, rewrite)` pairs.\n- Preview/apply parity is validated only by totals and per-file counts, not by a byte-for-byte diff of every replacement payload.",
|
|
113
113
|
"tools/ast-grep.md": "# ast_grep\n\n> Structural code search over supported source files via native ast-grep.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/ast-grep.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/ast-grep.md`\n- Key collaborators:\n - `crates/pi-natives/src/ast.rs` — native scan, parse, match engine\n - `crates/pi-natives/src/language/mod.rs` — language aliases and extension inference\n - `packages/coding-agent/src/tools/path-utils.ts` — path/glob parsing and multi-path resolution\n - `packages/coding-agent/src/tools/render-utils.ts` — parse-error dedupe and display caps\n - `packages/coding-agent/src/tools/match-line-format.ts` — anchor-prefixed match rendering\n - `packages/coding-agent/src/utils/file-display-mode.ts` — hashline vs line-number output mode\n - `packages/natives/native/index.d.ts` — JS-visible native binding contract\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `pat` | `string` | Yes | Single AST pattern. The wrapper trims it and rejects empty strings. |\n| `paths` | `string[]` | Yes | One or more files, directories, globs, or internal URLs with backing files. Empty entries are rejected. Globs are forbidden for internal URLs. |\n| `skip` | `number` | No | Match offset. Defaults to `0`, then `Math.floor(...)`; negatives and non-finite values fail. |\n\nPattern grammar and language support exposed to the model:\n- `$NAME` — capture one AST node.\n- `$_` — match one AST node without binding.\n- `$$$NAME` — capture zero or more AST nodes; ast-grep stops lazily at the next satisfiable node.\n- `$$$` — match zero or more AST nodes without binding.\n- Metavariable names must be uppercase and must stand for whole AST nodes, not partial tokens or string fragments.\n- Reusing the same metavariable requires identical code at each occurrence.\n- Patterns must parse as one valid AST node for the inferred target language.\n- Supported canonical languages come from `SupportLang::all_langs()` in `crates/pi-natives/src/language/mod.rs`: `astro`, `bash`, `c`, `cmake`, `cpp`, `csharp`, `dart`, `clojure`, `css`, `diff`, `dockerfile`, `elixir`, `erlang`, `go`, `graphql`, `haskell`, `hcl`, `html`, `ini`, `java`, `javascript`, `json`, `just`, `julia`, `kotlin`, `lua`, `make`, `markdown`, `nix`, `objc`, `ocaml`, `odin`, `perl`, `php`, `powershell`, `protobuf`, `python`, `r`, `regex`, `ruby`, `rust`, `scala`, `solidity`, `sql`, `starlark`, `svelte`, `swift`, `toml`, `tlaplus`, `tsx`, `typescript`, `verilog`, `vue`, `xml`, `yaml`, `zig`.\n\n## Outputs\n- Single-shot tool result.\n- Model-facing `content` is one text block:\n - grouped by file for directory/multi-file searches,\n - match lines rendered as `*LINE+HASH|text` in hashline mode or `*LINE|text` otherwise,\n - continuation lines for multi-line matches rendered with a leading space,\n - optional `meta: NAME=value` lines when ast-grep captured metavariables.\n- If no matches are found, text is `No matches found` or `No matches found. Parse issues mean the query may be mis-scoped; narrow paths before concluding absence.` plus formatted parse issues.\n- If the wrapper truncates visible results, the text ends with `Result limit reached; narrow paths or increase limit.`\n- `details` includes counts and metadata, not full match payloads:\n - `matchCount`, `fileCount`, `filesSearched`, `limitReached`\n - optional `parseErrors`, `scopePath`, `files`, `fileMatches`, `displayContent`, `meta`\n- Native ranges (`byteStart`, `byteEnd`, `startLine`, `startColumn`, `endLine`, `endColumn`) exist only inside the native result; the wrapper does not emit them directly to the model.\n\n## Flow\n1. `AstGrepTool.execute()` validates `pat`, normalizes `skip`, and normalizes each `paths` entry in `packages/coding-agent/src/tools/ast-grep.ts`.\n2. Internal URLs are resolved through `session.internalRouter`; entries without `sourcePath` fail, and internal-URL globs fail early.\n3. For multiple path inputs, `partitionExistingPaths()` drops missing bases only when at least one surviving base remains; if all bases are missing the call fails.\n4. `parseSearchPath()` splits a single path into `basePath` plus optional `glob`. `resolveExplicitSearchPaths()` collapses multiple inputs into a common base plus a brace-union glob, or separate `targets` when the only common base is a filesystem root.\n5. The wrapper stats the resolved base path to decide whether output should be grouped as a directory result.\n6. Execution dispatches to either:\n - one native `astGrep(...)` call for a single resolved base, or\n - `runMultiTargetAstGrep(...)`, which calls the native binding once per target, rebases paths back to the common root, sorts globally, then applies `skip` and the wrapper limit.\n7. Native `ast_grep` in `crates/pi-natives/src/ast.rs`:\n - normalizes and deduplicates patterns,\n - resolves a `MatchStrictness` (`smart` by default),\n - collects candidate files from a file or gitignore-aware directory scan,\n - infers language per candidate from extension unless `lang` was provided,\n - compiles the pattern separately for each language present,\n - reads each file, reports syntax-error trees as parse issues, runs `find_all`, and optionally captures metavariable bindings.\n8. Native results are sorted by path and source position, then paged by `offset`/`limit`.\n9. The TS wrapper normalizes parse-error strings, deduplicates them, groups matches by formatted path, renders anchor lines, appends limit/parse notices, and returns `toolResult(...).text(...).done()`.\n\n## Modes / Variants\n- Single file: native path is the file; output is a flat list of rendered match lines.\n- Directory + optional glob: native scan walks the directory, then filters by compiled glob.\n- Multiple explicit paths/globs: wrapper unions them into one synthetic scope or runs per-target native calls when paths only meet at root.\n- Internal URL inputs: only supported when the router can resolve them to a backing file path.\n- Hashline output mode vs plain line-number mode: controlled by `resolveFileDisplayMode()`; hashline mode requires the edit tool and non-raw, mutable sources.\n\n## Side Effects\n- Filesystem\n - Stats input paths in the TS wrapper.\n - Native code reads matched files and scans directories through `fs_cache`.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - None beyond normal tool transcript/result metadata.\n- Background work / cancellation\n - Native work runs on a blocking worker via `task::blocking(...)`.\n - Cancellation and optional native timeout are cooperative through `CancelToken::heartbeat()`.\n\n## Limits & Caps\n- Wrapper-visible result cap: `DEFAULT_AST_LIMIT = 50` in `packages/coding-agent/src/tools/ast-grep.ts`.\n - Single-target calls rely on the native default limit of 50 in `crates/pi-natives/src/ast.rs`.\n - Multi-target calls fetch `skip + 50 + 1` matches per target, then re-page after global sort.\n- Native `limit` is clamped to at least `1`; omitted `offset` defaults to `0` in `crates/pi-natives/src/ast.rs`.\n- Parse issues are rendered with at most `PARSE_ERRORS_LIMIT = 20` lines in `packages/coding-agent/src/tools/render-utils.ts`; `details.parseErrors` itself is only deduplicated, not capped.\n- Directory scans use `include_hidden: true`, `use_gitignore: true`, and skip `node_modules` unless the glob text explicitly mentions `node_modules` in `crates/pi-natives/src/ast.rs`.\n- No hard file-count cap is applied by the wrapper or native `ast_grep`; candidate count is whatever the resolved path/glob expands to after gitignore filtering.\n- Multi-path union deduplicates identical path inputs before resolution in `resolveExplicitSearchPaths()`.\n\n## Errors\n- TS wrapper throws `ToolError` for empty patterns, invalid `skip`, empty path entries, unsupported internal-URL globs, internal URLs without `sourcePath`, and missing paths.\n- Native code returns hard errors for:\n - unsupported explicit `lang`,\n - inability to infer language for a candidate when `lang` is not supplied,\n - invalid AST pattern compilation for every relevant language,\n - unreadable search roots or bad glob compilation,\n - cancellation (`Aborted: Signal`) or timeout (`Aborted: Timeout`).\n- File-level parse failures and many per-language pattern compile failures are non-fatal: they are accumulated in `parseErrors` and surfaced alongside successful matches.\n- `no matches` is not an error, even when parse issues were recorded.\n\n## Notes\n- `pat` is always wrapped into a one-element `patterns` array by the TS tool; the model cannot send multiple patterns through `ast_grep` even though the native binding supports it.\n- `ast_grep` can search mixed-language trees because native compilation happens per discovered language, but the prompt still tells the model to keep calls single-language when possible to reduce parse noise.\n- Pattern compilation is per language present in the candidate set. One pattern can succeed for some languages and generate per-file parse errors for others in the same run.\n- A file with tree-sitter error nodes still gets searched; the syntax warning is additive, not a skip condition.\n- For glob semantics, `*.ts` matches only direct children while `**/*.ts` recurses; this is covered by native tests in `crates/pi-natives/src/ast.rs`.\n- Output anchors are intended for follow-up tools, but the exact anchor format depends on session edit mode (`hashline` vs line-number mode).",
|
|
114
|
-
"tools/bash.md": "# bash\n\n> Execute a shell command in the session workspace, with optional PTY or background-job handling.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/bash.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/bash.md`\n- Key collaborators:\n - `packages/coding-agent/src/tools/bash-interactive.ts` — PTY/TUI execution path.\n - `packages/coding-agent/src/tools/bash-interceptor.ts` — blocks tool-better shell patterns.\n - `packages/coding-agent/src/tools/bash-skill-urls.ts` — expands internal URLs to paths.\n - `packages/coding-agent/src/exec/bash-executor.ts` — non-PTY shell execution.\n - `packages/coding-agent/src/session/streaming-output.ts` — tail buffer, truncation, artifact spill.\n - `packages/coding-agent/src/tools/tool-timeouts.ts` — timeout clamp bounds.\n - `packages/coding-agent/src/config/settings-schema.ts` — default interceptor rules.\n - `docs/bash-tool-runtime.md` — deeper executor/runtime notes; use as the companion doc for shell-session internals.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `command` | `string` | Yes | Shell command text to execute. A leading `cd <path> && ...` is rewritten into `cwd` only when `cwd` was omitted. |\n| `env` | `Record<string, string>` | No | Extra environment variables. Keys must match `^[A-Za-z_][A-Za-z0-9_]*$` or the tool throws. Values also go through internal-URL expansion. |\n| `timeout` | `number` | No | Timeout in seconds. Default `300`; clamped to `1..3600` by `clampTimeout(\"bash\", ...)`. |\n| `cwd` | `string` | No | Working directory, resolved against `session.cwd` via `resolveToCwd`. Must exist and be a directory. |\n| `pty` | `boolean` | No | Request PTY mode. Default `false`. PTY is used only when `pty: true`, `GJC_NO_PTY !== \"1\"`, and the tool context has a UI. |\n| `async` | `boolean` | No | Background execution request. Present only when `async.enabled` is true for the session. Returns immediately with a job id instead of waiting. |\n\n## Outputs\nThe tool returns a single `text` content block plus optional `details`.\n\n- Success, foreground:\n - `content[0].text`: command output, or `(no output)` when the command produced nothing.\n - `details.timeoutSeconds`: effective timeout after clamping.\n - `details.requestedTimeoutSeconds`: only present when the requested timeout was clamped.\n - `details.meta.truncation`: present when output was truncated in memory; includes `artifactId` when full output spilled to an artifact.\n- Success, background start (`async: true` or auto-background):\n - `content[0].text`: optional preview tail, timeout notice if any, then `Background job <id> started: <label>` with follow-up instructions.\n - `details.async`: `{ state: \"running\", jobId, type: \"bash\" }`.\n- Background progress / completion:\n - delivered through `onUpdate` / async job manager, not the initial return.\n - running updates contain tail text and `details.async.state: \"running\"` only after the job is considered backgrounded.\n - completion/failure updates carry final text and `details.async.state: \"completed\" | \"failed\"`.\n- Failure:\n - the tool throws `ToolError` / `ToolAbortError`; non-zero exits are surfaced as errors, not success results.\n\nStdout and stderr are merged before the model sees them. Non-zero exit codes are appended to the thrown error text as `Command exited with code <n>`.\n\n## Flow\n1. `BashTool.execute()` in `packages/coding-agent/src/tools/bash.ts` reads `command`, normalizes `env`, and defaults `timeout` to `300`.\n2. If `cwd` is absent, it rewrites a leading `cd <path> && ...` into the structured `cwd` field and strips that prefix from `command`.\n3. If `async: true` is requested while `async.enabled` is off, it throws `ToolError` before any execution.\n4. If `bashInterceptor.enabled` is on, `checkBashInterception()` runs against both the original command and the `cd`-stripped command. A matching enabled rule throws before URL expansion or execution.\n5. `expandInternalUrls()` rewrites supported internal URLs inside `command`, each `env` value, and protocol-looking `cwd` values. Command/env replacements are shell-escaped unless `noEscape` is requested by the caller path.\n6. `resolveToCwd()` resolves `cwd` against `session.cwd`; `fs.stat()` verifies that the target exists and is a directory.\n7. `clampTimeout(\"bash\", requestedTimeoutSec)` enforces `TOOL_TIMEOUTS.bash` (`default: 300`, `min: 1`, `max: 3600`). When clamped, `#buildCompletedResult()` / `#buildBackgroundStartResult()` append a notice line.\n8. Execution path splits:\n 1. `async: true` -> `#startManagedBashJob()` registers a session async job and returns immediately.\n 2. Non-PTY with `bash.autoBackground.enabled` and an async job manager -> starts a managed job, waits up to `min(thresholdMs, timeoutMs - 1000)`, and either returns the completed result or converts the run into a background job.\n 3. Foreground non-PTY, ACP client-terminal, and PTY waits are manager-backed when async support is available, so the fold chord can transfer ownership without restarting the command; otherwise they remain ordinary foreground waits.\n9. Foreground non-PTY calls `executeBash()` from `packages/coding-agent/src/exec/bash-executor.ts`.\n10. Foreground PTY calls `runInteractiveBashPty()` from `packages/coding-agent/src/tools/bash-interactive.ts`.\n11. Both paths allocate an output artifact first when `session.allocateOutputArtifact` is available. The artifact path/id are passed into the sink so large output can spill to disk.\n12. `executeBash()` loads shell settings, optional shell snapshot, and shell minimizer settings, then runs via a persistent native `Shell` session or one-shot `executeShell()`. `docs/bash-tool-runtime.md` covers that path in detail.\n13. `runInteractiveBashPty()` creates a `PtySession`, overlays an xterm-backed console UI, forwards user key input into the PTY, captures output through `OutputSink`, and keeps the PTY owned by the manager when its observer folds. Escape explicitly kills; disposal alone never kills folded work.\n14. On completion, `#buildCompletedResult()` formats `(no output)` when needed, attaches truncation metadata from the `OutputSink` summary, and re-checks exit status / timeout / cancellation before returning.\n15. On non-zero exit, timeout, missing exit status, or cancellation, `#buildResultText()` throws with the captured output included in the error message.\n\n## Modes / Variants\n1. Foreground non-PTY\n - Default path.\n - Uses `executeBash()`.\n - Streams tail-only updates through `streamTailUpdates()` and `TailBuffer(DEFAULT_MAX_BYTES)`.\n2. Foreground PTY\n - Requires `pty: true`, UI context, and `GJC_NO_PTY !== \"1\"`.\n - Uses `runInteractiveBashPty()` and a `PtySession` overlay; observer disposal is non-owning, while Escape or `job cancel` kills the session.\n - Supports interactive input; `Esc` kills the session from the overlay.\n3. Explicit background job\n - Requires `async: true` and `async.enabled`.\n - Registers a job with `session.asyncJobManager` and returns `{ state: \"running\", jobId }` immediately.\n4. Auto-backgrounded non-PTY job\n - Requires `bash.autoBackground.enabled`, no PTY, and an async job manager.\n - Starts like a foreground managed job, then backgrounds it when it outlives the wait window.\n5. Intercepted command\n - No subprocess created.\n\n6. Foreground fold\n - The remappable `app.tool.backgroundFold` chord (Alt+Shift+B by default; Cmd+B on macOS) can fold a running managed non-PTY Bash wait, ACP client-terminal wait, or PTY wait into a manager-owned job.\n - The foreground turn stops cleanly after the result boundary; completion wakes a later turn with a receipt containing output, original intent, and a `job` retrieval handle.\n - Folded jobs retain their original deadline and live `job tail` output. PTY observer disposal is non-owning; `job cancel` kills the live PTY through the manager lifecycle.\n\n## Side Effects\n- Filesystem\n - Validates `cwd` with `fs.stat()`.\n - May allocate and write artifact files for full output (`bash`) and minimizer-preserved raw output (`bash-original`).\n - `expandInternalUrls(..., { ensureLocalParentDirs: true })` creates parent directories for `local://` paths before execution.\n- Subprocesses / native bindings\n - Non-PTY uses native shell execution via `@gajae-code/natives` (`Shell.run()` or `executeShell()`).\n - PTY uses native `PtySession.start()`.\n- Session state\n - Reads session settings for async, auto-background, interceptor, tool availability, and shell configuration.\n - Registers jobs with `session.asyncJobManager` for explicit/auto background runs.\n - Uses `session.getSessionId()` to isolate shell reuse and async session keys.\n - Uses `session.allocateOutputArtifact()` for spill files.\n- User-visible prompts / interactive UI\n - PTY mode opens a TUI overlay titled `Console` and forwards input to the PTY.\n - Background start messages direct the agent to the `job` tool (use `list: true` for a snapshot, or pass `poll: [id]` to wait).\n- Background work / cancellation\n - Async and auto-background jobs continue after the initial tool return.\n - Cancellation aborts the native run; Escape and `job cancel` kill a PTY, while overlay disposal alone is non-owning for folded work.\n\n## Limits & Caps\n- Default timeout: `300s` (`TOOL_TIMEOUTS.bash.default` in `packages/coding-agent/src/tools/tool-timeouts.ts`).\n- Timeout clamp: `1..3600s` (`TOOL_TIMEOUTS.bash.min/max`).\n- Auto-background default threshold: `60_000ms` (`DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS` in `packages/coding-agent/src/tools/bash.ts`), further capped to `timeoutMs - 1000` by `#resolveAutoBackgroundWaitMs()`.\n- Hard kill grace beyond requested timeout in non-PTY executor: `5_000ms` (`HARD_TIMEOUT_GRACE_MS` in `packages/coding-agent/src/exec/bash-executor.ts`).\n- In-memory output tail cap: `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`). Once exceeded, the sink keeps only the tail window in memory.\n- Streaming callback throttle in `executeBash()`: `50ms` between `onChunk` calls when streaming is enabled.\n- TUI collapsed preview: `10` visual lines (`BASH_DEFAULT_PREVIEW_LINES`) when rendered inline in the agent UI; this is a renderer cap, not a tool output cap.\n\n## Errors\n- Input validation:\n - invalid env key -> `ToolError(\"Invalid bash env name: <key>\")`.\n - async requested while disabled -> `ToolError(\"Async bash execution is disabled...\")`.\n - missing async job manager -> `ToolError(\"Async job manager unavailable for this session.\")`.\n - missing/bad `cwd` -> `ToolError(\"Working directory does not exist: ...\")` or `ToolError(\"Working directory is not a directory: ...\")`.\n- Interceptor:\n - matched command -> `ToolError` with `Blocked: <rule.message>` and the original command.\n - invalid interceptor regexes are silently skipped by `compileRules()`.\n- Internal URL expansion:\n - unsupported scheme, unknown skill, path traversal, missing router support, or router resolution failures all throw `ToolError` from `packages/coding-agent/src/tools/bash-skill-urls.ts`.\n- Execution:\n - non-zero exit -> thrown `ToolError` containing captured output plus `Command exited with code <n>`.\n - missing exit code -> thrown `ToolError` with `Command failed: missing exit status`.\n - timeout -> thrown `ToolError`; PTY uses `Command timed out after <n> seconds`, non-PTY executor returns cancelled output that `BashTool` converts to an error.\n - user abort -> `ToolAbortError` when the caller signal is aborted.\n- Artifact allocation / artifact save failures are swallowed in `saveBashOriginalArtifact()` and `OutputSink.#createFileSink()`; execution continues without that artifact.\n\n## Notes\n- `strict = true` and `concurrency = \"exclusive\"` are set on `BashTool`; the tool does not run concurrently with another bash tool call in the same session.\n- `command` and `env` URL expansions shell-escape replacements; `cwd` expansion uses `noEscape: true` because it becomes a filesystem path argument, not shell text.\n- `checkBashInterception()` blocks only when the matching rule's `tool` name is present in `ctx.toolNames`; missing tools disable their corresponding rule.\n- Default interceptor rules come from `DEFAULT_BASH_INTERCEPTOR_RULES` in `packages/coding-agent/src/config/settings-schema.ts`:\n - `cat|head|tail|less|more` -> `read`\n - `grep|rg|ripgrep|ag|ack` -> `search`\n - `find|fd|locate` with name/type/glob flags -> `find`\n - `sed -i`, `perl -i`, `awk -i inplace` -> `edit`\n - `echo|printf|cat <<` with redirection -> `write`\n- PTY mode is ignored in non-UI contexts and when `GJC_NO_PTY=1`; the tool silently falls back to non-PTY execution.\n- Non-PTY runs merge `NON_INTERACTIVE_ENV` with `env`; PTY runs also prepend `NON_INTERACTIVE_ENV` before custom env values.\n- When the shell minimizer rewrites output inside `executeBash()`, the visible output is replaced with minimized text and a `[raw output: artifact://<id>]` footer may be appended if `onMinimizedSave` persisted the original text.\n- The TUI renderer parses partial JSON to recover `env` assignments early in streaming previews; that behavior is display-only.\n- For executor internals that are not tool-specific — shell session reuse keys, snapshots, prefix handling, and native timeout behavior — see `docs/bash-tool-runtime.md`.\n",
|
|
114
|
+
"tools/bash.md": "# bash\n\n> Execute a shell command in the session workspace, with optional PTY or background-job handling.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/bash.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/bash.md`\n- Key collaborators:\n - `packages/coding-agent/src/tools/bash-interactive.ts` — PTY/TUI execution path.\n - `packages/coding-agent/src/tools/bash-interceptor.ts` — blocks tool-better shell patterns.\n - `packages/coding-agent/src/tools/bash-skill-urls.ts` — expands internal URLs to paths.\n - `packages/coding-agent/src/exec/bash-executor.ts` — non-PTY shell execution.\n - `packages/coding-agent/src/session/streaming-output.ts` — tail buffer, truncation, artifact spill.\n - `packages/coding-agent/src/tools/tool-timeouts.ts` — timeout clamp bounds.\n - `packages/coding-agent/src/config/settings-schema.ts` — default interceptor rules.\n - `docs/bash-tool-runtime.md` — deeper executor/runtime notes; use as the companion doc for shell-session internals.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `command` | `string` | Yes | Shell command text to execute. A leading `cd <path> && ...` is rewritten into `cwd` only when `cwd` was omitted. |\n| `env` | `Record<string, string>` | No | Extra environment variables. Keys must match `^[A-Za-z_][A-Za-z0-9_]*$` or the tool throws. Values also go through internal-URL expansion. |\n| `timeout` | `number` | No | Timeout in seconds. Default `300`; clamped to `1..3600` by `clampTimeout(\"bash\", ...)`. |\n| `cwd` | `string` | No | Working directory, resolved against `session.cwd` via `resolveToCwd`. Must exist and be a directory. |\n| `pty` | `boolean` | No | Request PTY mode. Default `false`. PTY is used only when `pty: true`, `GJC_NO_PTY !== \"1\"`, and the tool context has a UI. |\n| `async` | `boolean` | No | Background execution request. Present only when `async.enabled` is true for the session. Returns immediately with a job id instead of waiting. |\n\n## Outputs\nThe tool returns a single `text` content block plus optional `details`.\n\n- Success, foreground:\n - `content[0].text`: command output, or `(no output)` when the command produced nothing.\n - `details.timeoutSeconds`: effective timeout after clamping.\n - `details.requestedTimeoutSeconds`: only present when the requested timeout was clamped.\n - `details.meta.truncation`: present when output was truncated in memory; includes `artifactId` when full output spilled to an artifact.\n- Success, background start (`async: true` or auto-background):\n - `content[0].text`: optional preview tail, timeout notice if any, then `Background job <id> started: <label>` with follow-up instructions.\n - `details.async`: `{ state: \"running\", jobId, type: \"bash\" }`.\n- Background progress / completion:\n - delivered through `onUpdate` / async job manager, not the initial return.\n - running updates contain tail text and `details.async.state: \"running\"` only after the job is considered backgrounded.\n - completion/failure updates carry final text and `details.async.state: \"completed\" | \"failed\"`.\n- Failure:\n - the tool throws `ToolError` / `ToolAbortError`; non-zero exits are surfaced as errors, not success results.\n\nStdout and stderr are merged before the model sees them. Non-zero exit codes are appended to the thrown error text as `Command exited with code <n>`.\n\n## Flow\n1. `BashTool.execute()` in `packages/coding-agent/src/tools/bash.ts` reads `command`, normalizes `env`, and defaults `timeout` to `300`.\n2. If `cwd` is absent, it rewrites a leading `cd <path> && ...` into the structured `cwd` field and strips that prefix from `command`.\n3. If `async: true` is requested while `async.enabled` is off, it throws `ToolError` before any execution.\n4. If `bashInterceptor.enabled` is on, `checkBashInterception()` runs against both the original command and the `cd`-stripped command. A matching enabled rule throws before URL expansion or execution.\n5. `expandInternalUrls()` rewrites supported internal URLs inside `command`, each `env` value, and protocol-looking `cwd` values. Command/env replacements are shell-escaped unless `noEscape` is requested by the caller path.\n6. `resolveToCwd()` resolves `cwd` against `session.cwd`; `fs.stat()` verifies that the target exists and is a directory.\n7. `clampTimeout(\"bash\", requestedTimeoutSec)` enforces `TOOL_TIMEOUTS.bash` (`default: 300`, `min: 1`, `max: 3600`). When clamped, `#buildCompletedResult()` / `#buildBackgroundStartResult()` append a notice line.\n8. Execution path splits:\n 1. `async: true` -> `#startManagedBashJob()` registers a session async job and returns immediately.\n 2. Non-PTY with `bash.autoBackground.enabled` and an async job manager -> starts a managed job, waits up to `min(thresholdMs, timeoutMs - 1000)`, and either returns the completed result or converts the run into a background job.\n 3. Foreground non-PTY, ACP client-terminal, and PTY waits are manager-backed when async support is available, so the fold chord can transfer ownership without restarting the command; otherwise they remain ordinary foreground waits.\n9. Foreground non-PTY calls `executeBash()` from `packages/coding-agent/src/exec/bash-executor.ts`.\n10. Foreground PTY calls `runInteractiveBashPty()` from `packages/coding-agent/src/tools/bash-interactive.ts`.\n11. Both paths allocate an output artifact first when `session.allocateOutputArtifact` is available. The artifact path/id are passed into the sink so large output can spill to disk.\n12. `executeBash()` loads shell settings, optional shell snapshot, and shell minimizer settings, then runs via a persistent native `Shell` session or one-shot `executeShell()`. `docs/bash-tool-runtime.md` covers that path in detail.\n13. `runInteractiveBashPty()` creates a `PtySession`, overlays an xterm-backed console UI, forwards user key input into the PTY, captures output through `OutputSink`, and keeps the PTY owned by the manager when its observer folds. Escape explicitly kills; disposal alone never kills folded work.\n14. On completion, `#buildCompletedResult()` formats `(no output)` when needed, attaches truncation metadata from the `OutputSink` summary, and re-checks exit status / timeout / cancellation before returning.\n15. On non-zero exit, timeout, missing exit status, or cancellation, `#buildResultText()` throws with the captured output included in the error message.\n\n## Modes / Variants\n1. Foreground non-PTY\n - Default path.\n - Uses `executeBash()`.\n - Streams tail-only updates through `streamTailUpdates()` and `TailBuffer(DEFAULT_MAX_BYTES)`.\n2. Foreground PTY\n - Requires `pty: true`, UI context, and `GJC_NO_PTY !== \"1\"`.\n - Uses `runInteractiveBashPty()` and a `PtySession` overlay; observer disposal is non-owning, while Escape or `job cancel` kills the session.\n - Supports interactive input; `Esc` kills the session from the overlay.\n3. Explicit background job\n - Requires `async: true` and `async.enabled`.\n - Registers a job with `session.asyncJobManager` and returns `{ state: \"running\", jobId }` immediately.\n4. Auto-backgrounded non-PTY job\n - Requires `bash.autoBackground.enabled`, no PTY, and an async job manager.\n - Starts like a foreground managed job, then backgrounds it when it outlives the wait window.\n5. Intercepted command\n - No subprocess created.\n\n6. Foreground fold\n - The remappable `app.tool.backgroundFold` chord (Alt+Shift+B by default; Cmd+B on macOS) can fold a running managed non-PTY Bash wait, ACP client-terminal wait, or PTY wait into a manager-owned job.\n - A user steer admitted into the live run folds the same three surfaces automatically once the command has run for `STEER_FOLD_GRACE_MS` (2000 ms, exported from `bash.ts`, not configurable). Steers inside the grace window let the command finish normally. The steer fold fires only while `busyPromptMode` is `steer`; `toolInterruptPolicy` is not a gate, since a fold never kills the command; the `bash.autoBackground` setting is not consulted.\n - Every fold records `metadata.foldReason` on the job (`steer` | `chord` | `timer` | `sdk_control`) through `AsyncJobManager.markBackgrounded(jobId, generation, reason)`. `async: true` jobs are backgrounded without a fold reason (`markStartedInBackground`).\n - The background-start result carries `details.foldReason`; a steer fold appends `Folded into background job <id> because a user steer arrived; the command keeps running with its original timeout and its result will wake a later turn.`\n - Chord, timer, and SDK-control folds stop the foreground turn cleanly after the result boundary; completion wakes a later turn with a receipt containing output, original intent, and a `job` retrieval handle. A steer fold does NOT end the turn: the same run consumes the steer at the tool boundary right after the fold result (subagent-await parity), remaining parallel tools in that batch are skipped, and the job's completion wakes a later turn.\n - Folded jobs retain their original deadline and live `job tail` output. PTY observer disposal is non-owning; `job cancel` kills the live PTY through the manager lifecycle. Esc still aborts and kills the foreground command; a steer never kills it.\n\n## Side Effects\n- Filesystem\n - Validates `cwd` with `fs.stat()`.\n - May allocate and write artifact files for full output (`bash`) and minimizer-preserved raw output (`bash-original`).\n - `expandInternalUrls(..., { ensureLocalParentDirs: true })` creates parent directories for `local://` paths before execution.\n- Subprocesses / native bindings\n - Non-PTY uses native shell execution via `@gajae-code/natives` (`Shell.run()` or `executeShell()`).\n - PTY uses native `PtySession.start()`.\n- Session state\n - Reads session settings for async, auto-background, interceptor, tool availability, and shell configuration.\n - Registers jobs with `session.asyncJobManager` for explicit/auto background runs.\n - Uses `session.getSessionId()` to isolate shell reuse and async session keys.\n - Uses `session.allocateOutputArtifact()` for spill files.\n- User-visible prompts / interactive UI\n - PTY mode opens a TUI overlay titled `Console` and forwards input to the PTY.\n - Background start messages direct the agent to the `job` tool (use `list: true` for a snapshot, or pass `poll: [id]` to wait).\n- Background work / cancellation\n - Async and auto-background jobs continue after the initial tool return.\n - Cancellation aborts the native run; Escape and `job cancel` kill a PTY, while overlay disposal alone is non-owning for folded work.\n\n## Limits & Caps\n- Default timeout: `300s` (`TOOL_TIMEOUTS.bash.default` in `packages/coding-agent/src/tools/tool-timeouts.ts`).\n- Timeout clamp: `1..3600s` (`TOOL_TIMEOUTS.bash.min/max`).\n- Auto-background default threshold: `60_000ms` (`DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS` in `packages/coding-agent/src/tools/bash.ts`), further capped to `timeoutMs - 1000` by `#resolveAutoBackgroundWaitMs()`.\n- Hard kill grace beyond requested timeout in non-PTY executor: `5_000ms` (`HARD_TIMEOUT_GRACE_MS` in `packages/coding-agent/src/exec/bash-executor.ts`).\n- In-memory output tail cap: `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`). Once exceeded, the sink keeps only the tail window in memory.\n- Streaming callback throttle in `executeBash()`: `50ms` between `onChunk` calls when streaming is enabled.\n- TUI collapsed preview: `10` visual lines (`BASH_DEFAULT_PREVIEW_LINES`) when rendered inline in the agent UI; this is a renderer cap, not a tool output cap.\n\n## Errors\n- Input validation:\n - invalid env key -> `ToolError(\"Invalid bash env name: <key>\")`.\n - async requested while disabled -> `ToolError(\"Async bash execution is disabled...\")`.\n - missing async job manager -> `ToolError(\"Async job manager unavailable for this session.\")`.\n - missing/bad `cwd` -> `ToolError(\"Working directory does not exist: ...\")` or `ToolError(\"Working directory is not a directory: ...\")`.\n- Interceptor:\n - matched command -> `ToolError` with `Blocked: <rule.message>` and the original command.\n - invalid interceptor regexes are silently skipped by `compileRules()`.\n- Internal URL expansion:\n - unsupported scheme, unknown skill, path traversal, missing router support, or router resolution failures all throw `ToolError` from `packages/coding-agent/src/tools/bash-skill-urls.ts`.\n- Execution:\n - non-zero exit -> thrown `ToolError` containing captured output plus `Command exited with code <n>`.\n - missing exit code -> thrown `ToolError` with `Command failed: missing exit status`.\n - timeout -> thrown `ToolError`; PTY uses `Command timed out after <n> seconds`, non-PTY executor returns cancelled output that `BashTool` converts to an error.\n - user abort -> `ToolAbortError` when the caller signal is aborted.\n- Artifact allocation / artifact save failures are swallowed in `saveBashOriginalArtifact()` and `OutputSink.#createFileSink()`; execution continues without that artifact.\n\n## Notes\n- `strict = true` and `concurrency = \"exclusive\"` are set on `BashTool`; the tool does not run concurrently with another bash tool call in the same session.\n- `command` and `env` URL expansions shell-escape replacements; `cwd` expansion uses `noEscape: true` because it becomes a filesystem path argument, not shell text.\n- `checkBashInterception()` blocks only when the matching rule's `tool` name is present in `ctx.toolNames`; missing tools disable their corresponding rule.\n- Default interceptor rules come from `DEFAULT_BASH_INTERCEPTOR_RULES` in `packages/coding-agent/src/config/settings-schema.ts`:\n - `cat|head|tail|less|more` -> `read`\n - `grep|rg|ripgrep|ag|ack` -> `search`\n - `find|fd|locate` with name/type/glob flags -> `find`\n - `sed -i`, `perl -i`, `awk -i inplace` -> `edit`\n - `echo|printf|cat <<` with redirection -> `write`\n- PTY mode is ignored in non-UI contexts and when `GJC_NO_PTY=1`; the tool silently falls back to non-PTY execution.\n- Non-PTY runs merge `NON_INTERACTIVE_ENV` with `env`; PTY runs also prepend `NON_INTERACTIVE_ENV` before custom env values.\n- When the shell minimizer rewrites output inside `executeBash()`, the visible output is replaced with minimized text and a `[raw output: artifact://<id>]` footer may be appended if `onMinimizedSave` persisted the original text.\n- The TUI renderer parses partial JSON to recover `env` assignments early in streaming previews; that behavior is display-only.\n- For executor internals that are not tool-specific — shell session reuse keys, snapshots, prefix handling, and native timeout behavior — see `docs/bash-tool-runtime.md`.\n",
|
|
115
115
|
"tools/bisect.md": "# `bisect` tool\n\n`bisect` finds the exact commit that introduced (or fixed) a behavior by driving\n`git bisect` with a shell predicate, then restores the working tree and reports\nthe culprit. It is a discoverable built-in tool (activated on demand through\n`search_tool_bm25`, like `debug`, `recipe`, and `checkpoint`).\n\n## Why a tool instead of raw `git bisect`\n\nRunning `git bisect` by hand is error-prone for an agent: it is easy to forget\n`git bisect reset`, misread which commit is culpable, or strand the repository\nin a detached bisect state. The tool:\n\n- validates preconditions before touching repo state,\n- always runs `git bisect reset` in a `finally` block and then discards any\n tracked-file edits the predicate made with `git reset --hard`, so the working\n tree's tracked files are restored to their pre-bisect state even on error or\n abort (untracked files the predicate created are left in place — the tool\n never deletes files it did not create),\n- returns a structured, durable result (culprit + per-step verdicts) instead of\n scrollback that must be re-parsed.\n\n## Parameters\n\n| Name | Default | Meaning |\n| --- | --- | --- |\n| `good` | — (required) | The **older** endpoint; must be an ancestor of `bad`. |\n| `bad` | `HEAD` | The **newer** endpoint. |\n| `run` | — (required) | Shell command evaluated at each revision. |\n| `invert` | `false` | Find the commit that *fixed* the behavior instead. |\n| `maxSteps` | `40` | Maximum bisection steps before giving up. |\n| `stepTimeoutMs` | `600000` | Per-step timeout; a timed-out step counts as a skip. |\n\n### Predicate exit-code contract\n\n`run` is executed as `sh -c <run>` from the repository root (the top level of the\nworking tree) at each candidate commit — even when the tool is invoked from a\nsubdirectory. Its exit code is mapped to a verdict:\n\n| Exit code | Verdict |\n| --- | --- |\n| `0` | good |\n| `125` | skip (revision cannot be tested) |\n| any other non-zero | bad |\n\n### Search direction\n\n`git bisect` requires `good` to be an ancestor of `bad`, so `good` is always the\nolder endpoint and `bad` the newer one, in both modes.\n\n- **Default (find the regression):** the predicate passes at `good` and fails at\n `bad`. The tool reports the first commit that turned it bad.\n- **`invert: true` (find the fix):** the predicate fails at `good` and passes at\n `bad`. Internally the good/bad verdict of each intermediate commit is flipped,\n so the search reports the first commit that turned it good. A skip is never\n reinterpreted.\n\n## Preconditions\n\n- The current directory must be inside a git worktree.\n- The worktree must be clean (no uncommitted tracked changes). Commit or stash\n first — bisect checks out historical commits and would clobber uncommitted\n work.\n- `good` must resolve, `bad` must resolve, they must differ, and `good` must be\n an ancestor of `bad`.\n\n## Result\n\nOn success the tool reports the first bad (or first fixing) commit with its\nauthor, date, subject, and changed files, plus every revision it tested and\ntheir verdicts. When the search cannot converge (only skipped commits remain, a\n`git bisect` step fails, or `maxSteps` is reached) it reports the reason and the\nrevisions tested. In all cases every tracked file is restored to its pre-bisect\nstate; if the predicate created untracked files, the result says so and leaves\nthem in place.\n\n## Examples\n\nFind the commit that broke a specific test (older `v1.4.0` passes, `HEAD` fails):\n\n```json\n{ \"good\": \"v1.4.0\", \"bad\": \"HEAD\", \"run\": \"bun test test/login.test.ts\" }\n```\n\nFind the commit that fixed a bug (older `v1.0.0` fails, `HEAD` passes):\n\n```json\n{ \"good\": \"v1.0.0\", \"bad\": \"HEAD\", \"run\": \"bun test test/login.test.ts\", \"invert\": true }\n```\n\n## Implementation\n\n- Tool + pure loop controller + parsers: `packages/coding-agent/src/tools/bisect.ts`\n- `git bisect` wrappers: the `bisect` namespace in `packages/coding-agent/src/utils/git.ts`\n- Description: `packages/coding-agent/src/prompts/tools/bisect.md`\n\nThe bisect loop is a pure function (`runBisectController`) with all git and\npredicate effects injected, so it is unit-tested without a real repository;\n`parseFirstBadCommit` and `classifyExit` are tested independently, and an\nintegration test drives the whole tool against a real temporary git repository.\n",
|
|
116
116
|
"tools/browser.md": "# browser\n\n> Open, reuse, close, and script Puppeteer tabs against headless Chromium or CDP-attached apps.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/browser.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/browser.md`\n- Key collaborators:\n - `packages/coding-agent/src/tools/browser/tab-supervisor.ts` — global tab registry; worker lifecycle; run/close coordination.\n - `packages/coding-agent/src/tools/browser/tab-worker.ts` — executes `run` code; implements the `tab` helper API.\n - `packages/coding-agent/src/tools/browser/tab-worker-entry.ts` — worker-thread transport bootstrap.\n - `packages/coding-agent/src/tools/browser/registry.ts` — browser-handle registry keyed by browser kind.\n - `packages/coding-agent/src/tools/browser/launch.ts` — Puppeteer loading, Chromium resolution/download, headless launch, stealth injection.\n - `packages/coding-agent/src/tools/browser/attach.ts` — CDP attach/reuse, target picking, spawned-app process handling.\n - `packages/coding-agent/src/tools/browser/tab-protocol.ts` — worker init/run/result message schema.\n - `packages/coding-agent/src/tools/browser/readable.ts` — `tab.extract()` readability extraction.\n - `packages/coding-agent/src/tools/browser/render.ts` — TUI rendering for `open`/`close` status lines and `run` JS cells.\n - `packages/coding-agent/src/tools/puppeteer/00_stealth_tampering.txt` — mask patched functions/descriptors as native.\n - `packages/coding-agent/src/tools/puppeteer/01_stealth_activity.txt` — synthesize visibility/focus/scroll activity.\n - `packages/coding-agent/src/tools/puppeteer/02_stealth_hairline.txt` — fix Modernizr hairline detection.\n - `packages/coding-agent/src/tools/puppeteer/03_stealth_botd.txt` — spoof `navigator.webdriver`, `window.chrome`, and Chrome fingerprint surfaces.\n - `packages/coding-agent/src/tools/puppeteer/04_stealth_iframe.txt` — patch iframe `contentWindow`/`srcdoc` behavior.\n - `packages/coding-agent/src/tools/puppeteer/05_stealth_webgl.txt` — spoof WebGL vendor/renderer/precision.\n - `packages/coding-agent/src/tools/puppeteer/06_stealth_screen.txt` — normalize screen/viewport/device-pixel-ratio values.\n - `packages/coding-agent/src/tools/puppeteer/07_stealth_fonts.txt` — spoof local fonts and perturb canvas text rendering.\n - `packages/coding-agent/src/tools/puppeteer/08_stealth_audio.txt` — spoof audio latency/sample-rate and perturb offline rendering.\n - `packages/coding-agent/src/tools/puppeteer/09_stealth_locale.txt` — force locale/languages/timezone/date strings.\n - `packages/coding-agent/src/tools/puppeteer/10_stealth_plugins.txt` — synthesize `navigator.plugins`/`navigator.mimeTypes`.\n - `packages/coding-agent/src/tools/puppeteer/11_stealth_hardware.txt` — spoof `navigator.hardwareConcurrency`.\n - `packages/coding-agent/src/tools/puppeteer/12_stealth_codecs.txt` — spoof media codec support.\n - `packages/coding-agent/src/tools/puppeteer/13_stealth_worker.txt` — carry UA/platform spoofing into `Worker`/`SharedWorker`.\n\n## Inputs\n\n### Shared fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `action` | `\"open\" \\| \"close\" \\| \"act\" \\| \"run\"` | Yes | Dispatches to the open/close/act/run path. |\n| `name` | `string` | No | Tab id. Defaults to `\"main\"`. Tabs live in a process-global map, so the same name is reused across later calls and in-process subagents until closed. |\n| `timeout` | `number` | No | Tool wall-clock timeout in seconds. Defaults to `30`; clamped to the browser tool range before execution. |\n\n### `action: \"open\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `url` | `string` | No | Navigate after the tab is ready. Existing reusable tabs also navigate when `url` is supplied. |\n| `viewport` | `{ width: number; height: number; scale?: number }` | No | Requested viewport. For headless launch this becomes the initial viewport; for a page it is applied with `page.setViewport()`. `scale` maps to Puppeteer `deviceScaleFactor`. |\n| `wait_until` | `\"load\" \\| \"domcontentloaded\" \\| \"networkidle0\" \\| \"networkidle2\"` | No | Navigation wait condition. Defaults to `\"networkidle2\"` where omitted. |\n| `dialogs` | `\"accept\" \\| \"dismiss\"` | No | Installs a page `dialog` handler that auto-accepts or auto-dismisses dialogs. Omitted means no handler. |\n| `app` | `{ path?: string; cdp_url?: string; browser?: \"chrome\"; user_data_dir?: string; profile_directory?: string; background?: boolean; no_focus?: boolean; cdp_port?: number; args?: string[]; target?: string }` | No | Selects browser kind. No `app` uses the session `browser.headless` setting. `app.path` alone is resolved against the session cwd and used as the executable path for spawn/attach reuse. `app.cdp_url` connects to an existing CDP endpoint. `app.browser: \"chrome\"` selects guarded saved-profile mode: `path` defaults to installed Chrome/Chromium and `profile_directory` defaults to `\"Default\"`, while `user_data_dir` is required and must be a non-default Chrome data directory because Chrome 136+ disables remote debugging for its default data directory. Non-Chrome executables and default Chrome data roots are rejected. `args` are appended only when spawning `app.path` or a Chrome profile. `target` is used for attached/spawned/profile page selection. |\n\n### `action: \"close\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `all` | `boolean` | No | Close every known tab. Omitted closes only `name`. |\n| `kill` | `boolean` | No | When a tab release drops a spawned-app browser handle to refcount 0, also terminate its process tree. For Chrome profile mode this kills only a profile browser process launched by GJC; externally-owned profile CDP and `app.cdp_url` are disconnect-only. Has no effect on headless shutdown. |\n\n### `action: \"run\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `code` | `string` | Yes | Async-function body executed in a VM context with `page`, `browser`, `tab`, `display`, `assert`, `wait`, `console`, timers, `URL`, `TextEncoder`, `TextDecoder`, and `Buffer` in scope. |\n\n### `action: \"act\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `actions` | non-empty `Array<ActionStep>` | Yes | Structured, ordered interaction steps. Prefer this to `run` for routine navigation and interaction. Verbs: `navigate`, `click`, `type`, `fill`, `select`, `press`, `scroll`, `back`, `wait`, `observe`, `extract`, `screenshot`. `click`/`type` accept an observed numeric `id` or selector; `fill`/`select` require selectors. |\n\n## Outputs\nThe tool returns one result per call; no streaming partial output is emitted from the browser implementation itself.\n\n- `open`: text content with `Opened` or `Reused`, browser description, URL, and optional title. `details` includes `action`, `name`, `browser`, `url`, `viewport`, and the same text in `details.result`.\n- `close`: text content with either `Closed ...` or `No tab named ...`. `details` includes `action`, `name`, and `details.result`.\n- `run`: ordered `content` array built as:\n 1. every `display(value)` call in execution order,\n 2. a runtime-diagnostics JSON block when the tab observed page exceptions or `console.error` events,\n 3. final return value, JSON-stringified unless already a string,\n 4. or `Ran code on tab \"...\"` if nothing else was produced.\n- `act`: the same ordered `content` shape, with per-step results as the final JSON return value, or `Ran <n> action(s) on tab \"...\"` if no step produced content.\n- `display(value)` coercion in `packages/coding-agent/src/tools/browser/tab-worker.ts`:\n - `{ type: \"image\", data: string, mimeType: string }` becomes image content,\n - `string` becomes text content,\n - other values become pretty JSON text when serializable, else `String(value)`.\n- `tab.screenshot()` also appends text plus an image content item unless `silent: true`; `details.screenshots` records persisted screenshot metadata `{ dest, mimeType, bytes, width, height }`.\n- `run` and `act` `details` include `action`, `name`, current `browser`/`url` when the tab exists, optional `screenshots`, and `details.result` containing only concatenated text outputs.\n- Runtime diagnostics are opt-in: `open(..., { diagnostics: true })` subscribes the tab to CDP `Runtime.exceptionThrown` and `console.error` events (an extra CDP session plus `Runtime.enable` per tab — the added per-event traffic is the reason it is not on by default). Each tab keeps the newest 20 events in memory. The next successful `run` or `act` drains them as `{ runtimeDiagnostics, runtimeDiagnosticsDropped }`; failed actions leave the mailbox for the next successful response. Each entry contains only kind, timestamp, origin-only URL, line/column, and an error class from a fixed allowlist of built-in error classes when available; path segments, query strings, messages, console arguments, values, and stacks are never retained. Serialization is bounded: the `url` field is capped with a visible `…` marker and the whole block is capped at 4 KiB, shedding the oldest entries first and marking the block with `runtimeDiagnosticsTruncated: true` when that cap is hit — truncation is never silent.\n\n## Flow\n1. `BrowserTool.execute()` (`packages/coding-agent/src/tools/browser.ts`) abort-checks, clamps `timeout` via `clampTimeout(\"browser\", ...)`, defaults `name` to `\"main\"`, and dispatches `open`, `close`, `act`, or `run`.\n2. `open` resolves browser kind with `resolveBrowserKind()`:\n - `app.cdp_url` → `{ kind: \"connected\" }` after trimming trailing slashes.\n - `app.browser: \"chrome\"` → `{ kind: \"chrome-profile\" }`. `path` defaults to installed Chrome/Chromium (`resolveSystemChromeForProfile()`, admitting only Chrome/Chromium brands) and `profile_directory` defaults to `\"Default\"`. `user_data_dir` is required, resolved against the session cwd, and rejected when it resolves (including through a symlink) to a platform default Stable/Beta/Dev/Canary/Chromium root; trusted Linux environment overrides plus Flatpak and Snap defaults are included. Chrome 136+ does not honor remote-debugging switches for default Chrome data directories. Explicit Edge, Brave, Vivaldi, Opera, and unknown browser executables are rejected before profile fields are resolved. `background`, `no_focus`, and optional `cdp_port` are copied through.\n - `app.path` → `{ kind: \"spawned\" }` after resolving against session cwd.\n - otherwise → `{ kind: \"headless\", headless: session.settings.get(\"browser.headless\") }`.\n3. `open` rejects reusing the same tab name across different browser kinds (`sameBrowserKind()`); callers must close first.\n4. `open` acquires a browser handle through `acquireBrowser()` (`packages/coding-agent/src/tools/browser/registry.ts`):\n - existing connected handle is reused by browser-kind key;\n - stale disconnected handles are disposed and recreated;\n - headless launches via `launchHeadlessBrowser()`;\n - `connected` waits for `${cdpUrl}/json/version`, then `puppeteer.connect()`;\n - `spawned` first tries `findReusableCdp()`, else kills same-path processes, allocates a free loopback port, spawns the executable with `--remote-debugging-port=<port>`, waits for CDP, then connects.\n - `chrome-profile` checks same-executable processes for matching `--user-data-dir` and `--profile-directory`; if the matching process already exposes a responding `127.0.0.1` CDP port it reuses and later only disconnects, if it is running without attachable CDP it refuses without killing or relaunching, and if it is not running GJC launches Chrome with `--remote-debugging-address=127.0.0.1`, an ephemeral or configured `--remote-debugging-port`, `--user-data-dir`, `--profile-directory`, and `--no-startup-window` when `background` or `no_focus` is set. Only this GJC-launched process is eligible for `kill` cleanup.\n5. `open` acquires a tab through `acquireTab()` (`packages/coding-agent/src/tools/browser/tab-supervisor.ts`):\n - same-name + same-browser + alive tab is reused unless `dialogs` changed;\n - same-name but different browser handle, dead state, or changed dialog policy forces release and recreation;\n - reusing with a new `url` navigates by issuing `await tab.goto(...)` through the worker.\n6. New tabs build a `WorkerInitPayload` in `buildInitPayload()`:\n - headless mode sends `url`, `waitUntil`, `viewport`, `dialogs`, and timeout;\n - attach mode resolves a page with `pickElectronTarget()`, gets its target id, and sends `targetId` plus `dialogs`.\n7. `acquireTab()` spawns a dedicated Bun `Worker` from `tab-worker-entry.ts`; if that fails it falls back to inline execution in the main thread (`spawnInlineWorker()`), preserving behavior but losing protection against synchronous infinite loops.\n8. `WorkerCore.#init()` (`packages/coding-agent/src/tools/browser/tab-worker.ts`) connects back to the browser websocket endpoint. Headless mode opens a new page, applies stealth patches, applies viewport, installs dialog handling if requested, and optionally navigates. Attach mode resolves the requested target page and optionally installs dialog handling.\n9. On success the worker sends `ready` with `{ url, title, viewport, targetId }`; the supervisor stores a `TabSession`, increments browser-handle refcount with `holdBrowser()`, and keeps the tab in a process-global `Map<string, TabSession>`.\n\n### Existing non-default Chrome profile mode\n\nUse this mode for a dedicated, persistent Chrome data root that already contains the automation profile and login state. Chrome 136+ rejects remote debugging against the browser's default data root, so do not point this mode at the daily Chrome root. Create and sign in to a separate root first, close that Chrome instance, then let GJC reopen it with the guarded CDP lifecycle:\n\n```json\n{\n \"action\": \"open\",\n \"name\": \"work-browser\",\n \"app\": {\n \"browser\": \"chrome\",\n \"user_data_dir\": \"~/Library/Application Support/GJC/Chrome Automation\",\n \"profile_directory\": \"Default\",\n \"background\": true,\n \"no_focus\": true,\n \"target\": \"example.com\"\n }\n}\n```\n\nSecurity and lifecycle rules:\n\n- CDP is bound to `127.0.0.1`; do not expose logged-in profile CDP ports on a public interface. A CDP client has full browser-account access.\n- `user_data_dir` must be a separate non-default data root. Platform Stable/Beta/Dev/Canary/Chromium defaults, Linux environment/Flatpak/Snap defaults, and aliases to them are rejected before launch. Use `app.cdp_url` only for an already-authorized endpoint that you intentionally started and control.\n- Saved-profile and attached-CDP automation can read and act with that profile's cookies and authenticated accounts. Use it only when that credentialed access is intentional.\n- Never use generic `app.path` spawning for a daily Chrome profile: it may kill stale same-path processes. Use explicit `app.browser: \"chrome\"` profile mode, which applies the ownership guards below.\n- A matching already-running profile is reused only when its localhost CDP endpoint responds. A matching profile running normally without CDP is refused with remediation text; GJC does not kill or relaunch it.\n- `background` and `no_focus` add Chromium's `--no-startup-window` launch guard. Focus avoidance is best-effort and platform-dependent; already-visible Chrome windows can still be selected by `target` but are not OS-keyboard/mouse driven.\n- Cleanup disconnects externally-owned CDP endpoints. `kill: true` terminates only the Chrome profile process that GJC launched for this mode.\n10. `run` requires non-empty `code`; `act` requires non-empty `actions`, validates and compiles them into injection-safe JSON-parsed code, then both delegate to `runInTab()`.\n11. `runInTabWithSnapshot()` rejects dead tabs and concurrent runs (`Tab ... is busy`), captures session cwd plus optional `browser.screenshotDir`, registers an abort hook, sends a `run` message to the worker, and races the result against `timeoutMs + 750` ms. Timeouts force-kill the tab worker and, for headless tabs, close the orphaned page target.\n12. `WorkerCore.#run()` creates a VM context, exposes the raw Puppeteer `page`/`browser` plus a synthetic `tab` API, and executes `(async () => { ...code... })()` via `vm.runInContext()`.\n13. The `tab` helper API implemented in `#createTabApi()` is:\n - `tab.name: string`\n - `tab.page: Page`\n - `tab.signal?: AbortSignal`\n - `tab.url(): string`\n - `tab.title(): Promise<string>`\n - `tab.goto(url, { waitUntil? })`\n - `tab.observe({ includeAll?, viewportOnly? })`\n - `tab.screenshot({ selector?, fullPage?, save?, silent? })`\n - `tab.extract(format = \"markdown\")`\n - `tab.click(selector)`\n - `tab.type(selector, text)`\n - `tab.fill(selector, value)`\n - `tab.press(key, { selector? })`\n - `tab.scroll(deltaX, deltaY)`\n - `tab.drag(from, to)`\n - `tab.waitFor(selector)`\n - `tab.evaluate(fn, ...args)`\n - `tab.scrollIntoView(selector)`\n - `tab.select(selector, ...values)`\n - `tab.uploadFile(selector, ...filePaths)`\n - `tab.waitForUrl(pattern, { timeout? })`\n - `tab.waitForResponse(pattern, { timeout? })`\n - `tab.id(n)`\n14. Selector handling in `normalizeSelector()` accepts plain CSS and Puppeteer query handlers, and rewrites legacy Playwright-style prefixes `p-text/`, `p-xpath/`, `p-pierce/`, `p-aria/`; other `p-*` prefixes throw a `ToolError`.\n15. `tab.observe()` clears the element cache, takes a Puppeteer accessibility snapshot, filters to interactive nodes unless `includeAll`, optionally filters to viewport-visible nodes, assigns numeric ids, caches `ElementHandle`s, and returns URL/title/viewport/scroll metadata plus `elements`.\n16. `tab.id(n)` resolves the cached `ElementHandle`, verifies `el.isConnected`, and throws a stale-id error after cache invalidation if the DOM changed or the cache was cleared.\n17. `tab.goto()` clears the cached element ids before navigating. Any new `tab.observe()` also clears and rebuilds the cache.\n18. `tab.click()` uses a custom retry loop for `text/...` selectors to find an actionable visible match; other selectors use `page.locator(...).click()` with the run timeout.\n19. `tab.screenshot()` captures either the whole page or a selector PNG, downsizes a copy for model output, chooses a persistence path, writes the image to disk, records metadata, and optionally emits text + image display entries.\n20. `display()` calls accumulate in an array. After code finishes, the worker posts `{ displays, returnValue, screenshots }`; `BrowserTool.#run()` appends the return value as trailing text content when not `undefined`.\n21. `close` releases one tab or all tabs via `releaseTab()` / `releaseAllTabs()`. Each tab aborts pending runs, asks the worker to close, waits up to `750` ms for a `closed` ack, terminates the worker, decrements browser refcount, and disposes the browser handle when refcount reaches zero.\n\n## Modes / Variants\n- **Action dispatch**\n - `open` — acquire/reuse browser + tab.\n - `close` — release one tab or all tabs.\n - `act` — validate and run structured interaction steps; preferred for routine navigation and interaction.\n - `run` — execute JS inside the tab worker.\n- **Browser kind**\n - **Headless**: launches local Chromium with Puppeteer, applies stealth patches, and creates a fresh page per tab.\n - **Spawned app (`app.path`)**: reuses an existing CDP-enabled process for that executable when possible; otherwise kills same-path processes, spawns the executable with remote debugging enabled, then attaches. No stealth patches are injected.\n - **Connected browser (`app.cdp_url`)**: attaches to an already-running CDP endpoint. No process ownership; close only disconnects.\n- **Target selection for attached/spawned browsers**\n - With `app.target`, `pickElectronTarget()` returns the first page whose URL or title contains the case-insensitive substring.\n - Without `app.target`, it skips titles/URLs matching `request handler|devtools|background page|background host|service worker` and otherwise falls back to the first page.\n- **Worker mode**\n - **Dedicated worker**: normal path; user code runs off the main thread and can be aborted even when it blocks synchronously.\n - **Inline fallback**: activated when Bun worker spawn fails; behavior matches, but synchronous infinite loops on user code cannot be interrupted.\n- **Dialog policy**\n - No `dialogs` field: no auto-handler.\n - `accept`/`dismiss`: page `dialog` events are handled automatically.\n - Changing dialog policy on an existing live tab forces tab recreation instead of mutating the worker in place.\n- **Screenshot persistence**\n - `save` provided: persist full-resolution PNG at the resolved cwd-relative or absolute path.\n - `browser.screenshotDir` session setting set: persist full-resolution PNG under that directory with a timestamped filename.\n - Neither set: persist the resized image to a temp-file path under the OS temp dir.\n\n## Side Effects\n- Filesystem\n - `loadPuppeteer()` writes `{}` to `<puppeteer-safe-dir>/package.json` before importing `puppeteer-core`.\n - First headless launch may download Chromium into the Puppeteer cache directory returned by `getPuppeteerDir()`.\n - `tab.screenshot()` creates parent directories and writes image files.\n - `tab.uploadFile()` resolves supplied paths against the session cwd.\n- Network\n - CDP attach paths poll `http://127.0.0.1:<port>/json/version` or the supplied `cdp_url` `/json/version`.\n - Headless/browser-attach sessions create CDP websocket connections.\n - Headless first-use Chromium download uses `@puppeteer/browsers`.\n - User `page` / `tab` operations perform normal browser network traffic.\n- Subprocesses / native bindings\n - Headless mode launches Chromium through Puppeteer.\n - `app.path` mode may spawn the target executable via `Bun.spawn()`.\n - `killExistingByPath()` / `gracefulKillTreeOnce()` use `@gajae-code/natives` process inspection/termination.\n - Worker mode uses Bun `Worker`; fallback mode does not.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Browser handles are cached in a process-global `Map` keyed by browser kind in `packages/coding-agent/src/tools/browser/registry.ts`.\n - Tabs are cached in a process-global `Map` keyed by `name` in `packages/coding-agent/src/tools/browser/tab-supervisor.ts`.\n - `run` captures session cwd and optional `browser.screenshotDir` for screenshot/save path resolution.\n - `restartForModeChange()` drops only headless tabs.\n- User-visible prompts / interactive UI\n - None beyond normal tool output. Dialog auto-handling is invisible unless it fails and emits debug logs.\n- Background work / cancellation\n - `open`, `run`, CDP waits, and browser actions thread through abort signals.\n - A timed-out `run` aborts the worker execution path and can tear down the tab.\n\n## Limits & Caps\n- Tool timeout clamp: default `30` s, min `1` s, max `30` s (`TOOL_TIMEOUTS.browser` in `packages/coding-agent/src/tools/tool-timeouts.ts`).\n- Supervisor grace period around init/run/close: `750` ms (`GRACE_MS` in `packages/coding-agent/src/tools/browser/tab-supervisor.ts`).\n- Puppeteer protocol timeout for launch/connect operations: `60_000` ms (`BROWSER_PROTOCOL_TIMEOUT_MS` in `packages/coding-agent/src/tools/browser/launch.ts`).\n- Connected-browser CDP readiness wait: `5_000` ms before `puppeteer.connect()` (`packages/coding-agent/src/tools/browser/registry.ts`).\n- Spawned-app CDP readiness wait after spawn: `30_000` ms (`packages/coding-agent/src/tools/browser/registry.ts`).\n- CDP polling cadence: 150 ms in `waitForCdp()` (`packages/coding-agent/src/tools/browser/attach.ts`).\n- Headless default viewport: `1365x768` at `deviceScaleFactor: 1.25` (`DEFAULT_VIEWPORT` in `packages/coding-agent/src/tools/browser/launch.ts`).\n- Screenshot model-attachment resize cap: `maxWidth 1024`, `maxHeight 1024`, `maxBytes 150 * 1024`, `jpegQuality 70` (`packages/coding-agent/src/tools/browser/tab-worker.ts`).\n- `tab.waitForUrl()` polling interval: `200` ms (`packages/coding-agent/src/tools/browser/tab-worker.ts`).\n- Drag simulation uses `12` mouse-move steps (`packages/coding-agent/src/tools/browser/tab-worker.ts`).\n\n## Errors\n- `BrowserTool.execute()` converts DOM-style `AbortError` into `ToolAbortError`; other errors propagate.\n- `run` hard-fails on missing code: `Missing required parameter 'code' for action 'run'.`\n- `open` fails when reusing a name across browser kinds: `Tab \"...\" is bound to a different browser (...). Close it first.`\n- `runInTabWithSnapshot()` fails when the tab is absent/dead (`Tab \"...\" is not alive. Reopen it.`) or already running (`Tab \"...\" is busy`).\n- Worker init failures and run failures are serialized through `RunErrorPayload`; `ToolError` and abort state are reconstructed on the host side by `errorFromPayload()`.\n- Attached-target mismatches surface as:\n - `No page targets available on the attached browser`\n - `No page target matched \"...\". Available pages:\\n...`\n - `Target ... is no longer available on the attached browser`\n- Spawned-app path validation requires an absolute executable path, not an app bundle path.\n- Spawn/attach failures are wrapped into `ToolError`s such as `Timed out waiting for CDP endpoint ...`, `Failed to attach to ...`, or `Connected to ... but puppeteer.connect failed: ...`.\n- `tab` helper errors are user-visible `ToolError`s, including unsupported selector prefix, stale/unknown element id, invalid drag target, missing upload files, non-`<select>` for `tab.select()`, non-file-input for `tab.uploadFile()`, and screenshot selector misses.\n- On run timeout, the worker reports `Browser code execution timed out after <ms>ms`; the supervisor may escalate to `Browser code execution hung past grace; tab killed` if the worker does not respond after the grace window.\n\n## Notes\n- `loadPuppeteer()` and `loadPuppeteerInWorker()` temporarily redirect `cwd` to a safe Puppeteer directory before importing `puppeteer-core`, because Puppeteer probes the current working directory during module load.\n- Headless launch prefers a detected system Chrome/Chromium, then `PUPPETEER_EXECUTABLE_PATH`, and only then downloads Chromium.\n- Headless launch always passes `--no-sandbox`, `--disable-setuid-sandbox`, `--disable-blink-features=AutomationControlled`, and a `--window-size=...` matching the initial viewport. It also ignores Puppeteer default args `--disable-extensions`, `--disable-default-apps`, and `--disable-component-extensions-with-background-pages`.\n- Proxy-related env vars only affect headless launch: `PUPPETEER_PROXY`, `PUPPETEER_PROXY_BYPASS_LOOPBACK`, and `PUPPETEER_PROXY_IGNORE_CERT_ERRORS`.\n- Stealth patches are applied only in headless mode. Spawned or externally connected browsers are intentionally left untouched.\n- `applyStealthPatches()` also strips Puppeteer's `//# sourceURL=__puppeteer_evaluation_script__` suffix from CDP `Runtime.evaluate` / `Runtime.callFunctionOn` payloads.\n- `tab.extract()` reads `page.content()`, runs Readability first, then falls back to `main article`/`article`/`main`/`[role='main']`/`body`, and returns `null` if neither extraction path yields content.\n- `close(all: true, kill: false)` disconnects from spawned/connected browsers when the last tab closes but leaves spawned app processes running.\n- Headless orphan cleanup is best-effort: if a worker dies before closing its page, the supervisor searches browser targets by `targetId` and closes that page.\n- Console methods inside `run` do not appear in tool output; they are forwarded as debug/warn/error logs through the worker transport.\n- Runtime diagnostics are not written to a separate browser state or log. Once surfaced, their bounded metadata follows the ordinary tool-result transcript lifecycle. Opening a tab without `diagnostics: true` creates no CDP `Runtime` subscription and no per-event work.",
|
|
117
117
|
"tools/calc.md": "# calc\n\n> Evaluates one or more arithmetic expressions and returns formatted numeric results.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/calculator.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/calculator.md`\n- Key collaborators:\n - `packages/coding-agent/src/tui.ts` — status lines and tree-list rendering\n - `packages/coding-agent/src/tools/render-utils.ts` — preview limits and formatting helpers\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `calculations` | `Calculation[]` | Yes | Batch of expressions to evaluate in order. |\n\n### `Calculation`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `expression` | `string` | Yes | Arithmetic expression string. |\n| `prefix` | `string` | Yes | Prepended verbatim to the rendered numeric result. |\n| `suffix` | `string` | Yes | Appended verbatim to the rendered numeric result. |\n\n## Outputs\n- Single-shot result.\n- `content[0].text` is the newline-joined `prefix + value + suffix` string for each calculation.\n- `details.results` is an array of `{ expression, value, output }`.\n- On renderer fallback, if `details` is missing but `content[0].text` exists, the TUI tries to pair each output line with the original expressions from call args.\n\n## Flow\n1. `execute()` wraps evaluation in `untilAborted(...)`.\n2. For each entry, `evaluateExpression(...)` tokenizes the expression, parses it with a recursive-descent parser, rejects non-finite outputs, and normalizes `-0` to `0`.\n3. `tokenizeExpression(...)` accepts whitespace, parentheses, operators, and number literals; any other character throws immediately.\n4. `ExpressionParser` applies precedence in this order: `+ -`, `* / %`, unary `+ -`, exponentiation `**`, parentheses/literals.\n5. Exponentiation is right-associative (`2 ** 3 ** 2` parses as `2 ** (3 ** 2)`).\n6. Each numeric result is formatted with `String(value)` and wrapped with the provided `prefix` and `suffix`.\n7. The tool returns text output plus structured `details`.\n\n## Side Effects\n- Background work / cancellation\n - Supports abort via `untilAborted(...)`.\n- Session state\n - None.\n- Filesystem / Network / Subprocesses\n - None.\n\n## Limits & Caps\n- Supported operators: `+`, `-`, `*`, `/`, `%`, `**` (`packages/coding-agent/src/tools/calculator.ts`).\n- Supported numeric literals:\n - decimal integers/floats, including leading-dot forms like `.5`\n - scientific notation like `1e10`, `2.5E-3`\n - hexadecimal `0x...`\n - binary `0b...`\n - octal `0o...`\n- Results must be finite; `Infinity` and `NaN` are rejected.\n- The renderer collapses long result lists using `PREVIEW_LIMITS.COLLAPSED_ITEMS` from `packages/coding-agent/src/tools/render-utils.ts`.\n\n## Errors\n- Invalid characters: e.g. `Invalid character \"x\" in expression`.\n- Malformed numbers: invalid prefixed literal, invalid exponent, invalid number.\n- Syntax errors: `Unexpected token in expression`, `Unexpected end of expression`, `Missing closing parenthesis`, `Expression is empty`.\n- Non-finite arithmetic: `Expression result is not a finite number`.\n- Any evaluation error aborts the whole batch; the tool does not return partial successes.\n\n## Notes\n- Despite the schema example showing `sqrt(16)`, the parser does not support functions, identifiers, units, or constants; only numeric literals, operators, and parentheses are accepted.\n- Precision is plain JavaScript `number` semantics throughout, including floating-point rounding behavior.\n- `/` and `%` use JavaScript numeric operators directly; there is no integer-only mode or unit handling.\n- Unary operators bind tighter than `*`/`/`/`%` but looser than exponentiation because unary parsing delegates to `#parsePower()`.\n",
|
|
@@ -124,7 +124,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
124
124
|
"tools/find.md": "# find\n\n> Find filesystem paths by glob; use `search` when you need content matches instead of path matches.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/find.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/find.md`\n- Key collaborators:\n - `packages/coding-agent/src/tools/path-utils.ts` — normalize inputs; split base path vs glob.\n - `packages/coding-agent/src/tools/list-limit.ts` — apply result-count caps.\n - `packages/coding-agent/src/session/streaming-output.ts` — truncate text output at byte cap.\n - `packages/coding-agent/src/tools/tool-result.ts` — build `content` and `details.meta`.\n - `packages/coding-agent/src/tools/output-meta.ts` — encode limit / truncation metadata.\n - `packages/coding-agent/src/tools/tool-errors.ts` — map user-facing tool errors.\n - `packages/coding-agent/src/tools/index.ts` — register the built-in local implementation.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `paths` | `string[]` | Yes | One or more globs, files, or directories. Empty strings are rejected. Multiple entries may be merged into one brace-union search when their base paths can be resolved together. |\n| `hidden` | `boolean` | No | Whether hidden files are included. Defaults to `true` (`hidden ?? true`). |\n| `limit` | `number` | No | Max returned paths. Defaults to `1000`. Must be a finite positive number; non-integers are floored. |\n\n## Outputs\nThe tool returns a single text block plus structured `details`.\n\n- Success text: newline-delimited paths, one per line, relative to the session cwd when possible; absolute when outside cwd. Exact file inputs return that file path as one line.\n- Empty result text: `No files found matching pattern`.\n- Multi-path partial miss: appends `Skipped missing paths: ...` after the result block, or after the empty-result line.\n- `details` may include:\n - `scopePath`: display form of the searched root or merged roots.\n - `fileCount`: number of paths returned after result limiting.\n - `files`: returned paths as an array.\n - `truncated`: whether result count or byte truncation occurred.\n - `resultLimitReached`: reached result limit.\n - `missingPaths`: skipped missing inputs in multi-path calls.\n - `truncation` / `meta.limits`: structured truncation and limit metadata for renderers.\n- Streaming: when the runtime supplies `onUpdate`, the local implementation emits incremental newline-delimited text snapshots during globbing, throttled to 200 ms.\n\n## Flow\n1. `FindTool.execute()` normalizes each `paths` entry with `normalizePathLikeInput()` and `/\\\\/g -> \"/\"` (`packages/coding-agent/src/tools/find.ts`). Empty normalized entries fail with `` `paths` must contain non-empty globs or paths ``.\n2. For multi-path local calls, `partitionExistingPaths(..., parseFindPattern)` (`packages/coding-agent/src/tools/path-utils.ts`) stats each base path. Missing entries are skipped; if all are missing, the tool throws `Path not found: ...`. Single missing paths still hard-fail.\n3. The tool tries `resolveExplicitFindPatterns()` to merge multiple inputs into one search rooted at a common base path. If that does not apply, it parses one input with `parseFindPattern()`.\n4. `parseFindPattern()` determines `(basePath, globPattern, hasGlob)`:\n - no glob chars (`*`, `?`, `[`, `{`) => search that path with implicit `**/*`.\n - glob in the first segment => search from `.` and, unless the pattern already starts with `**/`, prefix it with `**/`.\n - glob later in the path => split at the first glob-bearing segment.\n5. `resolveToCwd()` converts the base path to an absolute path under the session cwd. A resolved `/` is rejected with `Searching from root directory '/' is not allowed`.\n6. `limit` is defaulted to `DEFAULT_LIMIT` (`1000`) and validated as a positive finite integer. `hidden` defaults to `true`. The tool also creates a 5 s timeout via `AbortSignal.timeout(GLOB_TIMEOUT_MS)`.\n7. Execution then branches:\n - **Custom operations branch**: if `FindToolOptions.operations.glob` exists, the tool checks existence with `operations.exists()`, short-circuits exact-file inputs via `operations.stat()` when available, then calls `operations.glob(globPattern, searchPath, { ignore: [\"**/node_modules/**\", \"**/.git/**\"], limit })`.\n - **Built-in local branch**: the tool stats `searchPath`. Exact-file inputs return immediately. Directory inputs call `natives.glob()` with `fileType: File`, `hidden`, `maxResults: limit`, `sortByMtime: true`, `gitignore: true`, and the combined abort signal.\n8. In the local branch, optional `onMatch` callbacks convert each match to a cwd-relative display path and emit throttled progress updates.\n9. After native glob returns, JS sorts `result.matches` by `mtime` descending (`(b.mtime ?? 0) - (a.mtime ?? 0)`) before formatting paths.\n10. `buildResult()` applies `applyListLimit()` to cap the array again at `limit`, joins paths with `\\n`, then runs `truncateHead()` with `maxLines: Number.MAX_SAFE_INTEGER`. In practice this leaves the 50 KB byte cap in place while disabling the default 3000-line cap.\n11. `toolResult()` packages text plus `details`, and records result-limit / truncation metadata for renderers.\n\n## Modes / Variants\n- **Exact file path**: if the parsed input has no glob and the resolved path stats as a file, output is that one path.\n- **Directory path**: if the parsed input has no glob and stats as a directory, the tool searches it with implicit `**/*`.\n- **Single glob path**: one input parsed by `parseFindPattern()`.\n- **Merged multi-path search**: multiple inputs resolved by `resolveExplicitFindPatterns()` into one brace-union glob rooted at a common base path.\n- **Partial multi-path search with missing inputs**: local multi-path calls skip missing base paths and surface them as `missingPaths` / `Skipped missing paths: ...`.\n- **Custom delegated search**: uses injected `FindOperations` instead of local fs + native glob.\n\n## Side Effects\n- Filesystem\n - Stats the resolved base path, and in local multi-path mode stats every candidate base path up front.\n - Does not write files.\n- Subprocesses / native bindings\n - Built-in local mode calls the native `@gajae-code/natives` glob implementation.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Emits structured progress updates when `onUpdate` is provided.\n - Adds truncation / limit metadata to the tool result.\n- Background work / cancellation\n - Local globbing is cancellable through the caller abort signal plus an internal 5 s timeout.\n\n## Limits & Caps\n- Default result limit: `1000` (`DEFAULT_LIMIT` in `packages/coding-agent/src/tools/find.ts`).\n- Local glob timeout: `5000` ms (`GLOB_TIMEOUT_MS` in `packages/coding-agent/src/tools/find.ts`).\n- Output byte cap: `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`).\n- Default generic line cap in `truncateHead()` is `3000`, but `find` overrides `maxLines` to `Number.MAX_SAFE_INTEGER`, so byte size — not line count — is the practical output truncation cap.\n- Streaming update throttle: `200` ms between `onUpdate` emissions.\n- Sort order: most recent `mtime` first in the built-in local branch and promised in the prompt. The tool re-sorts in JS even though native glob receives `sortByMtime: true` so native code can still stop early at `maxResults`.\n\n## Errors\n- User-facing `ToolError`s from `FindTool.execute()` include:\n - `` `paths` must contain non-empty globs or paths ``\n - `Path not found: ...`\n - `Searching from root directory '/' is not allowed`\n - `Limit must be a positive number`\n - `Path is not a directory: ...`\n - `find timed out after 5s`\n- If the caller aborts, the local branch converts `AbortError` into `ToolAbortError`.\n- Non-`ENOENT` stat failures and other unexpected errors are rethrown.\n- Empty matches are not errors; they return the no-files text result.\n\n## Notes\n- Reach for `find` for filename / path discovery. Reach for `search` when the selection criterion is file contents or regex matches; `search` takes a `pattern` and returns anchored content matches, while `find` only returns matching paths (`packages/coding-agent/src/prompts/tools/find.md`, `packages/coding-agent/src/prompts/tools/search.md`).\n- Bare top-level globs are made recursive. `*.ts` is parsed as base `.` plus glob `**/*.ts`; `src/*.ts` stays rooted at `src` with a non-recursive `*.ts` segment; `src/**/*.ts` preserves explicit recursion.\n- `.gitignore` is always enabled in the built-in local branch (`gitignore: true`). There is no model-facing flag to disable it.\n- `hidden` defaults to `true`; hidden-file exclusion is opt-out, not opt-in.\n- Multi-path missing-input tolerance only applies in the built-in local branch. The custom-operations branch hard-fails the first missing `searchPath` it checks.\n- The custom `FindOperations.glob()` hook receives `ignore` and `limit`, but not the `hidden` flag or an explicit `.gitignore` toggle. A remote delegate must account for that itself if it wants parity with the local branch.\n- Built-in local globbing asks the native layer for `fileType: File`, so recursive directory searches yield files, not directories. Directory outputs are only possible through exact-path passthrough or custom delegates that return them.\n",
|
|
125
125
|
"tools/github.md": "# github\n\n> Dispatch GitHub CLI operations for repositories, issues, pull requests, search, and Actions run watching.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/gh.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/github.md`\n- Key collaborators:\n - `packages/coding-agent/src/tools/gh-format.ts` — shorten commit SHAs for summaries.\n - `packages/coding-agent/src/tools/gh-renderer.ts` — TUI rendering, especially `run_watch` live/result views.\n - `packages/coding-agent/src/utils/git.ts` — `gh`/`git` process wrappers, repo locking, branch config writes.\n - `packages/utils/src/dirs.ts` — base directory for dedicated PR worktrees.\n - `packages/coding-agent/src/sdk/session.ts` — session artifact allocation hook.\n - `packages/coding-agent/src/session/artifacts.ts` — artifact filename format `<id>.<toolType>.log`.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"repo_view\" \\| \"pr_create\" \\| \"pr_checkout\" \\| \"pr_push\" \\| \"search_issues\" \\| \"search_prs\" \\| \"search_code\" \\| \"search_commits\" \\| \"search_repos\" \\| \"run_watch\"` | Yes | Dispatch selector. `GithubTool.execute()` switches only on this field. |\n| `repo` | `string` | No | `owner/repo` override. Ignored when the identifier argument is already a full GitHub URL. For `search_issues`/`search_prs`/`search_code`/`search_commits`, defaults to the current checkout's `owner/repo` when omitted (skipped when the query already contains a `repo:`/`org:`/`user:`/`owner:` qualifier or when current-repo resolution fails). Required in practice when `gh` cannot infer repo context from the current checkout. |\n| `branch` | `string` | No | Used by `repo_view`, `pr_push`, and `run_watch`. `run_watch` falls back to current git branch when `run` is omitted; `pr_push` falls back to current branch. |\n| `pr` | `string \\| string[]` | No | Used by `pr_checkout`. Each item may be a PR number, branch name, or GitHub PR URL. Array form enables batching. Omitted means current branch PR. |\n| `force` | `boolean` | No | Used only by `pr_checkout`. Defaults to `false`; allows resetting an existing `pr-<number>` local branch to the PR head commit. |\n| `forceWithLease` | `boolean` | No | Used only by `pr_push`; passed through to git push. |\n| `title` | `string` | No | Used only by `pr_create`. Required unless `fill` is `true`. |\n| `body` | `string` | No | Used only by `pr_create`. Mutually exclusive with `fill`. Empty/omitted body becomes `--body \"\"` to suppress the interactive editor. Non-empty body is written to a temp file and passed as `--body-file`. |\n| `base` | `string` | No | Used only by `pr_create`; passed as `--base`. |\n| `head` | `string` | No | Used only by `pr_create`; passed as `--head`. |\n| `draft` | `boolean` | No | Used only by `pr_create`. Defaults to `false`. |\n| `fill` | `boolean` | No | Used only by `pr_create`. Defaults to `false`. Mutually exclusive with `title` and `body`. |\n| `reviewer` | `string[]` | No | Used only by `pr_create`; each entry becomes `--reviewer`. |\n| `assignee` | `string[]` | No | Used only by `pr_create`; each entry becomes `--assignee`. |\n| `label` | `string[]` | No | Used only by `pr_create`; each entry becomes `--label`. |\n| `query` | `string` | No | Used by all `search_*` ops. Required there. |\n| `limit` | `number` | No | Used by all `search_*` ops. Defaults to `10`, floored, clamped to `50`, and must be `> 0`. |\n| `run` | `string` | No | Used only by `run_watch`. Must be a numeric run ID or full GitHub Actions run URL. |\n| `tail` | `number` | No | Used only by `run_watch`. Defaults to `15`, floored, clamped to `200`, and must be `> 0`. |\n\n## Outputs\nThe tool returns a single text result built by `buildTextResult()` in `packages/coding-agent/src/tools/gh.ts`.\n\n- `content`: one text block. Multi-item ops join sections with blank lines and `---` separators.\n- `sourceUrl`: set for single repo/PR/run results when a canonical URL is known.\n- `details`: optional structured metadata used by the TUI renderer.\n - Common fields: `artifactId`, `repo`, `branch`, `worktreePath`, `remote`, `remoteBranch`, `headSha`, `runId`, `runIds`, `status`, `conclusion`, `failedJobs`.\n - `pr_checkout` adds `checkouts: GhPrCheckoutSummary[]`.\n - `run_watch` adds `watch: GhRunWatchViewDetails`, which drives the custom live/result renderer in `packages/coding-agent/src/tools/gh-renderer.ts`.\n- Artifact trailer: when `artifactId` is present, the text body gets an appended line like `Full failed-job logs: artifact://<id>`.\n - `run_watch` allocates artifacts with `session.allocateOutputArtifact(\"github\")`; persistent sessions therefore save failed-log bodies as `<artifact-dir>/<id>.github.log`.\n\n`run_watch` is the only streaming op. It emits `onUpdate` snapshots while polling, then returns one final text result.\n\n## Flow\n1. `GithubTool.createIf()` exposes the tool only when `git.github.available()` finds `gh` on `PATH`.\n2. `GithubTool.execute()` wraps dispatch in `untilAborted()` and switches on `params.op`.\n3. Each op normalizes optional strings, arrays, booleans, and numeric caps locally in `packages/coding-agent/src/tools/gh.ts`.\n4. CLI execution goes through `git.github.run/json/text()` in `packages/coding-agent/src/utils/git.ts`:\n - spawns `gh ...` with `Bun.spawn()`;\n - trims stdout/stderr unless `trimOutput: false`;\n - maps common auth/repo-context failures into tool-facing `ToolError` messages;\n - `json()` rejects empty or invalid JSON.\n5. Read-style ops (`repo_view`, `search_*`) fetch JSON and format Markdown-like text summaries. Single-issue and single-PR views were moved out of the tool and now resolve through the `issue://` / `pr://` internal URL schemes, which share the same SQLite cache.\n7. PR diffs moved out of the tool. `pr://<N>/diff` lists changed files, `pr://<N>/diff/<i>` slices a single file, and `pr://<N>/diff/all` returns the full unified diff — see `docs/tools/read.md`. All three variants share one `gh pr diff` invocation through the `pr-diff` cache row.\n8. `pr_checkout` resolves PR metadata first, then enters `git.withRepoLock()` before any git mutation so parallel checkout calls for the same primary repo do not race on shared `.git` state.\n9. `pr_push` reads PR head metadata back from git branch config, derives a refspec, then pushes with `git.push()`.\n10. `pr_create` shells out once, then best-effort re-reads the created PR for a richer summary.\n11. `run_watch` chooses either run mode (`run` supplied) or commit mode (`run` omitted), polls GitHub Actions APIs every 3 seconds, emits streaming updates, and may save a full failed-log artifact before returning.\n12. Final text goes through `toolResult().text(...)`; if `session.allocateOutputArtifact()` returns a slot, failed-log text is persisted with `Bun.write()`.\n\n## Modes / Variants\n\n### `repo_view`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `repo`, `branch` |\n| `gh` command | `gh repo view [<repo>] [--branch <branch>] --json <GH_REPO_FIELDS>` |\n| Batching | None |\n| Output | `# <owner/repo>` header, description, URL, default branch, requested branch, visibility, permission, primary language, stars, forks, archive/fork flags, updated timestamp, homepage, topics. `sourceUrl = data.url`. |\n\nIf `repo` is omitted, `gh` repository resolution is used.\n\nSingle-issue and single-PR reads live in the `issue://<N>` / `pr://<N>` URL schemes (see `docs/tools/read.md`). They share `~/.gjc/cache/github-cache.db` (override via `GJC_GITHUB_CACHE_DB`) and the `github.cache.softTtlSec` / `github.cache.hardTtlSec` / `github.cache.enabled` settings. The cache retains rendered Markdown plus the raw JSON payload returned by `gh`, including private bodies, comments, reviews, and review comments when comments are enabled; rows are scoped by the local GitHub credential fingerprint. Root and repo-scoped reads (`issue://`, `pr://owner/repo`) issue a live `gh issue list` / `gh pr list` for browsing; query params `state`, `limit`, `author`, `label` pass through to `gh` (`issue://` accepts `state=open|closed|all`; `pr://` also accepts `merged`). PR diffs ride the same cache under `pr://<N>/diff[/…]`: the listing, full diff, and per-file slices all share one `pr-diff` row keyed by repo and PR number.\n\n### `pr_create`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` plus either `fill=true` or `title` |\n| Optional fields | `repo`, `title`, `body`, `base`, `head`, `draft`, `fill`, `reviewer[]`, `assignee[]`, `label[]` |\n| `gh` command | `gh pr create ...` with flags assembled from provided fields |\n| Batching | None |\n| Output | `# Created Pull Request ...` summary with URL, state, draft flag, base/head, author, created time, labels, optional body. `sourceUrl` is the created PR URL. |\n\nBranches:\n- `fill && (title || body !== undefined)` throws.\n- Non-empty `body` is written under a temp dir `gh-pr-body-*` in `os.tmpdir()`, passed as `--body-file`, then removed in `finally`.\n- After creation, the tool parses the returned URL and best-effort runs `gh pr view <number> --repo <repo> --json <GH_PR_FIELDS_NO_COMMENTS>`; failures there are swallowed.\n\n### `pr_checkout`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `repo`, `pr`, `force` |\n| `gh` command | For each requested PR: `gh pr view [<pr>] [--repo <repo>] --json <GH_PR_CHECKOUT_FIELDS>`; cross-repo PRs may also call `gh repo view <headRepository> --json <GH_REPO_CLONE_FIELDS>`. |\n| Batching | Yes. `pr` may be `string[]`; each PR is resolved in parallel, but git mutations are serialized per primary repo by `git.withRepoLock()`. |\n| Output | Single PR: checkout/worktree summary plus `details.repo`, `details.branch`, `details.worktreePath`, `details.remote`, `details.remoteBranch`, `details.checkouts`. Batched: `# <n> Pull Request Worktrees (...)` plus one section per PR and aggregated `details.checkouts`. |\n\nWorktree and metadata behavior:\n- Local branch name is always `pr-<number>`.\n- Worktree path is `path.join(getWorktreesDir(), encodeRepoPathForFilesystem(primaryRepoRoot), localBranch)`, where `getWorktreesDir()` is `~/.gjc/wt`; effective path is `~/.gjc/wt/<encoded-primary-repo-root>/pr-<number>`.\n- Existing worktree detection is by branch ref `refs/heads/pr-<number>` from `git.worktree.list()`.\n- New worktree creation calls `git.worktree.add(repoRoot, finalWorktreePath, localBranch, { signal })` after verifying the path is neither already registered nor already present on disk.\n- For same-repo PRs, remote is `origin`. For cross-repo PRs, the tool resolves a clone URL for the head repo, reuses an existing remote with the same URL when possible, or creates `fork-<owner>` / `fork-<owner>-<n>`.\n- The branch push metadata is persisted with `git config` under the repository's shared `.git/config` as:\n - `branch.pr-<number>.remote`\n - `branch.pr-<number>.merge`\n - `branch.pr-<number>.pushRemote`\n - `branch.pr-<number>.gjcPrHeadRef`\n - `branch.pr-<number>.gjcPrUrl`\n - `branch.pr-<number>.gjcPrIsCrossRepository`\n - `branch.pr-<number>.gjcPrMaintainerCanModify`\n- If `refs/heads/pr-<number>` already exists at a different commit, checkout fails unless `force=true`, in which case `git branch --force` resets it to the fetched PR head.\n- If a matching worktree already exists, the tool reuses it and reports `reused: true`.\n\n### `pr_push`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `branch`, `forceWithLease` |\n| `gh` command | None. This path uses git, not `gh`. |\n| Batching | None |\n| Output | `# Pushed Pull Request Branch` summary with local branch, remote, remote branch, remote URL, PR URL, and force-with-lease flag. `sourceUrl = prUrl` when known. |\n\nPush target resolution reads the `branch.<name>.gjcPrHeadRef`, `pushRemote`/`remote`, `gjcPrUrl`, `gjcPrMaintainerCanModify`, and `gjcPrIsCrossRepository` git-config keys written by `pr_checkout`. If the current checked-out branch matches the target branch, the source ref is `HEAD`; otherwise it pushes `refs/heads/<branch>`. The refspec is `HEAD:refs/heads/<headRef>` or `refs/heads/<branch>:refs/heads/<headRef>`.\n\n### `search_issues`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/issues -f q=\"<query> [repo:<repo>] is:issue\" -F per_page=<limit>` |\n| Batching | None |\n| Output | `# GitHub issues search`, echoed query, optional repo, result count, then one bullet per issue with repo/state/author/labels/timestamps/URL. |\n\n`repo` defaults to the current checkout's `owner/repo` via `resolveSearchRepoScope()` when omitted. The default is suppressed when the query already contains a leading `repo:`/`org:`/`user:`/`owner:` qualifier or when `gh repo view` fails to resolve the current checkout (e.g. outside a github remote).\n\n### `search_prs`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/issues -f q=\"<query> [repo:<repo>] is:pr\" -F per_page=<limit>` |\n| Batching | None |\n| Output | Same shape as `search_issues`, labeled as pull requests. |\n\n`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.\n\n### `search_code`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/code -f q=\"<query> [repo:<repo>]\" -F per_page=<limit> -H \"Accept: application/vnd.github.text-match+json\"` |\n| Batching | None |\n| Output | `# GitHub code search`, result count, then one bullet per match with path, repo, short commit SHA, URL, and first normalized text-match fragment line when present. |\n\n`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.\n\n### `search_commits`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/commits -f q=\"<query> [repo:<repo>]\" -F per_page=<limit>` |\n| Batching | None |\n| Output | `# GitHub commits search`, result count, then one bullet per commit: short SHA + first commit-message line, repo, author, date, URL. |\n\n`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.\n\n### `search_repos`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `limit` |\n| `gh` command | `gh api -X GET /search/repositories -f q=\"<query>\" -F per_page=<limit>` |\n| Batching | None |\n| Output | `# GitHub repositories search`, result count, then one bullet per repo with first description line, language, stars, forks, open issues, visibility, archive/fork flags, updated time, URL. |\n\n`repo` is intentionally not used for this op.\n\n### `run_watch`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `repo`, `branch`, `run`, `tail` |\n| `gh` command | Repo resolution: `gh repo view --json nameWithOwner -q .nameWithOwner` when `repo` and run URL repo are both absent. Single-run mode uses `gh api --method GET /repos/<repo>/actions/runs/<runId>` and `gh api --method GET /repos/<repo>/actions/runs/<runId>/jobs`. Commit mode uses `gh api --method GET /repos/<repo>/branches/<branch>`, `gh api --method GET /repos/<repo>/actions/runs`, `gh api --method GET /repos/<repo>/actions/runs/<runId>/jobs`, and `gh api /repos/<repo>/actions/jobs/<jobId>/logs` for failed jobs. |\n| Batching | Implicit batching only in commit mode: all workflow runs for one commit are tracked together. |\n| Output | Streaming watch snapshots via `onUpdate`, then a final text report. On failure, appends `Full failed-job logs: artifact://<id>` and sets `details.artifactId`. |\n\nWatch flow:\n- `run` parsing accepts either a decimal run ID or a full run URL. URL repo must match explicit `repo` when both are given.\n- Poll interval is fixed at 3 seconds (`RUN_WATCH_INTERVAL_DEFAULT`).\n- Failure grace period is fixed at 5 seconds (`RUN_WATCH_GRACE_DEFAULT`). When any failed job appears before completion, the tool emits a note, waits once, re-fetches state, then collects logs so concurrent failures are included.\n- Failed-job logs are fetched with `gh api /repos/<repo>/actions/jobs/<jobId>/logs` via `git.github.run()`, not `json()`. Non-zero exit leaves `available: false` instead of failing the whole watch.\n- Inline result includes only the last `tail` lines per failed job. The saved artifact contains full logs (`mode: \"full\"`).\n- In commit mode, success is intentionally double-checked: once all known runs are successful, the tool waits one more poll interval and succeeds only if the set of run IDs is unchanged. This avoids returning before late workflow runs appear for the same commit.\n- `details.watch` drives a specialized renderer in `packages/coding-agent/src/tools/gh-renderer.ts`; non-watch results fall back to generic text rendering.\n\n## Side Effects\n- Filesystem\n - `pr_create` may create a temp dir under `os.tmpdir()` named `gh-pr-body-*`, write `body.md`, then remove the dir in `finally`.\n - `pr_checkout` may create directories under `~/.gjc/wt/<encoded-primary-repo-root>/` and add git worktrees there.\n - `run_watch` may write a session artifact with full failed-job logs.\n- Network\n - Every op shells out to `gh`, which then talks to GitHub APIs except `pr_push`.\n - `pr_push` uses git network transport to the configured remote.\n- Subprocesses / native bindings\n - All `gh` calls use `Bun.spawn([\"gh\", ...args])`.\n - `pr_checkout` and `pr_push` also invoke git helpers from `packages/coding-agent/src/utils/git.ts`.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - `run_watch` consumes `session.allocateOutputArtifact()` when failed-job logs are persisted.\n - Returned `details` objects carry run/checkouts metadata for the renderer/UI.\n- User-visible prompts / interactive UI\n - `gh` interactive editor fallback is suppressed for `pr_create` by forcing either `--body-file` or `--body \"\"`.\n - `gh-renderer` provides compact headers for all ops and a custom live watch view for `run_watch`.\n- Background work / cancellation\n - `run_watch` loops until success/failure and uses `scheduler.wait()` between polls.\n - `GithubTool.execute()` is wrapped in `untilAborted()`; `git.github.run()` forwards the abort signal into `Bun.spawn()`.\n\n## Limits & Caps\n- Search result default: `10` (`SEARCH_LIMIT_DEFAULT` in `packages/coding-agent/src/tools/gh.ts`).\n- Search result max: `50` (`SEARCH_LIMIT_MAX`).\n- PR file preview inside the `pr://` view: first `50` files only (`FILE_PREVIEW_LIMIT` in `gh.ts`).\n- Run-watch poll interval: `3s` (`RUN_WATCH_INTERVAL_DEFAULT`).\n- Run-watch failure grace period: `5s` (`RUN_WATCH_GRACE_DEFAULT`).\n- Run-watch failed-log tail default: `15` lines (`RUN_WATCH_TAIL_DEFAULT`).\n- Run-watch failed-log tail max: `200` lines (`RUN_WATCH_TAIL_MAX`).\n- PR review comments page size: `100` (`REVIEW_COMMENTS_PAGE_SIZE`).\n- Actions jobs page size: `100` (`RUN_JOBS_PAGE_SIZE`).\n- Search and tail numeric inputs are floored with `Math.floor()`, clamped to the max, and rejected when non-finite or `<= 0`.\n- `pr_checkout` batch fan-out is unbounded in tool code; all requested PRs are launched with `Promise.all()`.\n\n## Errors\n- Tool creation is skipped entirely when `gh` is not installed.\n- `git.github.run()` throws `ToolError(\"GitHub CLI (gh) is not installed...\")` if `gh` is missing at execution time.\n- `git.github.text/json()` map common failures to model-facing messages:\n - not authenticated → `GitHub CLI is not authenticated. Run \\`gh auth login\\`.`\n - missing repo context without explicit `repo` → `GitHub repository context is unavailable. Pass \\`repo\\` explicitly or run the tool inside a GitHub checkout.`\n - otherwise stderr/stdout text, or fallback `GitHub CLI command failed: gh ...`\n- `json()` also throws on empty stdout or invalid JSON.\n- Local validation errors throw `ToolError`, including:\n - missing required per-op fields (`query`, `title unless fill=true`)\n - invalid numeric `limit` / `tail`\n - invalid `run` format\n - `fill` combined with `title` or `body`\n - missing git repo / branch / HEAD context for checkout, push, or watch\n - `pr_push` on a branch without `gjcPrHeadRef` metadata\n - conflicting existing worktree path or branch without `force`\n- `run_watch` treats failed-job log fetches specially: missing log content does not fail the watch; it marks that log `available: false` and prints `Log tail unavailable.` / `Full log unavailable.`.\n- `pr_create` swallows only the post-create best-effort `gh pr view` refresh; the create step itself still fails normally.\n\n## Notes\n- `appendRepoFlag()` intentionally skips `--repo` when the identifier argument is already a full GitHub URL; that lets `gh` derive repo/number from the URL.\n- `normalizePrIdentifierList()` accepts `reviewer`, `assignee`, and `label` arrays too; the helper name is broader than its callers.\n- `pr_push` depends on `pr_checkout` having run first for that local branch; there is no alternate metadata source.\n- `pr_checkout` stores push metadata in branch config, not in the worktree directory. Reusing the same `pr-<number>` branch reuses those config keys.\n- Worktree write serialization is keyed by the primary repo root, not the current worktree path, because git worktrees share `.git/config`, `packed-refs`, commit-graph, and worktree metadata files.\n- `search_repos` is the only search op that never forwards `repo`; repository scoping must be expressed in the query itself.\n- `run_watch` success on commit mode means “all observed runs succeeded and no additional runs appeared one poll later”, not merely “latest poll looked green”.\n- The TUI renderer collapses failed log previews unless the result view is expanded; the underlying text result still contains the same tailed lines plus any artifact reference.\n",
|
|
126
126
|
"tools/irc.md": "# irc\n\n> Send short prose messages to other live agents in the current process.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/irc.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/irc.md`\n- Key collaborators:\n - `packages/coding-agent/src/registry/agent-registry.ts` — process-global live agent directory.\n - `packages/coding-agent/src/session/agent-session.ts` — side-channel reply generation and history injection.\n - `packages/coding-agent/src/prompts/system/irc-incoming.md` — no-tools auto-reply prompt.\n - `packages/coding-agent/src/tools/index.ts` — tool availability gating.\n - `packages/coding-agent/src/config/settings-schema.ts` — `irc.enabled` default.\n - `packages/coding-agent/src/modes/controllers/event-controller.ts` — renders IRC events into chat UI.\n - `packages/coding-agent/src/modes/utils/ui-helpers.ts` — formats `[IRC]` transcript lines.\n - `packages/coding-agent/src/task/executor.ts` — carries `irc.enabled` into subagents.\n\n## Inputs\n\n### `op: \"list\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"list\"` | Yes | Lists peers visible to the caller. |\n\n### `op: \"send\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"send\"` | Yes | Sends one message to one peer or to `\"all\"`. |\n| `to` | `string` | Yes | Peer id such as `0-Main`, or `\"all\"` for broadcast. Whitespace is trimmed. |\n| `message` | `string` | Yes | Message body. Whitespace is trimmed; empty-after-trim is rejected. |\n| `awaitReply` | `boolean` | No | Wait for prose replies. Defaults to `true` for direct messages and `false` for `to: \"all\"`. |\n\n## Outputs\n- Single-shot `AgentToolResult`; no streaming updates.\n- `content` is one text block.\n - `list` returns either `No other live agents.` or a bullet list headed by `<n> peer(s):`.\n - `send` returns delivery summary text, then optional `## Replies`, `## Failed`, and `Unknown / unavailable peers:` sections.\n- `details` is structured metadata:\n - `list`: `{ op, from, peers, channels }`\n - `send`: `{ op, from, to, delivered, replies?, failed?, notFound? }`\n- The tool does not return raw IRC frames, message ids, or a transcript object.\n\n## Flow\n1. `IrcTool.createIf` only constructs the tool when `irc.enabled` is on and the session has both an `AgentRegistry` and `getAgentId` (`packages/coding-agent/src/tools/irc.ts`).\n2. Tool discovery adds another gate in `packages/coding-agent/src/tools/index.ts`: if the caller is `0-Main` and `async.enabled` is off, `irc` is hidden because the main agent cannot talk to concurrent peers in sync mode.\n3. `execute` resolves the process-global registry and sender id. Missing either returns a text error result instead of throwing.\n4. `op: \"list\"` calls `registry.listVisibleTo(senderId)`, which exposes every other agent in flat namespace whose status is `running` or `idle` (`packages/coding-agent/src/registry/agent-registry.ts`).\n5. `list` formats human-readable lines and returns `channels` as `['all', ...peerIds]`. These are logical targets only; there is no channel join state.\n6. `op: \"send\"` trims `to` and `message`; missing values produce text errors.\n7. `send` resolves targets:\n - `to === \"all\"`: all visible peers.\n - otherwise: one exact registry id, excluding self and excluding peers not in `running`/`idle`.\n8. `send` chooses `awaitReply = params.awaitReply ?? !isBroadcast`.\n9. Each target is dispatched in parallel via `target.session.respondAsBackground(...)`. One slow or failing peer does not block dispatch to the others.\n10. `respondAsBackground` accepts each delivery into the recipient's volatile current-session exchange queue before observing it in the recipient or main UI, and before reporting sender delivery success:\n - `awaitReply === false`: accepts/queues the incoming message, then emits its `irc_message` event and forwards the display-only relay to the main session UI.\n - `awaitReply === true`: renders `packages/coding-agent/src/prompts/system/irc-incoming.md` and runs `runEphemeralTurn` with `toolChoice: \"none\"`. After a reply succeeds, it constructs and accepts/queues the ordered incoming + auto-reply pair, commits its IRC roster claim, then emits both `irc_message` events and forwards both display-only relays. A failed or aborted reply turn accepts and surfaces nothing.\n11. Deferred injection waits until the recipient is no longer streaming; `#flushPendingBackgroundExchanges` appends accepted custom messages through normal `message_start`/`message_end` external events so persistence and listeners see them.\n12. `send` aggregates `delivered`, `replies`, `failed`, and `notFound`, then returns one text summary plus matching `details`.\n\n## Modes / Variants\n- `list`: enumerate visible peers and logical channels.\n- `send` direct message: one exact peer id, default synchronous auto-reply.\n- `send` broadcast: `to: \"all\"`, default fire-and-forget (`awaitReply: false`) to every visible peer.\n- `send` with `awaitReply: false`: recipient records the incoming message but does not generate a reply.\n- `send` with `awaitReply: true`: recipient performs a no-tools ephemeral LLM turn and returns prose.\n\n## Side Effects\n- Session state\n - Reads from the process-global `AgentRegistry`.\n - Accepts each IRC delivery into a process-local, volatile recipient exchange queue before recipient/main observations and sender success. This acceptance is not durable delivery.\n - Emits `irc_message` session events on recipient sessions after acceptance.\n - Flushes accepted IRC custom messages into recipient history after the current stream finishes.\n - For non-main recipients, forwards display-only relay observations into the main session UI after acceptance; these relays are not persisted to the main agent history. Observer failures are isolated from accepted delivery.\n - Subagents inherit `irc.enabled` from task executor settings.\n- User-visible prompts / interactive UI\n - IRC events render as `[IRC]` transcript lines in the TUI.\n - Auto-replies are generated from `packages/coding-agent/src/prompts/system/irc-incoming.md` and explicitly forbid tool use.\n- Background work / cancellation\n - `send` starts one background `respondAsBackground` call per target.\n - The caller's `AbortSignal` is forwarded into each background reply turn.\n- Network\n - No IRC server connection.\n - When `awaitReply: true`, the recipient may make model-provider API calls through `runEphemeralTurn`.\n- Filesystem\n - No direct filesystem writes in the tool itself.\n\n## Limits & Caps\n- Availability gates:\n - `irc.enabled` defaults to `true` in `packages/coding-agent/src/config/settings-schema.ts`.\n - Main agent tool discovery suppresses `irc` when `async.enabled` is off (`packages/coding-agent/src/tools/index.ts`).\n- Visibility scope: only peers in status `running` or `idle` are addressable via `listVisibleTo`.\n- Reply execution:\n - No tools are available in auto-reply turns (`toolChoice: \"none\"` in `runEphemeralTurn`).\n - No internal timeout, retry, backoff, rate limit, or reply length cap is defined in `irc.ts`; behavior relies on the underlying model stream and any upstream API limits.\n- Flush scheduling: deferred history injection polls every `50` ms while the recipient is still streaming (`#scheduleBackgroundExchangeFlush` in `packages/coding-agent/src/session/agent-session.ts`).\n\n## Errors\n- The tool returns text errors, not thrown exceptions, for:\n - missing registry: `IRC is unavailable in this session.`\n - missing sender id: `IRC is unavailable: caller has no agent id.`\n - missing `to`: `` `to` is required for op=\"send\". ``\n - missing `message`: `` `message` is required for op=\"send\". ``\n - unknown op: `Unknown irc op.`\n- Unknown, self-addressed, non-running, and non-idle direct targets are reported under `details.notFound` and in the text footer `Unknown / unavailable peers:`.\n- If a target has no attached session, it is treated as not found.\n- Exceptions from reply generation before awaited-exchange acceptance are caught per-target and surfaced under `details.failed` as `{ id, error }`; other recipients still complete. A provider failure or sender abort therefore emits no `irc_message` observations and accepts no recipient exchange. Recipient/main observer failures after acceptance are isolated and do not turn a delivered exchange into a sender failure.\n- If no target succeeds, `send` still returns normally with `No recipients received the message.` and optional `failed`/`notFound` metadata.\n\n## Notes\n- This is IRC-like naming only. There are no servers, sockets, nick registration, auth handshakes, channels beyond `all`, or commands such as join/part/topic.\n- Addressing is by exact agent id from the registry; there is no fuzzy lookup or aliasing.\n- `channels` in `list` is synthetic output: `all` plus visible peer ids. Nothing is persisted across calls as channel membership.\n- Recipient history, not sender history, receives accepted IRC custom messages when the recipient flushes its current-session queue. Acceptance is process-local and volatile: it does not promise durable storage, fsync, recovery, deduplication, or replay across process loss.\n- The main UI may show IRC relays for conversations it was not part of, but those relay records are explicitly display-only.\n- Because reply generation snapshots in-flight assistant text, a recipient can answer based on partially streamed context.\n- Direct self-messaging is rejected by resolving the target as unavailable.\n\n## Sidebar\n\n- `irc.sidebar.enabled` defaults to `true`: the read-only sidebar is available when `irc.enabled` is also enabled, but starts closed. `app.irc.sidebar.toggle` (default `Alt+I`, remappable) opens or hides it.\n- The sidebar retains the active runtime UI session's IRC observations only. It is not written to disk or restored into another session. Each arrival decides its inline lifetime once: arrivals while the panel is visible expire 10 seconds after observation; arrivals while it is closed persist inline. Later toggles do not change that decision.\n- An eligible first live inline arrival while the sidebar is closed shows a one-time hint using the resolved toggle key (for example, `Alt+I opens sidebar`). Rebuilds do not show or consume this hint.\n- When open, the transcript/sidebar split targets 70:30. The sidebar keeps a 30-column minimum only while the transcript can retain at least half the usable width; below that boundary the sidebar yields completely and the transcript renders full width. Retained sidebar messages are Discord-style blocks: `sender → recipient · HH:mm`, followed by the retained body with a two-column indent and one blank row between messages. Sender and recipient display fields are normalized and bounded to 256 UTF-8 bytes at complete grapheme boundaries.\n- The sidebar retains at most 10,000 observations and 16 MiB of UTF-8 message payload per runtime UI session, evicting the oldest observations first. An individual observation larger than the 16 MiB budget is omitted rather than truncated or admitted by evicting the rest of the backlog. Replay suppression tracks at most 100,000 unique observation identities and then fails closed for unseen arrivals until the runtime UI instance ends, preventing forgotten identities from resurrecting across eviction or fork cleanup while keeping memory bounded. Both inline transcript and sidebar body rendering independently materialize at most 2,048 rows from a bounded 64 KiB UTF-8 source projection, preferring recent retained content and showing explicit message/backlog elision markers when necessary. The bounded, runtime-only, read-only backlog does not affect welcome-screen row reservation, which counts transcript rows only.\n- While the sidebar is visible, Kitty terminals keep rendering real images in the transcript (Kitty placements are cursor-neutral and compose safely with the split). Cursor-advancing protocols — iTerm2 inline images and raw SIXEL sequences — are represented by compact text placeholders so they cannot corrupt the split. Hiding the sidebar restores normal rendering for every protocol.\n- A successful `/fork` starts a new logical UI session: it clears the sidebar ledger, hides the panel, and resets roster-delivery state. Failed or cancelled forks preserve the current runtime sidebar state.\n\n## Hidden peer roster reminders\n\nWhen the live peer roster changes, an eligible model turn receives one hidden single-line reminder listing stable agent ids and roster labels. The initial empty roster produces no reminder; a later transition to empty does. Running/idle status changes alone do not count as a roster change.\n\nNormal turns commit an atomic roster claim on successful completion. Awaited IRC auto-replies defer that commit until their incoming + auto-reply exchange is accepted; failed or aborted reply turns release the claim for a later retry. Isolated `/btw` turns do not acquire or commit roster claims. These reminders are context-only and never appear in the transcript or persisted history.",
|
|
127
|
-
"tools/job.md": "# job\n\n> Wait for or cancel background jobs managed by the session async runtime.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/job.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/job.md`\n- Key collaborators:\n - `packages/coding-agent/src/async/job-manager.ts` — job registry, cancellation, delivery suppression.\n - `packages/coding-agent/src/async/support.ts` — feature gating for background jobs.\n - `packages/coding-agent/src/tools/bash.ts` — explicit async bash and auto-backgrounded bash jobs.\n - `packages/coding-agent/src/task/index.ts` — async task-job scheduling.\n - `packages/coding-agent/src/sdk/session.ts` — automatic follow-up delivery for unsuppressed completions.\n - `packages/coding-agent/src/config/settings-schema.ts` — `async.pollWaitDuration` options.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `poll` | `string[]` | No | Job ids to watch. Cannot be combined with `list`. If omitted (and `cancel` is also omitted), the tool watches all running jobs. If provided, missing ids are silently filtered out before waiting. |\n| `cancel` | `string[]` | No | Job ids to cancel before any polling. Missing ids are reported as `not_found`; non-running ids as `already_completed`. |\n| `list` | `boolean` | No | Return an immediate snapshot of every job spawned by the calling agent (running + completed within retention) without waiting. Read-only — cannot be combined with `poll` or `cancel`. |\n\n## Outputs\nThe tool returns one text block plus `details`.\n\n- `content[0].text`: markdown-like plain text sections assembled by `#buildResult(...)`:\n - `## Cancelled (N)` for cancel outcomes.\n - `## Completed (N)` for non-running jobs, including stored `resultText` and `errorText`.\n - `## Still Running (N)` for jobs still in `running`.\n- `details.jobs`: array of snapshots:\n - `id: string`\n - `type: \"bash\" | \"task\"`\n - `status: \"running\" | \"completed\" | \"failed\" | \"cancelled\"`\n - `label: string`\n - `durationMs: number`\n - optional `resultText`, `errorText`\n- `details.cancelled` appears only when `cancel` was passed; each item is `{ id, status }` where status is `\"cancelled\" | \"not_found\" | \"already_completed\"`.\n\nStreaming behavior:\n- During a polling wait, `execute(...)` emits `onUpdate(...)` every 500 ms with an empty text block and fresh `details.jobs` snapshots.\n- Final return is single-shot after a completion, timeout, abort, or immediate fast path.\n\nRead-only snapshot path:\n- Calling `job` with `list: true` returns a markdown summary of every job spawned by the calling agent (running + completed within retention) without waiting.\n\n## Flow\n1. `JobTool.createIf(...)` in `packages/coding-agent/src/tools/job.ts` only exposes the tool when `isBackgroundJobSupportEnabled(...)` returns true for either `async.enabled` or `bash.autoBackground.enabled`.\n2. `execute(...)` fetches `session.asyncJobManager`. If absent, it returns `Async execution is disabled; no background jobs are available.`\n3. `cancel` ids are processed first:\n - `manager.getJob(id)` missing → `not_found`.\n - existing job with `status !== \"running\"` → `already_completed`.\n - running job → `manager.cancel(id)`, which sets `job.status = \"cancelled\"`, aborts the controller, and schedules eviction.\n4. Polling mode is chosen with `const shouldPoll = requestedPollIds !== undefined || cancelIds.length === 0`:\n - only `cancel` present → return immediately, no wait.\n - explicit `poll`, or no args at all → proceed to watch jobs.\n5. Watch set resolution:\n - explicit `poll` → map ids through `manager.getJob(...)` and drop missing ones.\n - no `poll` and no `cancel` → `manager.getRunningJobs()`.\n6. Empty watch set returns immediately:\n - if cancellations happened, return snapshots for the cancelled ids that still exist.\n - else return either `No matching jobs found for IDs: ...` or `No running background jobs to wait for.`\n7. If every watched job is already non-running, `#buildResult(...)` returns immediately without waiting.\n8. Otherwise the tool waits on `Promise.race(...)` across:\n - every watched running job's `job.promise`,\n - a timeout promise for `async.pollWaitDuration`,\n - the tool-call abort signal when present.\n9. Before waiting, it calls `manager.watchJobs(watchedJobIds)`. This suppresses automatic completion delivery for those ids while they are being watched.\n10. If `onUpdate` exists, a 500 ms interval sends progress snapshots from `#snapshotJobs(...)`; one snapshot is emitted immediately before entering the race.\n11. In `finally`, the tool always calls `manager.unwatchJobs(...)`, clears the timeout, and stops the progress interval.\n12. `#buildResult(...)` deduplicates jobs, snapshots current manager state, then calls `manager.acknowledgeDeliveries(...)` for every non-running job in the result. That suppresses later automatic follow-up delivery for the same completions and removes queued deliveries for those ids.\n13. The final text groups jobs by non-running vs still-running state. A timeout is not an error path; it simply returns the current snapshot.\n\n## Modes / Variants\n- Poll all running jobs: call with neither `poll` nor `cancel`.\n- Poll explicit ids: call with `poll` only.\n- Cancel only: call with `cancel` only; cancellations happen and the tool returns immediately.\n- Cancel then poll: call with both. Cancellations are applied first, then the tool watches the remaining resolved `poll` ids.\n- Read-only inspection: call with `list: true` for the same snapshot data without waiting on completion.\n\nForeground fold contract:\n- The `app.tool.backgroundFold` chord (Alt+Shift+B by default; Cmd+B on macOS) can fold managed non-PTY Bash, ACP client-terminal, and PTY waits into the same job manager.\n- Folded completion wakes a fresh turn with a receipt containing bounded output, the original request, and a `job` retrieval handle. The original command deadline remains active and `job tail` is available while the job runs.\n- PTY overlay disposal is non-owning; explicit Escape and `job cancel` stop the PTY through its lifecycle hook. `task` and `subagent` waits are not foldable.\n\nSpawn paths that produce jobs:\n- `packages/coding-agent/src/tools/bash.ts`\n - `async: true` always registers a `type: \"bash\"` job with `AsyncJobManager.register(...)` and returns a start message.\n - auto-background mode (`bash.autoBackground.enabled`) starts the same managed job path for non-PTY commands, waits up to `min(bash.autoBackground.thresholdMs, timeoutMs - 1000)`, and if the command is still running returns a background-job start result instead of inline command output.\n- `packages/coding-agent/src/task/index.ts`\n - when `async.enabled` is on, the chosen agent is not blocking, and `tasks.length > 0`, each task item is registered as a `type: \"task\"` job.\n\nLifecycle and exact state names:\n- Conceptual scheduling path: `pending` (only task-progress bookkeeping before work starts) → `running` → `completed` / `failed`; cancellation changes a running async job to `cancelled`.\n- Exact `AsyncJob.status` values in `packages/coding-agent/src/async/job-manager.ts`: `\"running\" | \"completed\" | \"failed\" | \"cancelled\"`.\n- Exact per-task progress values in `packages/coding-agent/src/task/types.ts`: `\"pending\" | \"running\" | \"completed\" | \"failed\" | \"aborted\"`.\n\n## Side Effects\n- Filesystem\n - None in `job.ts` itself.\n - Jobs being observed may already have written artifacts/results through their own tool runtimes.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Reads and mutates `session.asyncJobManager` state.\n - `watchJobs(...)` / `unwatchJobs(...)` toggle delivery suppression for the watched ids.\n - `acknowledgeDeliveries(...)` marks completed ids as suppressed and removes queued deliveries for them.\n - `cancel(...)` aborts running jobs through each job's `AbortController`.\n- User-visible prompts / interactive UI\n - Polling emits periodic `onUpdate` snapshots every 500 ms.\n - Automatic job completion follow-ups are generated by `packages/coding-agent/src/sdk/session.ts` only for unsuppressed deliveries.\n- Background work / cancellation\n - Waiting uses a timeout plus optional tool-call abort signal.\n - Cancelling a job does not synchronously await teardown; it flips state, aborts, and returns control to the manager/job promise.\n\n## Limits & Caps\n- Poll wait duration comes from `async.pollWaitDuration` in `packages/coding-agent/src/config/settings-schema.ts`:\n - allowed values: `5s`, `10s`, `30s`, `1m`, `5m`\n - default: `30s`\n- Progress update cadence while polling: `PROGRESS_INTERVAL_MS = 500` in `packages/coding-agent/src/tools/job.ts`.\n- Async job retention default: `DEFAULT_RETENTION_MS = 5 * 60 * 1000` in `packages/coding-agent/src/async/job-manager.ts`.\n- Manager fallback max-running limit: `DEFAULT_MAX_RUNNING_JOBS = 15` in `packages/coding-agent/src/async/job-manager.ts`.\n- Session wiring clamps `async.maxJobs` to `1..100` before constructing the manager in `packages/coding-agent/src/sdk/session.ts`; settings default is `100` in `packages/coding-agent/src/config/settings-schema.ts`.\n- Async completion delivery retry backoff in `packages/coding-agent/src/async/job-manager.ts`:\n - base `500` ms\n - max `30_000` ms\n - jitter `< 200` ms\n - exponent capped at 8 doublings\n\n## Errors\n- Tool-disabled path is returned as normal text, not thrown: `Async execution is disabled; no background jobs are available.`\n- Polling a nonexistent id is not an exception:\n - with `poll` only, missing ids are dropped; if none remain the tool returns `No matching jobs found for IDs: ...`.\n - with `cancel`, each missing id is reported as `not_found` in `details.cancelled` and text.\n- Cancelling a non-running job is not an exception; it reports `already_completed` even if the actual status is `completed`, `failed`, or `cancelled`.\n- Tool-call abort during polling stops waiting and returns a final snapshot through `#buildResult(...)`; it does not cancel watched jobs.\n- Failures inside the underlying async work are stored on the job (`status: \"failed\"`, `errorText`) and reported in normal tool output, not rethrown by `job`.\n- Calling `list: true` against an empty manager returns a normal empty-list result rather than throwing; missing ids passed to `poll` are silently filtered.\n\n## Notes\n- `job` waits for the first watched running job to settle, not for all watched jobs. If others remain `running`, they are reported under `## Still Running`; the caller must invoke `job` again to continue waiting.\n- Delivery suppression is the key difference between snapshot and automatic delivery:\n - snapshots (`job` calls with `poll` or `list: true`) read current manager state;\n - follow-up delivery comes from `AsyncJobManager.#enqueueDelivery(...)` and `sdk.ts` `onJobComplete`;\n - watched or acknowledged ids are suppressed via `isDeliverySuppressed(...)`.\n- `manager.cancel(id)` sets `status = \"cancelled\"` before the underlying promise settles. The job function may later populate `resultText` or `errorText`; `job-manager.ts` preserves that text but does not transition the status away from `cancelled`.\n- Retention eviction removes the job record, suppression flags, and watch flag together. After eviction, both `job` calls and `list: true` snapshots behave as if the id never existed.\n",
|
|
127
|
+
"tools/job.md": "# job\n\n> Wait for or cancel background jobs managed by the session async runtime.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/job.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/job.md`\n- Key collaborators:\n - `packages/coding-agent/src/async/job-manager.ts` — job registry, cancellation, delivery suppression.\n - `packages/coding-agent/src/async/support.ts` — feature gating for background jobs.\n - `packages/coding-agent/src/tools/bash.ts` — explicit async bash and auto-backgrounded bash jobs.\n - `packages/coding-agent/src/task/index.ts` — async task-job scheduling.\n - `packages/coding-agent/src/sdk/session.ts` — automatic follow-up delivery for unsuppressed completions.\n - `packages/coding-agent/src/config/settings-schema.ts` — `async.pollWaitDuration` options.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `poll` | `string[]` | No | Job ids to watch. Cannot be combined with `list`. If omitted (and `cancel` is also omitted), the tool watches all running jobs. If provided, missing ids are silently filtered out before waiting. |\n| `cancel` | `string[]` | No | Job ids to cancel before any polling. Missing ids are reported as `not_found`; non-running ids as `already_completed`. |\n| `list` | `boolean` | No | Return an immediate snapshot of every job spawned by the calling agent (running + completed within retention) without waiting. Read-only — cannot be combined with `poll` or `cancel`. |\n\n## Outputs\nThe tool returns one text block plus `details`.\n\n- `content[0].text`: markdown-like plain text sections assembled by `#buildResult(...)`:\n - `## Cancelled (N)` for cancel outcomes.\n - `## Completed (N)` for non-running jobs, including stored `resultText` and `errorText`.\n - `## Still Running (N)` for jobs still in `running`.\n- `details.jobs`: array of snapshots:\n - `id: string`\n - `type: \"bash\" | \"task\"`\n - `status: \"running\" | \"completed\" | \"failed\" | \"cancelled\"`\n - `label: string`\n - `durationMs: number`\n - optional `resultText`, `errorText`\n- `details.cancelled` appears only when `cancel` was passed; each item is `{ id, status }` where status is `\"cancelled\" | \"not_found\" | \"already_completed\"`.\n\nStreaming behavior:\n- During a polling wait, `execute(...)` emits `onUpdate(...)` every 500 ms with an empty text block and fresh `details.jobs` snapshots.\n- Final return is single-shot after a completion, timeout, abort, or immediate fast path.\n\nRead-only snapshot path:\n- Calling `job` with `list: true` returns a markdown summary of every job spawned by the calling agent (running + completed within retention) without waiting.\n\n## Flow\n1. `JobTool.createIf(...)` in `packages/coding-agent/src/tools/job.ts` only exposes the tool when `isBackgroundJobSupportEnabled(...)` returns true for either `async.enabled` or `bash.autoBackground.enabled`.\n2. `execute(...)` fetches `session.asyncJobManager`. If absent, it returns `Async execution is disabled; no background jobs are available.`\n3. `cancel` ids are processed first:\n - `manager.getJob(id)` missing → `not_found`.\n - existing job with `status !== \"running\"` → `already_completed`.\n - running job → `manager.cancel(id)`, which sets `job.status = \"cancelled\"`, aborts the controller, and schedules eviction.\n4. Polling mode is chosen with `const shouldPoll = requestedPollIds !== undefined || cancelIds.length === 0`:\n - only `cancel` present → return immediately, no wait.\n - explicit `poll`, or no args at all → proceed to watch jobs.\n5. Watch set resolution:\n - explicit `poll` → map ids through `manager.getJob(...)` and drop missing ones.\n - no `poll` and no `cancel` → `manager.getRunningJobs()`.\n6. Empty watch set returns immediately:\n - if cancellations happened, return snapshots for the cancelled ids that still exist.\n - else return either `No matching jobs found for IDs: ...` or `No running background jobs to wait for.`\n7. If every watched job is already non-running, `#buildResult(...)` returns immediately without waiting.\n8. Otherwise the tool waits on `Promise.race(...)` across:\n - every watched running job's `job.promise`,\n - a timeout promise for `async.pollWaitDuration`,\n - the tool-call abort signal when present.\n9. Before waiting, it calls `manager.watchJobs(watchedJobIds)`. This suppresses automatic completion delivery for those ids while they are being watched.\n10. If `onUpdate` exists, a 500 ms interval sends progress snapshots from `#snapshotJobs(...)`; one snapshot is emitted immediately before entering the race.\n11. In `finally`, the tool always calls `manager.unwatchJobs(...)`, clears the timeout, and stops the progress interval.\n12. `#buildResult(...)` deduplicates jobs, snapshots current manager state, then calls `manager.acknowledgeDeliveries(...)` for every non-running job in the result. That suppresses later automatic follow-up delivery for the same completions and removes queued deliveries for those ids.\n13. The final text groups jobs by non-running vs still-running state. A timeout is not an error path; it simply returns the current snapshot.\n\n## Modes / Variants\n- Poll all running jobs: call with neither `poll` nor `cancel`.\n- Poll explicit ids: call with `poll` only.\n- Cancel only: call with `cancel` only; cancellations happen and the tool returns immediately.\n- Cancel then poll: call with both. Cancellations are applied first, then the tool watches the remaining resolved `poll` ids.\n- Read-only inspection: call with `list: true` for the same snapshot data without waiting on completion.\n\nForeground fold contract:\n- The `app.tool.backgroundFold` chord (Alt+Shift+B by default; Cmd+B on macOS) can fold managed non-PTY Bash, ACP client-terminal, and PTY waits into the same job manager. A user steer admitted into the live run folds the same waits automatically after a fixed 2 s grace window (`STEER_FOLD_GRACE_MS`) when `busyPromptMode=steer` (`toolInterruptPolicy` is not a gate).\n- A steer fold appends `Folded into background job <id> because a user steer arrived; the command keeps running with its original timeout and its result will wake a later turn.` to the background-start result. Every fold records `metadata.foldReason` (`steer` | `chord` | `timer` | `sdk_control`); `list: true` snapshots expose it as `foldReason`, and the SDK `runtime.jobs.list` payload carries it beside `backgrounded`. The manager emits one `onFold` notification per fold, which the SDK host publishes as a replayable `bash_folded` frame.\n- Chord/timer/SDK-control folds wake a fresh turn on completion with a receipt containing bounded output, the original request, and a `job` retrieval handle. A steer fold keeps the current turn running so the steer is consumed immediately; the job's completion still wakes a later turn. The original command deadline remains active and `job tail` is available while the job runs.\n- PTY overlay disposal is non-owning; explicit Escape and `job cancel` stop the PTY through its lifecycle hook. `task` and `subagent` waits are not foldable.\n\nSpawn paths that produce jobs:\n- `packages/coding-agent/src/tools/bash.ts`\n - `async: true` always registers a `type: \"bash\"` job with `AsyncJobManager.register(...)` and returns a start message.\n - auto-background mode (`bash.autoBackground.enabled`) starts the same managed job path for non-PTY commands, waits up to `min(bash.autoBackground.thresholdMs, timeoutMs - 1000)`, and if the command is still running returns a background-job start result instead of inline command output.\n- `packages/coding-agent/src/task/index.ts`\n - when `async.enabled` is on, the chosen agent is not blocking, and `tasks.length > 0`, each task item is registered as a `type: \"task\"` job.\n\nLifecycle and exact state names:\n- Conceptual scheduling path: `pending` (only task-progress bookkeeping before work starts) → `running` → `completed` / `failed`; cancellation changes a running async job to `cancelled`.\n- Exact `AsyncJob.status` values in `packages/coding-agent/src/async/job-manager.ts`: `\"running\" | \"completed\" | \"failed\" | \"cancelled\"`.\n- Exact per-task progress values in `packages/coding-agent/src/task/types.ts`: `\"pending\" | \"running\" | \"completed\" | \"failed\" | \"aborted\"`.\n\n## Side Effects\n- Filesystem\n - None in `job.ts` itself.\n - Jobs being observed may already have written artifacts/results through their own tool runtimes.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Reads and mutates `session.asyncJobManager` state.\n - `watchJobs(...)` / `unwatchJobs(...)` toggle delivery suppression for the watched ids.\n - `acknowledgeDeliveries(...)` marks completed ids as suppressed and removes queued deliveries for them.\n - `cancel(...)` aborts running jobs through each job's `AbortController`.\n- User-visible prompts / interactive UI\n - Polling emits periodic `onUpdate` snapshots every 500 ms.\n - Automatic job completion follow-ups are generated by `packages/coding-agent/src/sdk/session.ts` only for unsuppressed deliveries.\n- Background work / cancellation\n - Waiting uses a timeout plus optional tool-call abort signal.\n - Cancelling a job does not synchronously await teardown; it flips state, aborts, and returns control to the manager/job promise.\n\n## Limits & Caps\n- Poll wait duration comes from `async.pollWaitDuration` in `packages/coding-agent/src/config/settings-schema.ts`:\n - allowed values: `5s`, `10s`, `30s`, `1m`, `5m`\n - default: `30s`\n- Progress update cadence while polling: `PROGRESS_INTERVAL_MS = 500` in `packages/coding-agent/src/tools/job.ts`.\n- Async job retention default: `DEFAULT_RETENTION_MS = 5 * 60 * 1000` in `packages/coding-agent/src/async/job-manager.ts`.\n- Manager fallback max-running limit: `DEFAULT_MAX_RUNNING_JOBS = 15` in `packages/coding-agent/src/async/job-manager.ts`.\n- Session wiring clamps `async.maxJobs` to `1..100` before constructing the manager in `packages/coding-agent/src/sdk/session.ts`; settings default is `100` in `packages/coding-agent/src/config/settings-schema.ts`.\n- Async completion delivery retry backoff in `packages/coding-agent/src/async/job-manager.ts`:\n - base `500` ms\n - max `30_000` ms\n - jitter `< 200` ms\n - exponent capped at 8 doublings\n\n## Errors\n- Tool-disabled path is returned as normal text, not thrown: `Async execution is disabled; no background jobs are available.`\n- Polling a nonexistent id is not an exception:\n - with `poll` only, missing ids are dropped; if none remain the tool returns `No matching jobs found for IDs: ...`.\n - with `cancel`, each missing id is reported as `not_found` in `details.cancelled` and text.\n- Cancelling a non-running job is not an exception; it reports `already_completed` even if the actual status is `completed`, `failed`, or `cancelled`.\n- Tool-call abort during polling stops waiting and returns a final snapshot through `#buildResult(...)`; it does not cancel watched jobs.\n- Failures inside the underlying async work are stored on the job (`status: \"failed\"`, `errorText`) and reported in normal tool output, not rethrown by `job`.\n- Calling `list: true` against an empty manager returns a normal empty-list result rather than throwing; missing ids passed to `poll` are silently filtered.\n\n## Notes\n- `job` waits for the first watched running job to settle, not for all watched jobs. If others remain `running`, they are reported under `## Still Running`; the caller must invoke `job` again to continue waiting.\n- Delivery suppression is the key difference between snapshot and automatic delivery:\n - snapshots (`job` calls with `poll` or `list: true`) read current manager state;\n - follow-up delivery comes from `AsyncJobManager.#enqueueDelivery(...)` and `sdk.ts` `onJobComplete`;\n - watched or acknowledged ids are suppressed via `isDeliverySuppressed(...)`.\n- `manager.cancel(id)` sets `status = \"cancelled\"` before the underlying promise settles. The job function may later populate `resultText` or `errorText`; `job-manager.ts` preserves that text but does not transition the status away from `cancelled`.\n- Retention eviction removes the job record, suppression flags, and watch flag together. After eviction, both `job` calls and `list: true` snapshots behave as if the id never existed.\n",
|
|
128
128
|
"tools/lsp.md": "# lsp\n\n> Query language servers for diagnostics, navigation, symbols, renames, code actions, capabilities, and raw requests.\n\n## Source\n- Entry: `packages/coding-agent/src/lsp/index.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/lsp.md`\n- Key collaborators:\n - `packages/coding-agent/src/lsp/client.ts` — client process lifecycle and JSON-RPC\n - `packages/coding-agent/src/lsp/config.ts` — config loading, auto-detect, server selection\n - `packages/coding-agent/src/lsp/lspmux.ts` — optional `lspmux` command wrapping\n - `packages/coding-agent/src/lsp/edits.ts` — apply `WorkspaceEdit` and text edits\n - `packages/coding-agent/src/lsp/utils.ts` — URI conversion, symbol resolution, formatting, glob expansion\n - `packages/coding-agent/src/lsp/types.ts` — tool schema and protocol types\n - `packages/coding-agent/src/lsp/clients/index.ts` — custom linter client cache/factory\n - `packages/coding-agent/src/lsp/clients/lsp-linter-client.ts` — LSP-backed linter adapter\n - `packages/coding-agent/src/lsp/clients/biome-client.ts` — Biome CLI diagnostics/formatting adapter\n - `packages/coding-agent/src/lsp/clients/swiftlint-client.ts` — SwiftLint CLI diagnostics adapter\n - `packages/coding-agent/src/tools/index.ts` — tool registration and `lsp.enabled` gating\n - `packages/coding-agent/src/tools/tool-timeouts.ts` — timeout defaults and clamping\n - `packages/coding-agent/src/lsp/defaults.json` — built-in server definitions for auto-detect\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `action` | string enum | Yes | One of `diagnostics`, `definition`, `references`, `hover`, `symbols`, `rename`, `rename_file`, `code_actions`, `type_definition`, `implementation`, `status`, `reload`, `capabilities`, `request`. |\n| `file` | string | No | File path; for `diagnostics` also a glob; for supported workspace forms use `\"*\"`; for `rename_file` this is the source path. |\n| `line` | number | No | 1-indexed line number for position-based actions. Defaults to `1` on the single-file action path. |\n| `symbol` | string | No | Substring used to resolve the column on `line`. Supports `name#N` occurrence selectors; `N` is 1-indexed and defaults to `1`. |\n| `query` | string | No | Workspace symbol query, code-action selector/filter, or LSP method name for `action=request`. |\n| `new_name` | string | No | Required for `rename` and `rename_file`. |\n| `apply` | boolean | No | For `rename`/`rename_file`, apply unless explicitly `false`. For `code_actions`, list unless explicitly `true`. |\n| `timeout` | number | No | Seconds, clamped by `clampTimeout(\"lsp\", ...)` to `5..60`, default `20`. |\n| `payload` | string | No | JSON string for `action=request`; overrides auto-built params. |\n\n## Outputs\n- Single-shot `AgentToolResult`.\n- `content` is always one text block: `[{ type: \"text\", text: string }]`.\n- `details` is `LspToolDetails`: `action`, `success`, optional `serverName`, optional original `request`.\n- No streaming updates.\n- No artifact URIs or background jobs.\n- Many validation failures are returned as ordinary text results with `details.success: false`; aborts throw `ToolAbortError` instead.\n\n## Flow\n1. `packages/coding-agent/src/tools/index.ts` registers `lsp: LspTool.createIf`; session creation also gates it behind `session.enableLsp !== false` and `settings.get(\"lsp.enabled\")`.\n2. `LspTool.execute()` in `packages/coding-agent/src/lsp/index.ts` clamps `timeout` with `clampTimeout(\"lsp\", ...)`, builds an `AbortSignal.timeout(...)`, and combines it with the caller signal.\n3. `getConfig()` loads and caches `LspConfig` per cwd, applies idle-timeout config via `setIdleTimeout()`, and reuses the cached config on later calls.\n4. Config loading in `packages/coding-agent/src/lsp/config.ts` merges `defaults.json` with JSON/YAML overrides. Project-controlled configuration may control declarative matching, activation, and capabilities, but cannot define launch fields, initialization options, or opaque server settings. Canonical trusted user configuration outside the project may retain those process-affecting fields. The loader can also preserve them from preloaded trusted external plugin roots outside the project, but no current production CLI/startup path supplies those roots; project-controlled plugin roots remain untrusted, and the quarantined `--plugin-dir` surface does not grant launch authority. With no overrides, auto-detection intersects root markers with trusted external executable discovery and rejects repository-owned lexical paths as well as symlink-resolved project binaries.\n5. Server routing uses `getServersForFile()` / `getServerForFile()` from `config.ts`: extension or basename match, then sort primary servers before linters. `index.ts` further filters custom linter clients out of navigation/refactor paths with `getLspServersForFile()` / `getLspServerForFile()`.\n6. `getOrCreateClient()` in `client.ts` creates one process per trusted `command:cwd` launch definition, optionally wraps supported commands with `lspmux`, spawns the server, starts the background message reader, sends `initialize`, stores server capabilities, then sends `initialized`.\n7. The message reader in `client.ts` parses LSP frames, resolves pending requests, caches `publishDiagnostics`, tracks `$/progress` tokens for project-load completion, answers `workspace/configuration`, and applies `workspace/applyEdit` requests through `applyWorkspaceEdit()`.\n8. File-scoped actions call `ensureFileOpen()` before requests. Column resolution uses `resolveSymbolColumn()` from `utils.ts`: read the target file, pick first non-whitespace when `symbol` is omitted, otherwise find the exact or case-insensitive match on the target line and honor `#N` occurrence selectors.\n9. Actions dispatch in `LspTool.execute()` through dedicated branches: workspace-only branches (`status`, the rejected workspace-diagnostics form, workspace `symbols`, workspace `reload`, `capabilities`, `request`) run before the single-file switch; all other single-file actions share one client lookup and `switch(action)`.\n10. Requests go through `sendRequest()` in `client.ts`, which allocates an incrementing JSON-RPC id, installs abort and timeout handling, sends `$/cancelRequest` on abort, and rejects on timeout or process exit.\n11. Actions that return edits either preview with `formatWorkspaceEdit()` or apply with `applyWorkspaceEdit()` from `edits.ts`; `rename_file` also performs the filesystem rename and then sends `workspace/didRenameFiles`.\n12. Non-abort failures inside the single-file action block are converted to `LSP error: ...`; many precondition failures return explicit text without throwing.\n\n## Modes / Variants\n### Routing and workspace scope\n- `file: \"*\"` is special for `diagnostics`, `symbols`, and `reload`; diagnostics rejects it without launching a subprocess, while symbols and reload retain workspace behavior.\n- `status` ignores `file`.\n- `capabilities` with omitted `file` or `\"*\"` inspects all non-custom LSP servers; with a concrete file it scopes to matching non-custom servers.\n- `request` with omitted `file` or `\"*\"` chooses the first available non-custom LSP server; with a concrete file it chooses that file's primary non-linter server.\n- `rename_file` sends `workspace/willRenameFiles` and `workspace/didRenameFiles` to every non-custom LSP server from `getLspServers(config)`, not just one file-scoped server.\n- Diagnostics are the only tool action that queries both normal LSP servers and custom linter clients (`BiomeClient`, `SwiftLintClient`, or `LspLinterClient`).\n\n### `diagnostics`\n**Inputs**\n- Required: a concrete file or glob.\n- Optional: `timeout`.\n\n**Execution**\n- `file: \"*\"`: returns `success: false` with guidance to use a concrete file/glob for LSP diagnostics and an execution-authorized tool for build or typecheck commands. It does not launch a subprocess.\n- Concrete file or glob: `resolveDiagnosticTargets()` treats non-globs as one target, otherwise expands a `Bun.Glob` up to `MAX_GLOB_DIAGNOSTIC_TARGETS`.\n- Per file, every matching server runs: custom clients call `lint(file)`; real LSP servers optionally wait for project load, capture `diagnosticsVersion`, `refreshFile()`, then `waitForDiagnostics()` for fresh `publishDiagnostics`.\n- Results are deduplicated by range+message and severity-sorted.\n\n**Output text**\n- Single target with no issues: `OK`.\n- Single target with issues: `<summary>:\\n<grouped diagnostics>`.\n- Batch/glob target: one section per file, plus an initial truncation warning when the glob exceeds the file cap.\n- `file: \"*\"`: `Workspace build diagnostics are unavailable via lsp...` with `details.success: false`.\n\n### `definition`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `timeout`.\n\n**Execution**\n- Sends `textDocument/definition` with `{ textDocument, position }`.\n- Accepts `Location`, `Location[]`, `LocationLink`, or `LocationLink[]`; `normalizeLocationResult()` converts `LocationLink` to `targetSelectionRange ?? targetRange`.\n- Waits for project load before the request.\n\n**Output text**\n- `No definition found` or `Found N definition(s):` followed by `file:line:col` and one context line above/below each location.\n\n### `type_definition`\nSame as `definition`, but sends `textDocument/typeDefinition` and reports `type definition(s)`.\n\n### `implementation`\nSame as `definition`, but sends `textDocument/implementation` and reports `implementation(s)`.\n\n### `references`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `timeout`.\n\n**Execution**\n- Sends `textDocument/references` with `includeDeclaration: true`.\n- For project-aware servers, retries up to `REFERENCES_RETRY_COUNT` times when the only hit is the queried declaration; between retries it waits for project load and sleeps `REFERENCES_RETRY_DELAY_MS`.\n- First `REFERENCE_CONTEXT_LIMIT` references include surrounding context; the rest are location-only.\n\n**Output text**\n- `No references found` or `Found N reference(s):` with contextual entries first, then `... M additional reference(s) shown without context` when truncated.\n\n### `hover`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `timeout`.\n\n**Execution**\n- Sends `textDocument/hover`.\n- `extractHoverText()` flattens strings, markup content, marked-string objects, or arrays into plain text.\n\n**Output text**\n- `No hover information` or the extracted hover text.\n\n### `symbols`\n**Inputs**\n- Workspace mode: `file: \"*\"` or omitted file on the early workspace branch, plus required `query`.\n- Document mode: required `file`.\n- Optional: `timeout`.\n\n**Execution**\n- Workspace mode sends `workspace/symbol` to every non-custom LSP server, post-filters matches with `filterWorkspaceSymbols()`, deduplicates with `dedupeWorkspaceSymbols()`, then truncates to `WORKSPACE_SYMBOL_LIMIT`.\n- Document mode sends `textDocument/documentSymbol` to the primary server. If the first item has `selectionRange`, it formats hierarchical `DocumentSymbol`s; otherwise it formats flat `SymbolInformation`s.\n\n**Output text**\n- Workspace mode: `Found N symbol(s) matching \"query\":` plus formatted `name @ file:line:col`, with an omission line when over the limit.\n- Document mode: `Symbols in <file>:` plus hierarchical or flat symbol lines.\n\n### `rename`\n**Inputs**\n- Required: `file`, `new_name`.\n- Optional: `line`, `symbol`, `apply`, `timeout`.\n\n**Execution**\n- Waits for project load, sends `textDocument/rename`, receives a `WorkspaceEdit`.\n- `apply !== false` applies edits immediately with `applyWorkspaceEdit()`.\n- `apply === false` renders a preview with `formatWorkspaceEdit()`.\n\n**Output text**\n- `Rename returned no edits`, `Applied rename:` plus applied change lines, or `Rename preview:` plus summarized edits.\n\n### `rename_file`\n**Inputs**\n- Required: `file` source path, `new_name` destination path.\n- Optional: `apply`, `timeout`.\n\n**Execution**\n- Resolves absolute source and destination, rejects identical paths, missing source, existing destination, empty rename set, or directories with more than `MAX_RENAME_PAIRS` files.\n- `enumerateRenamePairs()` returns one `{oldUri,newUri}` pair for a file or walks every regular file in a directory tree.\n- Sends `workspace/willRenameFiles` with `{ files: pairs }` to every non-custom LSP server; collects returned `WorkspaceEdit`s and server notes.\n- Preview mode (`apply === false`) only formats those edits.\n- Apply mode runs each returned `WorkspaceEdit`, renames the source path on disk, sends `textDocument/didClose` for every renamed open file, deletes those `openFiles` entries, then sends `workspace/didRenameFiles`.\n\n**Output text**\n- Preview: `Rename preview: <file-count label> → <dest>` plus per-server edit summaries and optional server notes.\n- Apply: `Renamed <file-count label> → <dest>` plus applied edit summaries, filesystem rename line, and optional server notes.\n\n### `code_actions`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `query`, `apply`, `timeout`.\n\n**Execution**\n- Reads cached diagnostics for the open URI from `client.diagnostics` and sends `textDocument/codeAction` for a zero-width range at the resolved position.\n- When `apply !== true`, `query` is passed as `context.only: [query]`; this is a server-side kind filter.\n- When `apply === true`, `query` becomes a required client-side selector: either a zero-based numeric index or a case-insensitive substring of the action title.\n- Applying a `CodeAction` uses `applyCodeAction()`: optionally `codeAction/resolve`, then `applyWorkspaceEdit(edit)`, then optional `workspace/executeCommand`.\n- Applying a bare `Command` only runs `workspace/executeCommand`.\n\n**Output text**\n- List mode: `N code action(s):` plus `index: [kind] title` lines.\n- Apply mode success: `Applied \"title\":` plus `Workspace edit:` and/or `Executed command(s):` sections.\n- Apply mode miss: `No code action matches \"query\". Available actions:`.\n- Apply mode with no edit/command: `Action \"title\" has no workspace edit or command to apply`.\n\n### `status`\n**Inputs**\n- None.\n\n**Execution**\n- Reads configured servers from cached `LspConfig`, not `getActiveClients()`.\n- Calls `detectLspmux(session.cwd)` and appends status text when a trusted `lspmux` is installed for the session trust root.\n\n**Output text**\n- `Active language servers: ...` or `No language servers configured for this project`, optionally followed by `lspmux: active (multiplexing enabled)` or `lspmux: installed but server not running`.\n\n### `reload`\n**Inputs**\n- Workspace mode: `file: \"*\"` or omitted `file`.\n- Single-file mode: required `file`.\n- Optional: `timeout`.\n\n**Execution**\n- Workspace mode reloads every non-custom LSP server.\n- Single-file mode reloads the primary server for that file.\n- `reloadServer()` tries `rust-analyzer/reloadWorkspace`, then `workspace/didChangeConfiguration` with `{ settings: {} }`; if neither works it kills the process so the next request cold-starts a new client.\n\n**Output text**\n- One line per server: `Reloaded <server>`, `Restarted <server>`, or `Failed to reload <server>: ...`.\n\n### `capabilities`\n**Inputs**\n- Optional: `file`, `timeout`.\n\n**Execution**\n- With a concrete `file`, inspects matching non-custom servers for that file.\n- With omitted `file` or `\"*\"`, inspects every non-custom configured server.\n- Starts servers as needed and dumps `client.serverCapabilities ?? {}` as pretty JSON.\n\n**Output text**\n- Per server: `<server>:` followed by indented `capabilities: { ... }`, or `<server>: failed to start (...)`.\n\n### `request`\n**Inputs**\n- Required: `query` method name.\n- Optional: `file`, `line`, `symbol`, `payload`, `timeout`.\n\n**Execution**\n- Chooses one non-custom server: file-scoped primary server, otherwise the first configured non-custom server.\n- Param building precedence:\n 1. If `payload` is present, parse JSON and use it verbatim.\n 2. Else if `file` is concrete and `line` is present, build `{ textDocument: { uri }, position: { line: line - 1, character } }` using `resolveSymbolColumn()`.\n 3. Else if `file` is concrete, build `{ textDocument: { uri } }`.\n 4. Else use `{}`.\n- Opens the file first when `file` is concrete.\n\n**Output text**\n- Success: `<server> ← <method>:\\n<formatted result>`, where non-string results are `JSON.stringify(..., null, 2)` and nullish values become `null`.\n- Failure: `LSP error from <server> on <method>: ...`.\n\n## Side Effects\n- Filesystem\n - Reads config files, target files, and root markers.\n - `rename` and `code_actions` may edit/create/delete/rename files via `applyWorkspaceEdit()`.\n - `rename_file` always renames the source path on disk in apply mode.\n - Server-initiated `workspace/applyEdit` requests also mutate files through `applyWorkspaceEdit()`.\n- Network\n - None directly; communication is local stdio JSON-RPC to subprocesses.\n- Subprocesses / native bindings\n - Spawns language servers with `ptree.spawn()`.\n - `BiomeClient` and `SwiftLintClient` spawn CLI tools.\n - Optional lspmux detection uses the trusted external `lspmux` executable; supported servers may be wrapped through `lspmux client`.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Caches config per cwd in `configCache`.\n - Caches LSP clients per `command:cwd`, with `pendingRequests`, `diagnostics`, `openFiles`, `serverCapabilities`, and project-load state.\n - Caches custom linter clients by `serverName:cwd`.\n - Updates client `lastActivity`; optional idle-timeout cleanup is driven by `setIdleTimeout()`.\n- Background work / cancellation\n - Every request has an abortable timeout signal.\n - Aborting an in-flight LSP request sends `$/cancelRequest`.\n - Background message readers persist for each live client until process exit/shutdown.\n\n## Limits & Caps\n- Tool timeout clamp: default `20`, min `5`, max `60` seconds — `TOOL_TIMEOUTS.lsp` in `packages/coding-agent/src/tools/tool-timeouts.ts`.\n- LSP request default timeout inside `sendRequest()`: `30_000ms` — `DEFAULT_REQUEST_TIMEOUT_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Warmup initialize timeout default: `5_000ms` — `WARMUP_TIMEOUT_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Project-load wait fallback: `15_000ms` — `PROJECT_LOAD_TIMEOUT_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Idle-client sweep interval when enabled: `60_000ms` — `IDLE_CHECK_INTERVAL_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Diagnostic message output cap: first `50` messages — `DIAGNOSTIC_MESSAGE_LIMIT` in `packages/coding-agent/src/lsp/index.ts`.\n- Single-file diagnostics wait: `3_000ms` — `SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS`.\n- Batch/glob diagnostics wait per file: `400ms` — `BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS`.\n- Glob diagnostic target cap: first `20` matches — `MAX_GLOB_DIAGNOSTIC_TARGETS`.\n- Workspace symbol cap: first `200` entries — `WORKSPACE_SYMBOL_LIMIT`.\n- Reference context cap: first `50` references include source context — `REFERENCE_CONTEXT_LIMIT`.\n- References retry count: `2` retries, `250ms` backoff — `REFERENCES_RETRY_COUNT`, `REFERENCES_RETRY_DELAY_MS`.\n- Directory rename cap: `1_000` file pairs — `MAX_RENAME_PAIRS`.\n- `detectLspmux()` state cache TTL: `5 * 60 * 1000ms`; liveness check timeout: `1_000ms` — `STATE_CACHE_TTL_MS`, `LIVENESS_TIMEOUT_MS` in `packages/coding-agent/src/lsp/lspmux.ts`.\n\n## Errors\n- Missing or invalid inputs are usually returned as text with `details.success: false`, not thrown:\n - missing `file`/`query`/`new_name`\n - invalid JSON in `payload`\n - no matching server\n - invalid `rename_file` source/destination conditions\n- `resolveSymbolColumn()` throws explicit errors for missing files, missing symbols, and out-of-bounds `#N` selectors; these surface as `LSP error: ...` or request-specific error text.\n- `sendRequest()` rejects on timeout with `LSP request <method> timed out after <ms>ms`.\n- Client process exit rejects all pending requests with an exit-code/stderr error assembled in `getOrCreateClient()`.\n- Single-file action failures inside the main `try` become `LSP error: <message>`.\n- `request` has its own error envelope: `LSP error from <server> on <method>: <message>`.\n- Some server failures are intentionally softened:\n - diagnostics continue when one server fails\n - `rename_file` suppresses `workspace/willRenameFiles` “method not found” errors and records other server errors as notes\n - `code_actions` ignores `codeAction/resolve` failures and applies unresolved actions when possible\n- Aborts are not converted to text: `ToolAbortError` is rethrown.\n\n## Notes\n- `status` reports configured/available servers from `LspConfig`, not currently active client processes from `getActiveClients()`.\n- `getLspServerForFile()` excludes `createClient` adapters and linter-only servers; navigation/refactor actions never target Biome/SwiftLint custom clients.\n- `getServersForFile()` matches both file extensions and exact basenames from `fileTypes`; config can target names like `Dockerfile` if present.\n- `symbol` matching is exact first, then case-insensitive, and falls back to the Nth occurrence on the specified line only; it never scans other lines.\n- `code_actions` uses `query` in two different ways: server-side `context.only` filter in list mode, client-side title/index selector in apply mode.\n- `rename` and `rename_file` default to apply. Preview requires `apply: false`.\n- `request` with `file: \"*\"` is treated the same as omitted `file`: it does not build workspace-specific params.\n- `reload` does not recreate a client immediately after killing it; the next request triggers reinitialization.\n- `workspace/applyEdit` can apply edits initiated by the server outside the direct tool action result path.\n- `GJC_DISABLE_LSPMUX=1` is the canonical opt-out; `PI_DISABLE_LSPMUX=1` is a supported compatibility alias. A truthy value for either variable disables lspmux probing and wrapping. Only `rust-analyzer` is in `DEFAULT_SUPPORTED_SERVERS`.\n- Startup LSP warmup (`discoverStartupLspServers(cwd)` in `sdk.ts`) is gated on `enableLsp && options.hasUI && settings.get(\"lsp.diagnosticsOnWrite\")` — print, ACP, and script sessions skip it and let `getOrCreateClient()` cold-start servers on demand. See `docs/sdk-embedding.md` § Startup performance.\n- `configCache` is per-process and never auto-invalidated; config changes require a fresh process to be observed by `getConfig()` callers.\n",
|
|
129
129
|
"tools/monitor.md": "# monitor\n\n> Start a background monitor that streams stdout lines as task notifications. Mirrors Claude Code's `Monitor` tool surface.\n\n## Source\n\n- Entry: `packages/coding-agent/src/tools/monitor.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/monitor.md`\n- Key collaborators:\n - `packages/coding-agent/src/async/job-manager.ts` — stores the captured stream via `appendOutput`/`readOutputSince` and delivers the final background-job result.\n - `packages/coding-agent/src/exec/bash-executor.ts` and `packages/coding-agent/src/session/streaming-output.ts` — provide the unthrottled `onRawChunk` capture hook that feeds the manager.\n - `packages/coding-agent/src/tools/bash.ts` — exposes `BashTool.startMonitorJob(...)`, a Bash-aligned helper that preserves interception, cwd/env expansion, artifacts, timeouts, and raw stream capture.\n - `packages/coding-agent/src/tools/job.ts` — polls/cancels the monitor task by id; there is no sibling `MonitorKill` tool.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `command` | `string` | Yes | Shell command to run as a background monitor. Each sanitized stdout line is captured; persistent notifications are coalesced before delivery. |\n| `kind` | `\"log\" \\| \"poll\" \\| \"watch\" \\| \"other\"` | Yes | Category of monitor. Surfaces in listings. |\n| `description` | `string` | Yes | Short human-readable summary of what is being monitored. |\n| `timeout` | `number` | No | Maximum wall-clock seconds the monitor may run before automatic shutdown. Omit for session lifetime. |\n| `persistent` | `boolean` | No | Keep the monitor running after its first emitted event. Persistent monitors survive until session end, timeout, or explicit cancel via `job`. Defaults to `false`. |\n\n## Outputs\n\nThe tool returns one text block plus `details`:\n\n- `content[0].text`: `Monitor started · task <task_id> · persistent: true|false`.\n- `details`: `{ taskId, kind, description, command, persistent }`.\n\nEach newline-terminated stdout line is appended to the manager-owned cursor. Persistent monitors debounce notification delivery and send the latest line with a count of coalesced earlier lines; ordinary log/poll traffic therefore does not create one model turn per event-loop tick. Use `job` with the returned `taskId` to inspect completion state or terminate the monitor.\n\n## Behavior / Lifecycle\n\n1. `MonitorTool.createIf(session)` gates the tool on `isBackgroundJobSupportEnabled(session.settings)` — identical to `JobTool`'s gate.\n2. `execute(...)` delegates to `BashTool.startMonitorJob(...)`, so Monitor inherits Bash's interception rules, cwd normalization, internal URL expansion, environment construction, artifact allocation, timeout clamping, and unthrottled raw capture.\n3. The helper mirrors every sanitized raw chunk to `manager.appendOutput(jobId, chunk)` and line-buffers the stream. Persistent notifications are latest-biased and coalesced over a short debounce window; terminal completion flushes the newest pending line.\n4. Non-persistent monitors auto-cancel after delivering their first stdout-line notification. Persistent monitors terminate when the underlying command exits, `timeout` elapses, the calling agent is torn down, or the user cancels the returned background task via `job`. Cancellation and eviction purge pending persistent notifications rather than delivering stale output.\n\n## Errors\n\n- `ToolError`: `Async execution is disabled; the monitor tool is unavailable in this session.` — emitted when `AsyncJobManager.instance()` returns `undefined`.\n- Invalid parameter shapes are rejected by zod with the project's standard validation error path.\n\n## Examples\n\nTail an error log and react when lines appear:\n\n```jsonc\n{\n \"command\": \"tail -F /var/log/app.log | grep -i error\",\n \"kind\": \"log\",\n \"description\": \"Tail app.log for errors\"\n}\n```\n\nPoll CI status until the build completes (15-minute timeout):\n\n```jsonc\n{\n \"command\": \"while true; do gh run view --json status,conclusion --jq .status; sleep 30; done\",\n \"kind\": \"poll\",\n \"description\": \"Watch CI build status\",\n \"timeout\": 900,\n \"persistent\": false\n}\n```\n\n## Parity oracle\n\nThe schema and behavior captured in this doc are pinned by the fixture at\n`packages/coding-agent/test/fixtures/claude-code-tools/monitor.schema.json`,\ncaptured from the upstream Claude Code CLI (`claude --version 2.1.152`). Any\ndeviation from that fixture is a parity bug.\n",
|
|
130
130
|
"tools/python.md": "# python\n\n> Execute Python in a persistent session-owned REPL with an append-only JSONL transcript.\n\n> **Notice:** this tool is available without an active `autoresearch` mission. For multi-language or one-off code execution use [`eval`](./eval.md); both tools use the same Python execution stack but retain separate kernels.\n\n## Source\n\n- Entry: `packages/coding-agent/src/tools/python.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/python.md`\n- Registration: `packages/coding-agent/src/tools/descriptors.ts` (`loadMode: \"discoverable\"`)\n- Key collaborators:\n - `packages/coding-agent/src/gjc-runtime/python-transcript.ts` — runtime-owned append-only JSONL transcript recording\n - `packages/coding-agent/src/gjc-runtime/session-layout.ts` — session-rooted transcript and artifact paths\n - `packages/coding-agent/src/eval/py/executor.ts` — kernel session retention and `disposeKernelSessionsByOwner`\n - `docs/python-repl.md` — Python kernel/gateway internals\n\n## Availability\n\nRegistered at construction as a **discoverable** builtin with `defaultInactive: true`. It is therefore present in the registry but **not** in the active tool set for a normal session, and is activated on demand through the tool-discovery path (`search_tool_bm25` → `activateDiscoveredTools`).\n\nActivation replaces the active set, so callers must pass the full merged list of tool names. `setActiveToolsByName` silently drops names it does not recognize, which is why the tool is registered at construction rather than injected later.\n\n## Inputs\n\n| Field | Type | Notes |\n| :--- | :--- | :--- |\n| `action` | `\"execute\" \\| \"clear\"` | Optional; defaults to `\"execute\"`. |\n| `code` | `string` | Required for `execute`; ignored for `clear`. |\n\nThere is deliberately **no** separate teardown tool. Clearing the kernel is an action on this same tool.\n\n## Session resolution\n\nEvery call resolves the current GJC session id. `execute` and `clear` return an actionable error when no session id is available, because the kernel owner and session-rooted paths cannot be derived. No active `autoresearch` mission is required.\n\nThe REPL runs in the session cwd and retains variables, imports, and loaded data across `execute` calls for that session.\n\n## Kernel ownership and teardown\n\nThe kernel owner id is `python:<session-id>`, deliberately distinct from the `eval` owner so the two never alias and cleanup remains scoped to the owning session.\n\nThe session's kernel is disposed on:\n\n- the `clear` action,\n- session cleanup, including a session identity transition,\n- signal exit (Ctrl-C).\n\n`gjc autoresearch clear` clears autoresearch state only; it does not dispose a Python kernel.\n\n## Transcript recording\n\nThe runtime records one JSONL entry per `execute` at `.gjc/_session-{sessionid}/ipykernels/{datetime}-{kernelid}/transcript.jsonl`, containing `timestamp`, `code`, `output`, `exitCode`, `cancelled`, and `truncated`. `{kernelid}` is the executor identity captured when the kernel is acquired, so a real kernel replacement uses a new transcript directory. `clear` records no entry.\n\nDisplay artifacts use the stable session-rooted directory `.gjc/_session-{sessionid}/ipykernels/artifacts`, rather than a per-kernel transcript directory.\n\n## Related\n\n- [`eval`](./eval.md) — session-scoped Python/JavaScript execution\n- `docs/python-repl.md` — kernel lifecycle, wire protocol, output capture\n",
|