@sayknow-cli/coding-agent 0.6.3 → 0.6.5

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +2 -2
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/settings-schema.d.ts +7 -3
  5. package/dist/types/i18n/messages/en.d.ts +16 -0
  6. package/dist/types/modes/components/session-selector.d.ts +7 -3
  7. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  8. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  9. package/dist/types/modes/components/user-message.d.ts +3 -1
  10. package/dist/types/modes/components/welcome.d.ts +49 -6
  11. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  12. package/dist/types/modes/controllers/selector-controller.d.ts +5 -0
  13. package/dist/types/modes/interactive-mode.d.ts +7 -1
  14. package/dist/types/modes/theme/defaults/index.d.ts +342 -0
  15. package/dist/types/modes/theme/theme.d.ts +5 -1
  16. package/dist/types/modes/types.d.ts +1 -1
  17. package/dist/types/session/session-manager.d.ts +9 -0
  18. package/dist/types/session/session-stars.d.ts +15 -0
  19. package/dist/types/tui/output-block.d.ts +12 -1
  20. package/dist/types/tui/utils.d.ts +1 -3
  21. package/dist/types/utils/session-title-generation.d.ts +6 -0
  22. package/dist/types/utils/title-generator.d.ts +11 -2
  23. package/package.json +7 -7
  24. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  25. package/src/cli/session-picker.ts +4 -0
  26. package/src/config/keybindings.ts +1 -1
  27. package/src/config/settings-schema.ts +4 -3
  28. package/src/i18n/messages/de.messages.ts +4 -1
  29. package/src/i18n/messages/de.settings.ts +2 -0
  30. package/src/i18n/messages/de.ts +16 -0
  31. package/src/i18n/messages/en.ts +16 -0
  32. package/src/i18n/messages/es.messages.ts +4 -1
  33. package/src/i18n/messages/es.settings.ts +2 -0
  34. package/src/i18n/messages/es.ts +16 -0
  35. package/src/i18n/messages/fr.messages.ts +4 -1
  36. package/src/i18n/messages/fr.settings.ts +2 -0
  37. package/src/i18n/messages/fr.ts +16 -0
  38. package/src/i18n/messages/ja.messages.ts +4 -1
  39. package/src/i18n/messages/ja.settings.ts +2 -0
  40. package/src/i18n/messages/ja.ts +16 -0
  41. package/src/i18n/messages/ko.messages.ts +4 -1
  42. package/src/i18n/messages/ko.settings.ts +2 -0
  43. package/src/i18n/messages/ko.ts +16 -0
  44. package/src/i18n/messages/zh.messages.ts +2 -1
  45. package/src/i18n/messages/zh.settings.ts +2 -0
  46. package/src/i18n/messages/zh.ts +16 -0
  47. package/src/internal-urls/docs-index.generated.ts +10 -10
  48. package/src/lsp/render.ts +0 -1
  49. package/src/modes/components/session-selector.ts +71 -21
  50. package/src/modes/components/sessions-dashboard.ts +8 -5
  51. package/src/modes/components/settings-selector.ts +1 -0
  52. package/src/modes/components/status-line/presets.ts +5 -5
  53. package/src/modes/components/status-line/separators.ts +4 -0
  54. package/src/modes/components/tool-execution.ts +6 -19
  55. package/src/modes/components/tool-status-header.ts +103 -12
  56. package/src/modes/components/user-message-selector.ts +57 -15
  57. package/src/modes/components/user-message.ts +25 -15
  58. package/src/modes/components/welcome.ts +374 -478
  59. package/src/modes/controllers/command-controller.ts +56 -7
  60. package/src/modes/controllers/input-controller.ts +4 -0
  61. package/src/modes/controllers/selector-controller.ts +91 -10
  62. package/src/modes/interactive-mode.ts +101 -12
  63. package/src/modes/theme/defaults/glow-octopus.json +114 -0
  64. package/src/modes/theme/defaults/index.ts +6 -0
  65. package/src/modes/theme/defaults/ink-octopus.json +114 -0
  66. package/src/modes/theme/defaults/violet-octopus.json +114 -0
  67. package/src/modes/theme/theme.ts +18 -2
  68. package/src/modes/types.ts +1 -1
  69. package/src/prompts/system/title-system.md +1 -1
  70. package/src/sdk/protocol/operation-inventory.generated.json +40 -0
  71. package/src/session/agent-session.ts +12 -0
  72. package/src/session/internal/managed-session-scope.ts +10 -0
  73. package/src/session/session-manager.ts +26 -1
  74. package/src/session/session-stars.ts +104 -0
  75. package/src/slash-commands/builtin-registry.ts +104 -4
  76. package/src/tools/debug.ts +0 -1
  77. package/src/tools/fetch.ts +0 -1
  78. package/src/tui/output-block.ts +29 -61
  79. package/src/tui/utils.ts +1 -8
  80. package/src/utils/session-title-generation.ts +29 -0
  81. package/src/utils/title-generator.ts +46 -2
@@ -35,7 +35,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
35
35
  "handoff-generation-pipeline.md": "# `/handoff` generation pipeline\n\nThis document describes how the coding-agent implements `/handoff`: trigger path, oneshot generation, session switch, context reinjection, persistence, and UI behavior.\n\n## Scope\n\nCovers:\n\n- Interactive `/handoff` command dispatch\n- `AgentSession.handoff()` lifecycle and state transitions\n- `generateHandoff(...)` request shape\n- How old/new sessions persist handoff data differently\n- UI behavior for success, cancel, and failure\n\nDoes not cover:\n\n- Generic tree navigation/branch internals\n- Non-handoff session commands (`/new`, `/fork`, `/resume`)\n\n## Implementation files\n\n- [`../src/modes/controllers/input-controller.ts`](../packages/coding-agent/src/modes/controllers/input-controller.ts)\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- [`packages/agent/src/compaction/compaction.ts`](../packages/agent/src/compaction/compaction.ts)\n- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)\n- [`../src/extensibility/slash-commands.ts`](../packages/coding-agent/src/extensibility/slash-commands.ts)\n\n## Trigger path\n\n1. `/handoff` is declared in builtin slash command metadata (`slash-commands.ts`) with optional inline hint: `[focus instructions]`.\n2. In interactive input handling (`InputController`), submit text matching `/handoff` or `/handoff ...` is intercepted before normal prompt submission.\n3. The editor is cleared and `handleHandoffCommand(customInstructions?)` is called.\n4. `CommandController.handleHandoffCommand` performs a preflight guard using current entries:\n - Counts `type === \"message\"` entries.\n - If `< 2`, it warns: `Nothing to hand off (no messages yet)` and returns.\n\nThe same minimum-content guard exists again inside `AgentSession.handoff()` and throws if violated. This duplicates safety at both UI and session layers.\n\n## End-to-end lifecycle\n\n### 1) Start handoff generation\n\n`AgentSession.handoff(customInstructions?)`:\n\n- Reads current branch entries (`sessionManager.getBranch()`).\n- Validates minimum message count (`>= 2`).\n- Creates `#handoffAbortController` and links any caller-provided abort signal to it.\n- Resolves the current model API key through `ModelRegistry`.\n- Calls `generateHandoff(...)` with:\n - live agent messages (`agent.state.messages`),\n - the current model and API key,\n - the base system prompt (`#baseSystemPrompt`),\n - the live tool array (`agent.state.tools`),\n - optional focus instructions,\n - coding-agent message conversion (`convertToLlm`),\n - provider metadata and `initiatorOverride: \"agent\"`.\n\n`generateHandoff(...)` lives in `packages/agent/src/compaction/compaction.ts` next to summarization. It renders `packages/agent/src/compaction/prompts/handoff-document.md` via `renderHandoffPrompt(...)` with optional `additionalFocus`.\n\n### 2) Generate and capture output\n\n`generateHandoff(...)` converts the existing `AgentMessage[]` history to real LLM `Message[]` history, then appends one trailing agent-attributed `user` message containing the rendered handoff prompt.\n\nThe request uses `completeSimple(...)` directly:\n\n```ts\nawait completeSimple(\n model,\n {\n systemPrompt,\n messages: requestMessages,\n tools,\n },\n {\n apiKey,\n signal,\n reasoning: Effort.High,\n toolChoice: \"none\",\n initiatorOverride,\n metadata,\n },\n);\n```\n\nImportant generation properties:\n\n- The request preserves the live provider cache prefix by reusing the same system prompt, tool definitions, and real message history shape as the active agent.\n- The handoff instruction is a trailing `user` message, not a developer message, so the cached prefix remains aligned with the prior turn.\n- `toolChoice: \"none\"` prevents intentional tool dispatch.\n- The returned assistant content is filtered to text blocks and joined with `\\n`; stray tool-call blocks are ignored if a provider does not honor `toolChoice: \"none\"`.\n- `stopReason === \"error\"` throws a generation error.\n\nNo agent-loop events are used for capture. The handoff path no longer waits for `agent_end` and no longer scans the latest assistant message.\n\n### 3) Cancellation checks\n\nCancellation throws `Error(\"Handoff cancelled\")`; a completed generation with no text returns `undefined`.\n\n- caller signal aborts `#handoffAbortController`\n- `completeSimple(...)` receives the abort signal\n- aborted handoff signal or provider `AbortError` is normalized to `Error(\"Handoff cancelled\")`\n- empty generated text returns `undefined`\n\n`AgentSession.handoff()` always clears `#handoffAbortController` in `finally`.\n\n### 4) New session creation\n\nIf text was generated and not aborted:\n\n1. Flush current session writer (`sessionManager.flush()`).\n2. Cancel session-owned async jobs.\n3. Start a brand-new session with `parentSession` pointing at the previous session file when one exists.\n4. Reset in-memory agent state (`agent.reset()`).\n5. Rebind `agent.sessionId` to the new session id.\n6. Rekey/reset hindsight state for the new session.\n7. Clear queued context arrays (`#steeringMessages`, `#followUpMessages`, `#pendingNextTurnMessages`) and any scheduled hidden next-turn generation.\n8. Reset todo reminder counter.\n\n### 5) Handoff-context injection\n\nThe generated handoff document is wrapped by coding-agent session glue and appended to the new session as a `custom_message` entry:\n\n```text\n<handoff-context>\n...handoff text...\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.\n```\n\nInsertion call:\n\n```ts\nthis.sessionManager.appendCustomMessageEntry(\"handoff\", handoffContent, true, undefined, \"agent\");\n```\n\nSemantics:\n\n- `customType`: `\"handoff\"`\n- `display`: `true` (visible in TUI rebuild)\n- attribution: `\"agent\"`\n- Entry type: `custom_message` (participates in LLM context)\n\n### 6) Rebuild active agent context\n\nAfter injection:\n\n1. `buildDisplaySessionContext()` resolves message list for current leaf.\n2. `agent.replaceMessages(sessionContext.messages)` makes the injected handoff message active context.\n3. Todo phases are synchronized from the new branch.\n4. Method returns `{ document: handoffText, savedPath? }`.\n\nAt this point, the active LLM context in the new session contains the injected handoff message, not the old transcript.\n\n## Persistence model: old session vs new session\n\n### Old session\n\nHandoff generation is a oneshot request, not a visible agent turn. The generated handoff text is not appended to the old session as an assistant message.\n\nResult: the original session keeps its prior transcript unchanged except for data already persisted before handoff began.\n\n### New session\n\nAfter session reset, handoff is persisted as `custom_message` with `customType: \"handoff\"`.\n\n`buildSessionContext()` converts this entry into a runtime custom/user-context message via `createCustomMessage(...)`, so it is included in future prompts from the new session.\n\nAuto-triggered handoffs can additionally save the handoff document as a session artifact when `compaction.handoffSaveToDisk` is enabled; `handoff()` returns its resolvable `artifact://<id>` URI as `savedPath`. Manual `/handoff` does not save an artifact.\n\n## Controller/UI behavior\n\n`CommandController.handleHandoffCommand` behavior:\n\n- Shows a status loader: `Generating handoff… (esc to cancel)`.\n- Calls `await session.handoff(customInstructions)`.\n- If result is `undefined`: `showError(\"Handoff cancelled\")`.\n- On success:\n - `rebuildChatFromMessages()` (loads new session context, including injected handoff)\n - invalidates status line and editor top border\n - reloads todos\n - appends success chat line: `New session started with handoff context`\n- On exception:\n - if message is `\"Handoff cancelled\"` or error name is `AbortError`: `showError(\"Handoff cancelled\")`\n - otherwise: `showError(\"Handoff failed: <message>\")`\n- Stops the loader, restores the previous Escape handler, and requests render at end.\n\nManual `/handoff` no longer streams the generated document into chat. A cancellable loader remains visible while the oneshot request runs, and the chat is rebuilt after generation completes.\n\n## Cancellation semantics\n\n### Session-level cancellation primitive\n\n`AgentSession` exposes:\n\n- `abortHandoff()` → aborts `#handoffAbortController`\n- `isGeneratingHandoff` → true while controller exists\n\nWhen this abort path is used, the abort signal is passed to `completeSimple(...)`; `handoff()` normalizes the cancellation to `Error(\"Handoff cancelled\")`, and command controller maps it to cancellation UI.\n\n### Interactive `/handoff` path\n\nThe command controller installs a temporary Escape handler for `/handoff` while the loader is visible. Pressing Escape calls `session.abortHandoff()`, which aborts the `completeSimple(...)` request through `#handoffAbortController`.\n\n## Aborted vs failed handoff\n\nCurrent UI classification:\n\n- **Aborted/cancelled**\n - `abortHandoff()` path triggers `\"Handoff cancelled\"`, or\n - thrown `AbortError`\n - UI shows `Handoff cancelled`\n- **Failed**\n - any other thrown error from `handoff()` / `generateHandoff()` / provider request path\n - UI shows `Handoff failed: ...`\n\nAdditional nuance: if generation completes but no text is returned, `handoff()` returns `undefined` and controller currently reports **cancelled**, not **failed**.\n\n## Short-session and minimum-content guardrails\n\nTwo guards prevent low-signal handoffs:\n\n- UI layer (`handleHandoffCommand`): warns and returns early for `< 2` message entries\n- Session layer (`handoff()`): throws the same condition as an error\n\nThis avoids creating a new session with empty/near-empty handoff context.\n\n## Concurrency: the shared session-transition lease\n\n`handoff()` does not run concurrently with any other session-identity transition.\nA single synchronously-acquired lease (`#beginSessionTransition` / `#endSessionTransition`)\nserializes every operation that replaces or rewrites session identity/history:\n\n- `handoff()`\n- `compact()`\n- `newSession()` / `switchSession()` / `branch()` / `clearContext()`\n- `fork()`\n- `navigateTree()`\n\nEach of these acquires the lease at its entry (before its first `await`) and releases\nit in its `finally`. Because acquisition is synchronous and up front, exclusion is\n**symmetric**: whichever transition starts first owns the lease, and any peer that\nstarts while it is held is rejected with an `Error` carrying `code: \"busy\"` and a\nmessage of the form `Cannot start <kind> while a <holder> transition is in progress.`\nThe rejection happens at the peer's own lease-acquisition point, i.e. **before any\nsession mutation**, so a losing transition never partially mutates the session.\n\nAuto-triggered handoff acquires the lease through `handoff()` itself; the maintenance\norchestrator does not hold the lease, so an auto-handoff running inside post-turn\nmaintenance does not self-deadlock even while auto-compaction owns its own abort\ncontroller.\n\nThis lease is distinct from the turn-start guard (`#assertNoHandoffTransition`), which\nfences external turn starters (prompt / steer / follow-up / continuation) for the whole\nhandoff transition and rejects them with `Cannot start a turn while a handoff is in progress.`\n\n## State transition summary\n\nHigh-level state flow:\n\n1. Interactive slash command intercepted.\n2. Preflight message-count guard.\n3. `#handoffAbortController` created (`isGeneratingHandoff = true`).\n4. `generateHandoff(...)` issues one `completeSimple(...)` request with live system prompt, tools, message history, and trailing handoff prompt.\n5. Assistant response text blocks are joined; tool-call blocks are discarded.\n6. If missing text → return `undefined`; if aborted → cancellation error path.\n7. If present:\n - flush old session\n - cancel async jobs\n - create new empty session with previous session as parent\n - reset runtime queues/counters\n - append `custom_message(handoff)`\n - optionally save an auto-triggered handoff document under the session artifacts directory when `compaction.handoffSaveToDisk` is enabled\n8. Controller rebuilds chat UI and announces success.\n9. `#handoffAbortController` cleared (`isGeneratingHandoff = false`).\n\n## Known assumptions and limitations\n\n- No structural validation checks that generated markdown follows the requested section format.\n- Missing generated text is reported as cancellation in controller UX.\n- Manual handoff has no streaming visibility; a cancellable loader is shown until the UI updates after generation completes.\n- Auto-triggered handoffs can save the handoff document as a session artifact (`artifact://<id>`) when `compaction.handoffSaveToDisk` is enabled; save failure is logged and does not fail the handoff.\n",
36
36
  "hermes-mcp-bridge.md": "# Coordinator MCP bridge\n\nSKC exposes a native outward MCP bridge for external coordinators:\n\n```bash\nskc mcp-serve coordinator\n```\n\n`skc mcp-serve hermes` is accepted as a compatibility alias for the same coordinator bridge.\n\nThe bridge is intentionally separate from SKC's client-side MCP runtime. It lets an external coordinator discover and control SDK-backed sessions, queue bounded follow-up prompts, read status/artifacts, handle structured questions, and write coordination reports without scraping terminal scrollback.\n\n## Core contract and adapters\n\nThe coordinator bridge is intentionally a core contract with multiple adapters, not an MCP-only or Hermes-only product direction. Hermes is one compatibility preset, not a privileged integration mode:\n\n- `packages/coding-agent/src/coordinator/contract.ts` owns transport-neutral server metadata and tool names.\n- `skc mcp-serve coordinator` is the outward MCP adapter for external agents.\n- `skc coordinator` is the read-only CLI/debug adapter for humans and scripts that need to inspect the same contract without starting MCP transport.\n- `skc setup hermes` is the compatibility setup adapter that renders coordinator config and operator guidance.\n\nFuture session, turn, question, artifact, and report behavior should move toward shared coordinator core services that both MCP and CLI adapters call instead of duplicating transport-specific logic.\n\n## Coordinator setup adapter\n\nUse `skc setup hermes` to render or install a portable MCP setup package for any controller that accepts Hermes-compatible MCP config:\n\n```bash\nskc setup hermes --root /path/to/repo --profile my-bot --repo sayknow-cli\n```\n\nThe default mode is render-only and writes no files. To install into a Hermes profile:\n\n```bash\nskc setup hermes \\\n --root /path/to/repo \\\n --profile my-bot \\\n --repo sayknow-cli \\\n --mutation sessions,questions,reports \\\n --profile-dir /path/to/hermes/profile \\\n --install\n```\n\nThe generated setup is model-agnostic and worktree-isolated. By default it renders `SKC_COORDINATOR_MCP_SESSION_COMMAND` as `skc --worktree`, which is a typed selector for SDK lifecycle creation—not a shell command the bridge runs. Spawned sessions launch inside a SKC-managed sibling worktree while SKC retains the source repository as project identity. Users who need a stable named branch can set `--worktree-name`:\n\n```bash\nskc setup hermes \\\n --root /path/to/repo \\\n --worktree-name hermes-sayknow-cli\n```\n\nThe runtime accepts only the literal selectors `skc` and `skc --worktree [name]`. It rejects local wrappers, shell syntax, tmux flags, and model/provider flags before creating a session. Existing setup configs that contain a legacy explicit `--session-command` must be changed to one of those selectors; provider and model resolution remains normal SKC configuration, not coordinator command injection.\n\nRun a non-mutating setup smoke check with:\n\n```bash\nskc setup hermes --root /path/to/repo --smoke\n```\n\nSmoke verifies the MCP server/tool contract. It does not call a downstream LLM and does not validate provider credentials.\n\n\n## Safety model\n\nThe bridge is read-only and fail-closed by default.\n\nRequired root allowlist:\n\n```bash\nexport SKC_COORDINATOR_MCP_WORKDIR_ROOTS=\"/path/to/repo:/path/to/worktrees\"\n```\n\nMutating tools require both startup opt-in and per-call consent:\n\n```bash\nexport SKC_COORDINATOR_MCP_MUTATIONS=\"sessions,questions,reports\"\n```\n\nEvery mutating MCP call that requires a caller key must include `allow_mutation: true` and the required caller-provided `idempotency_key`. The bridge durably binds the key to the tool and canonical arguments, serializes concurrent duplicates, replays the original bounded public response, and rejects reuse with different arguments as `idempotency_conflict`.\n\n`skc_coordinator_start_session` uses SDK lifecycle control with the configured typed SKC selector. `skc setup hermes` writes `skc --worktree` by default:\n\n```bash\nexport SKC_COORDINATOR_MCP_SESSION_COMMAND=\"skc --worktree\"\n```\n\nThe only supported values are `skc` and `skc --worktree [name]`; this variable is never evaluated as a shell command. The coordinator binds registration, reuse, and control to the broker's exact canonical workspace and endpoint generation, then discovers the generation-bound SDK endpoint internally. Endpoint credentials are never persisted in coordinator records or returned by coordinator tools. `skc_coordinator_read_coordination_status` returns a canonical polling snapshot for public session, state, turn, question, report, and bounded event data. Tmux identifiers, when supplied while registering an existing session, are advisory process metadata only; they do not provide control authority, machine viewing, startup, prompt injection, or determine turn completion.\n\nFor resume safety, prefer the generated SKC-native worktree selector over creating a git worktree in Hermes itself. SKC's launch path records the original repo as the project identity while running in the worktree, so session listing/resume can still group the session under the source project. If Hermes creates and later deletes an unmanaged worktree, a saved session may still exist but its cwd can be gone.\n\nArtifact reads are canonicalized, symlink escapes are rejected, and returned content is byte-capped by `SKC_COORDINATOR_MCP_ARTIFACT_BYTE_CAP`.\n\n`skc setup hermes` renders `SKC_COORDINATOR_MCP_WORKDIR_ROOTS` with the host platform path delimiter (`:` on POSIX, `;` on Windows). Manual configs should prefer the same encoding.\n\n## Optional namespace\n\nUse namespace variables to prevent cross-profile or cross-repo enumeration:\n\n```bash\nexport SKC_COORDINATOR_MCP_PROFILE=\"team-a\"\nexport SKC_COORDINATOR_MCP_REPO=\"sayknow-cli\"\n```\n\nMissing namespace never widens into global session enumeration.\n\n## Tool surface\n\nRead tools:\n\n- `skc_coordinator_list_sessions`\n- `skc_coordinator_read_status`\n- `skc_coordinator_read_tail`\n- `skc_coordinator_list_questions`\n- `skc_coordinator_list_artifacts`\n- `skc_coordinator_read_artifact`\n- `skc_coordinator_read_coordination_status`\n- `skc_coordinator_read_turn`\n- `skc_coordinator_await_turn`\n- `skc_coordinator_watch_events`\n\n\nMutating tools:\n\n- `skc_coordinator_start_session`\n- `skc_coordinator_register_session`\n- `skc_coordinator_send_prompt`\n- `skc_coordinator_submit_question_answer`\n- `skc_coordinator_report_status`\n- `skc_delegate_plan`\n- `skc_delegate_execute`\n- `skc_delegate_team`\n\nThe `skc_delegate_*` tools are high-level, session-level delegation: each starts (or reuses) an SDK-discovered session and sends one workflow-tagged turn for `/skill:ralplan`, `/skill:ultragoal`, or `/skill:team`, returning a durable `turn_id`, status, and artifact references. They use the same `sessions` mutation class and fail-closed workdir gating as `skc_coordinator_start_session`, and emit a `delegation.started` event. Pass `await_completion: true` to use the durable bounded await/report path; `timeout_ms` and `poll_interval_ms` apply to that completion payload. Without it, the tool returns immediately after SDK acknowledgement. Pass `cwd` and `task`; set `allow_mutation: true` and a caller-provided `idempotency_key` only with startup mutation opt-in plus per-call consent. Optionally pass `mpreset` (same semantics as `skc --mpreset <profile>`) to `skc_coordinator_start_session` or a delegate tool to authoritatively activate a SKC model profile when starting a fresh session — it is resolved through the merged built-in/custom profile registry, applied from the first turn, and surfaced in status; unknown names are rejected with the available-profile listing, and reusing a session with a conflicting `mpreset` fails with `mpreset_conflict`. This is distinct from the advisory `model` prompt hint. Prefer these over manual `start_session` + `send_prompt` when delegating a whole workflow.\n\n`skc_coordinator_register_session` registers an existing SDK-discoverable SKC session for coordinator control. It validates the workdir allowlist and session id, then verifies the broker's exact canonical workspace and endpoint generation before writing a credential-free session record. Optional tmux identifiers are retained only as advisory process metadata and are never machine-read.\n## Turn orchestration flow\n\nExternal coordinators should treat turns, not terminal scrollback, as the unit of work:\n\n1. Call `skc_coordinator_start_session` with `allow_mutation: true` and `idempotency_key`.\n2. Call `skc_coordinator_send_prompt` with `allow_mutation: true` and `idempotency_key`.\n3. Store the returned `turn_id`.\n4. Poll `skc_coordinator_read_turn`, or call bounded `skc_coordinator_await_turn`, until the turn is terminal.\n5. Pull `skc_coordinator_list_questions` with the required `session_id`; it reconciles pending `workflow.gates.list` rows and returns bounded questions, diagnostics, and reconciliation state. Submit each pending row with `skc_coordinator_submit_question_answer`.\n\n6. Use `skc_coordinator_report_status` with `session_id` and `turn_id` to write explicit completion/failure evidence.\n Use `status: \"cancelled\"` for coordinator-policy cancellation, and `status: \"failed\"` plus `blocker` for provider/tool/task failures.\n\n`skc_coordinator_send_prompt` returns versioned top-level routing fields that exactly mirror its nested durable `turn`: `status`, `queued`, and `delivered` equal `turn.status`, `turn.delivery.queued`, and `turn.delivery.delivered`; `active_turn_id` is the new turn id unless this response queued a follow-up, in which case it is the existing active turn id.\n\n```json\n{\n \"ok\": true,\n \"session_id\": \"skc-coordinator-demo\",\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"active_turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"status\": \"active\",\n \"queued\": false,\n \"delivered\": true\n}\n```\n\nA session may have only one active turn by default. A second prompt is rejected with `active_turn_exists` unless the caller explicitly passes `queue: true` or `force: true`. Queued turns are durable and the next queued turn is promoted when the active turn reaches a terminal `skc_coordinator_report_status`. Force supersedes the previous active turn and audits that state in the turn journal.\nCoordinator cancellation is recorded through `skc_coordinator_report_status` with terminal `status: \"cancelled\"`; this updates durable turn state but does not control any process. If the correct policy is replacement work rather than cancellation, send the replacement prompt with `force: true` so the previous active turn is superseded and audited.\n\n`skc_coordinator_read_turn` returns the authoritative durable turn and SDK-only advisory status. For the latest assistant output, use `skc_coordinator_read_tail`; it queries `session.last_assistant` through the session SDK and returns only the requested bounded line suffix, never terminal output.\n\n```json\n{\n \"ok\": true,\n \"turn\": {\n \"schema_version\": 1,\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"session_id\": \"skc-coordinator-demo\",\n \"status\": \"completed\",\n \"final_response\": {\n \"text\": \"Done\",\n \"format\": \"markdown\",\n \"source\": \"report_status\",\n \"artifact_path\": null,\n \"truncated\": false\n },\n \"evidence\": [{ \"path\": \"artifact.txt\" }],\n \"error\": null\n },\n \"advisory_status\": {\n \"authority\": \"sdk\",\n \"live\": true,\n \"is_streaming\": false\n }\n}\n```\n\nThe coordinator MCP bridge is currently a durable polling/await surface. It does not expose a push subscription stream; external coordinators should poll `skc_coordinator_read_coordination_status`, `skc_coordinator_read_turn`, or bounded `skc_coordinator_await_turn` instead of waiting for server-sent push events.\n\nExternal `session_id`, `turn_id`, and `question_id` values are validated before path use, and loaded records must match the requested session/turn owner.\n\n### Coordinator question pull loop\n\n`skc_coordinator_list_questions` requires `session_id` and reconciles the session's pending `workflow.gates.list` rows on every call. Its bounded response contains public `questions`, `diagnostics`, and `reconciliation`; `status: \"pending\"` selects pending rows, while `status: \"open\"` remains a compatibility alias. More than one pending question may be returned. Public rows expose only the safe question shape, public option ids, and a fresh `answer_binding` for each pending row—never raw/private gate payloads or values.\n\n`skc_coordinator_submit_question_answer` requires `session_id`, `turn_id`, `question_id`, `answer_binding`, `answer`, `idempotency_key`, and `allow_mutation: true`. Copy the identifiers and binding from the pending row and use the advertised answer shape. The bridge re-reconciles and revalidates ownership, pending state, and the binding before calling `workflow.gate_answer`; it never invokes generic `ask.answer`. An incomplete snapshot fails as `terminal_uncertain`; stale, terminal, missing, or ownership-mismatched rows are non-answerable. Restart can remint or quarantine gates, so re-list instead of reusing old rows. Identical idempotent replay returns the original accepted result; the same key with different arguments fails `idempotency_conflict`.\n\nThis pull-loop contract is independent of #2549/#2551 and unattended plain-CLI handling.\n\n## Coordinator event journal\n\nThe bridge persists a restart-safe event journal under the configured coordinator state namespace, for example:\n\n```text\n$SKC_COORDINATOR_MCP_STATE_ROOT/<profile>/<repo>/events/event-journal.jsonl\n```\n\nEach event is a bounded JSONL record with `schema_version`, monotonic namespace-local `seq`, stable `id`, `timestamp`, canonical `kind`, optional `session_id`/`turn_id`/`question_id`/`report_id`, short `summary`, optional `payload_ref`, and bounded scalar `metadata`. Full prompts, reports, final responses, and artifacts stay in their existing turn/report/artifact read paths; event records only point at them.\n\n`skc_coordinator_watch_events` is a bounded long-poll MCP tool, not an unbounded stream. Inputs are `after_seq` (default `0`), optional `session_id`, optional `event_types`, `timeout_ms` capped at 30000, and `limit` capped at 100. If matching events already exist after `after_seq`, it returns immediately. Otherwise it waits for the event journal to change or for timeout. The response includes `events`, `latest_seq`, `timed_out`, and `transport: { \"mcp\": \"long_poll\", \"push_subscriptions\": false }`, so coordinators can persist `latest_seq` and resume safely after restart.\n\n`skc_coordinator_read_coordination_status` keeps its existing report fields and now also includes `latest_event_seq` plus recent event summaries for snapshot-style consumers.\n\n## Generic controller config snippet\n\n```json\n{\n \"mcp_servers\": {\n \"skc_coordinator\": {\n \"command\": \"skc\",\n \"args\": [\"mcp-serve\", \"coordinator\"],\n \"env\": {\n \"SKC_COORDINATOR_MCP_WORKDIR_ROOTS\": \"/path/to/repo\",\n \"SKC_COORDINATOR_MCP_PROFILE\": \"team-a\",\n \"SKC_COORDINATOR_MCP_REPO\": \"project\",\n \"SKC_COORDINATOR_MCP_SESSION_COMMAND\": \"skc --worktree\"\n },\n \"enabled\": true\n }\n }\n}\n```\n\n## Smoke check\n\n```bash\nskc mcp-serve coordinator --check --json\n```\n\nExpected result includes `ok: true`, server name `skc-coordinator-mcp`, and the SKC-named tool list. The JSON check is discovery-only and non-mutating: it retains those legacy fields and adds `catalog: { \"ready\": true, \"reason\": null }` and `broker`. `broker.discovery_status` is `ready`, `unavailable`, or `error`, with reason `null`, `absent_or_invalid`, `unsupported_state_version`, `discovery_access_denied`, or `discovery_read_failed`. `broker.operational_ready` is always `null`; the check does not connect, ensure/bootstrap, write, repair, or delete. `bootstrap_supported` is `true` and `bootstrap_attempted` is `false`. It does not expose broker authority, path, endpoint, process metadata, token, or raw error details. `skc mcp-serve hermes --check --json` returns the identical coordinator check payload; its human output remains the server/tools summary.\n",
37
37
  "hotspot-map-successor.md": "# cpu-hotspot-map.json — successor pointer\n\n[`cpu-hotspot-map.json`](./cpu-hotspot-map.json) is **closed out**. All 11 CPU hotspots (H01–H11) and 5 memory hotspots (M01–M05) are resolved or rationally deferred across Optimization Suites v1 (#356), v2 (#530), and v3 (#548/#557/#558). Do **not** treat it as an open implementation backlog.\n\nThat map was a **static structural ranking** (algorithmic complexity × trigger frequency). Its `method` field records that real CPU self-time was \"to be measured by the agreed profiling corpus during optimization.\"\n\nFuture perf prioritization comes from the **profiling corpus**, not from this static map:\n\n- Evidence classes (`wallClockPhase`, `processCpuUsage`, `profilerSelfTime`, `rssMemory`, `byteParity`) and the corpus schema: see `docs/perf-profiling-corpus.md` (added with the corpus foundation).\n- Native algorithmic ports proposed for leftover hotspots are gated by [`native-ffi-optimization-policy.md`](./native-ffi-optimization-policy.md).\n\nA hotspot may be labeled `CPU-self-time confirmed` only when a `profilerSelfTime` artifact exists; v1–v3 shipped wins are otherwise classified as `covered-current`, `not-visible`, `needs-trace-coverage`, or `fallback-toggle-confirmed`.\n",
38
- "keybindings.md": "# Keybindings\n\nRun `/hotkeys` inside an `skc` session to see the active chords for your current build. The list reflects any remaps loaded from disk and any bindings added by extensions.\n\n## Customize keybindings\n\nUser remaps live in `~/.skc/agent/keybindings.json`. The file is a JSON object whose keys are keybinding action IDs and whose values are either one chord string or an array of chord strings. It is not read from `~/.skc/agent/config.yml`, and there is no nested `keybindings` object.\n\n```json\n{\n \"app.commandPalette.open\": \"ctrl+p\",\n \"app.model.cycleForward\": \"alt+n\",\n \"app.model.selectTemporary\": \"alt+p\",\n \"app.plan.toggle\": \"alt+shift+p\"\n}\n```\n\nChord names are case-insensitive. New configuration should use canonical textual IDs rather than matching the labels shown in the UI.\nConfiguration uses portable canonical key IDs, not the labels printed by a particular host: use `ctrl`, `alt`, `shift`, and `super` with a key name, for example `ctrl+p`, `alt+enter`, `shift+tab`, and `super+c`. Matching is case-insensitive, but new configuration should use this canonical textual form so the same file remains portable.\n\nRuntime UI labels are platform-native. On macOS, `Ctrl`, `Alt`, `Shift`, and `Super` display as `⌃`, `⌥`, `⇧`, and `⌘`; MacBook keycaps such as Return, Escape, Tab, Delete, and the arrow keys display as `↩`, `⎋`, `⇥`, `⌫`/`⌦`, and arrows. These glyphs are display labels only: configure `super+c`, not `⌘C`, and `alt+enter`, not `⌥↩`.\nOn macOS, Option shortcuts work only when the terminal sends Option as Meta/Esc or uses an enhanced keyboard protocol that reports the modifier. Command/Super is usually handled by the terminal or operating system and does not reach SKC. Windows Alt and macOS Option both use the canonical `alt` ID in configuration. Text produced by an Option key as composed Unicode cannot be reverse-inferred as an Option chord.\n\nFor terminals that do not forward Option, remap the queue actions to canonical Control chords (choose unclaimed chords appropriate for your terminal), for example:\n\n```json\n{\n \"app.message.queue\": \"ctrl+q\",\n \"app.message.dequeue\": [\"ctrl+pageup\", \"ctrl+pagedown\"]\n}\n```\nStatic onboarding and generated reference material describe shipped defaults and must stay host-independent. The active runtime surface is authoritative for effective bindings after user remaps and extensions load: use `/hotkeys` to see those bindings on the current platform.\n\nSet an action to an empty array to disable it:\n\n```json\n{\n \"app.stt.toggle\": []\n}\n```\n\n## Common action IDs\n\n| Action ID | Default | Meaning |\n| --- | --- | --- |\n| `app.commandPalette.open` | `ctrl+p` | Open the command palette |\n| `app.model.cycleForward` | `alt+n` | Cycle role models forward |\n| `app.model.cycleBackward` | `alt+shift+n` | Cycle role models backward |\n| `app.model.selectTemporary` | `alt+p` | Pick a model temporarily for this session |\n| `app.model.select` | `ctrl+l` | Open the model selector and set roles |\n| `app.plan.toggle` | `alt+shift+p` | Toggle plan mode |\n| `app.history.search` | `ctrl+r` | Search prompt history |\n| `app.tools.expand` | `ctrl+o` | Toggle tool-output expansion |\n| `app.thinking.toggle` | `ctrl+t` | Toggle thinking-block visibility |\n| `app.thinking.cycle` | `shift+tab` | Cycle thinking level |\n| `app.editor.external` | `ctrl+g` | Edit the draft in `$VISUAL` / `$EDITOR` |\n| `app.message.followUp` | _(none)_ | Optional remap for a follow-up message; `ctrl+enter` is reserved for editor newline |\n| `app.message.queue` | `alt+enter` (`alt+q` on darwin/win32) | Explicitly queue a message for the next turn |\n| `app.message.dequeue` | `alt+up`, `alt+down` | Open the queue and select a queued message to edit |\n\n| `app.clipboard.copyLine` | `alt+shift+l` | Copy the current line |\n| `app.clipboard.copyPrompt` | `alt+shift+c` | Copy the whole prompt |\n| `app.stt.toggle` | `alt+h` | Toggle speech-to-text recording |\n| `app.irc.sidebar.toggle` | `alt+i` | Toggle IRC sidebar |\n\nOlder unqualified action names are migrated when `keybindings.json` is loaded, but new docs and new configs should use the namespaced action IDs above.\n\nOn macOS, Option+Q queues a message for the next turn; on native Windows terminals, the equivalent default is Alt+Q. Windows Terminal and PowerShell commonly reserve Alt+Enter for fullscreen before SKC can receive it. Users who prefer another chord can remap `app.message.queue` in `~/.skc/agent/keybindings.json`.\n\nWhen messages are queued, use Option+Up/Down on macOS (Alt+Up/Down on Windows) to open the queue and select a message. In the queue, Return edits the selected message, Forward Delete (`⌦`; Fn+Delete on compact Mac keyboards) removes it, Control+Up/Down reorders it within its delivery group, and Escape closes the queue. Reordering does not convert compaction, steer, and follow-up messages into one another.\n\nIn the main SKC composer, plain `PageUp` / `PageDown` page the visible transcript lane instead of browsing prompt history; the status line and composer remain fixed at the bottom while manually scrolled. When SKC owns mouse input (`mouse.enabled: true`), the wheel moves the transcript by three rows per notch. Ordinary typing or paste keeps editor focus and returns to live output before editing; use `Up` / `Down` or `Ctrl+R` for prompt history. Autocomplete and selector surfaces still use `PageUp` / `PageDown` for list paging while they have focus.\n\n## Auditing default-key collisions\n\nSome default chords are intentionally reused across different UI contexts, where the focused component disambiguates them at dispatch time. For example `Enter` maps to both input submit and selection confirm, and `Ctrl+C` maps to both input copy and selection cancel. These are not conflicts — only one context is active at a time.\n\nTo audit the registry for keys whose default binding is claimed by more than one action, use `detectDefaultKeyCollisions(definitions)` from `@sayknow-cli/tui/keybindings`. It returns one entry per colliding key with the list of claiming action IDs, which is useful when adding new defaults or reviewing the surface. User-remap conflicts (multiple actions bound to the same chord in `keybindings.json`) continue to be reported separately by `KeybindingsManager.getConflicts()`.\n\nTwo audit clarifications for the current surface:\n\n- `app.clipboard.copyLine` is registry-backed and dispatched through the input controller's custom key handlers, not hardcoded.\n- `tui.input.copy` is declared in the registry but is not currently dispatched by `Editor.handleInput`.\n\nThe editor's configurable action defaults (including the platform-aware `app.clipboard.pasteImage` default) are derived directly from the central `KEYBINDINGS` registry, so there is a single source of truth for those defaults.\n\n## Current surface audit\n\nAuthoritative inventory of the keybinding registry, one row per action. Generated from `TUI_KEYBINDINGS` (`packages/tui/src/keybindings.ts`) and `KEYBINDINGS` (`packages/coding-agent/src/config/keybindings.ts`). Every action ID below is remappable via `~/.skc/agent/keybindings.json` unless noted. A drift test (`packages/coding-agent/test/keybindings-audit.test.ts`) asserts every registry action ID appears in this table.\n\n### Editor context (`tui.editor.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.editor.cursorUp` | `up` | |\n| `tui.editor.cursorDown` | `down` | |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | `ctrl+b` also `app.tool.backgroundFold` (other context) |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | `ctrl+left` also `app.tree.foldOrUp` |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | `ctrl+right` also `app.tree.unfoldOrDown` |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+a` | |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+e` | |\n| `tui.editor.jumpForward` | `ctrl+]` | |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | |\n| `tui.editor.pageUp` | `pageUp` | |\n| `tui.editor.pageDown` | `pageDown` | |\n| `tui.editor.deleteCharBackward` | `backspace` | |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | `ctrl+d` also `app.exit` / `app.session.delete` |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace`, `ctrl+backspace` | |\n| `tui.editor.deleteWordForward` | `alt+delete`, `alt+d` | |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | |\n| `tui.editor.yank` | `ctrl+y` | |\n| `tui.editor.yankPop` | `alt+y` | |\n| `tui.editor.undo` | `ctrl+-`, `ctrl+_` | |\n\n### Input context (`tui.input.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.input.newLine` | `Shift+Enter` | `Ctrl+Enter` and `Ctrl+Shift+Enter` are also accepted by the editor when the terminal encodes them distinctly |\n\n| `tui.input.submit` | `enter` | also `tui.select.confirm` (other context) |\n| `tui.input.tab` | `tab` | |\n| `tui.input.copy` | `ctrl+c` | declared but not dispatched by `Editor.handleInput` |\n\n### Selection context (`tui.select.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.select.up` | `up` | |\n| `tui.select.down` | `down` | |\n| `tui.select.pageUp` | `pageUp` | |\n| `tui.select.pageDown` | `pageDown` | |\n| `tui.select.confirm` | `enter` | |\n| `tui.select.cancel` | `escape`, `ctrl+c` | `escape` also `app.interrupt` |\n\n### Application context (`app.*`)\n\n| Action ID | Default | Domains |\n| --- | --- | --- |\n| `app.interrupt` | escape | global |\n| `app.clear` | ctrl+c | global |\n| `app.exit` | ctrl+d | global |\n| `app.suspend` | ctrl+z | global |\n| `app.thinking.cycle` | shift+tab | composer |\n| `app.thinking.toggle` | ctrl+t | composer |\n| `app.commandPalette.open` | ctrl+p | composer |\n| `app.model.cycleForward` | alt+n | composer |\n| `app.model.cycleBackward` | alt+shift+n | composer |\n| `app.model.select` | ctrl+l | composer |\n| `app.model.selectTemporary` | alt+p | composer |\n| `app.tools.expand` | ctrl+o | composer |\n| `app.todo.toggle` | alt+shift+t | composer |\n| `app.tool.backgroundFold` | ctrl+b | composer |\n| `app.editor.external` | ctrl+g | composer |\n| `app.message.followUp` | _(none)_ | composer |\n| `app.message.queue` | alt+q (darwin/win32) / alt+enter (linux) | composer |\n| `app.message.dequeue` | alt+up, alt+down | composer |\n| `app.clipboard.pasteImage` | ctrl+v (darwin/linux) / alt+v (win32) | composer |\n| `app.clipboard.copyLine` | alt+shift+l | composer |\n| `app.clipboard.copyPrompt` | alt+shift+c | composer |\n| `app.oauth.copyUrl` | alt+shift+u | composer |\n| `app.session.new` | ctrl+n | composer |\n| `app.session.tree` | _(none)_ | composer |\n| `app.session.fork` | _(none)_ | composer |\n| `app.session.resume` | _(none)_ | composer |\n| `app.session.observe` | ctrl+s | composer |\n| `app.session.dashboard` | _(none)_ | composer |\n| `app.jobs.open` | alt+j | composer |\n| `app.session.togglePath` | ctrl+p | selector |\n| `app.session.toggleSort` | ctrl+s | selector |\n| `app.session.rename` | ctrl+r | selector |\n| `app.session.delete` | ctrl+d | selector |\n| `app.session.deleteNoninvasive` | ctrl+backspace | selector |\n| `app.tree.foldOrUp` | ctrl+left, alt+left | selector |\n| `app.tree.unfoldOrDown` | ctrl+right, alt+right | selector |\n| `app.plan.toggle` | alt+shift+p | composer |\n| `app.history.search` | ctrl+r | composer |\n| `app.stt.toggle` | alt+h | composer |\n| `app.irc.sidebar.toggle` | alt+i | composer |\n| `app.transcript.browse` | _(none)_ | composer |\n| `app.transcript.prevTurn` | _(none)_ | composer |\n| `app.transcript.nextTurn` | _(none)_ | composer |\n| `app.mode.cycle` | _(none)_ | composer |\n| `app.tasks.toggle` | alt+t | composer |\n| `app.queue.togglePane` | _(none)_ | composer |\n| `app.message.sendNow` | _(none)_ | composer |\n\n### Global engine context (`tui.global.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.global.debug` | `shift+ctrl+d` | Toggle debug overlay; resolved through the registry in `tui.ts` |\n\nCross-context default reuse (`ctrl+s`, `ctrl+r`, `ctrl+d`, `ctrl+b`, `ctrl+left`/`ctrl+right`, `enter`, `escape`, `ctrl+c`) is intentional: each pair is active in a different focused context and is disambiguated at dispatch time. Use `detectDefaultKeyCollisions()` (above) to re-derive this list from the registry.\n\n### Not yet registry-managed\n\nA few contexts still match chords directly instead of resolving through the registry, and are tracked for a later phase:\n\n- Tree selector (`tree-selector.ts`): up/down/left/right/enter, `ctrl+c`, filter cycling (`ctrl+o` / `ctrl+shift+o`), filter modes (`alt+d/t/u/l/a`), label edit (`shift+l`).\n- Parts of the model selector.\n",
38
+ "keybindings.md": "# Keybindings\n\nRun `/hotkeys` inside an `skc` session to see the active chords for your current build. The list reflects any remaps loaded from disk and any bindings added by extensions.\n\n## Customize keybindings\n\nUser remaps live in `~/.skc/agent/keybindings.json`. The file is a JSON object whose keys are keybinding action IDs and whose values are either one chord string or an array of chord strings. It is not read from `~/.skc/agent/config.yml`, and there is no nested `keybindings` object.\n\n```json\n{\n \"app.commandPalette.open\": \"ctrl+p\",\n \"app.model.cycleForward\": \"alt+n\",\n \"app.model.selectTemporary\": \"alt+p\",\n \"app.plan.toggle\": \"alt+shift+p\"\n}\n```\n\nChord names are case-insensitive. New configuration should use canonical textual IDs rather than matching the labels shown in the UI.\nConfiguration uses portable canonical key IDs, not the labels printed by a particular host: use `ctrl`, `alt`, `shift`, and `super` with a key name, for example `ctrl+p`, `alt+enter`, `shift+tab`, and `super+c`. Matching is case-insensitive, but new configuration should use this canonical textual form so the same file remains portable.\n\nRuntime UI labels are platform-native. On macOS, `Ctrl`, `Alt`, `Shift`, and `Super` display as `⌃`, `⌥`, `⇧`, and `⌘`; MacBook keycaps such as Return, Escape, Tab, Delete, and the arrow keys display as `↩`, `⎋`, `⇥`, `⌫`/`⌦`, and arrows. These glyphs are display labels only: configure `super+c`, not `⌘C`, and `alt+enter`, not `⌥↩`.\nOn macOS, Option shortcuts work only when the terminal sends Option as Meta/Esc or uses an enhanced keyboard protocol that reports the modifier. Command/Super is usually handled by the terminal or operating system and does not reach SKC. Windows Alt and macOS Option both use the canonical `alt` ID in configuration. Text produced by an Option key as composed Unicode cannot be reverse-inferred as an Option chord.\n\nFor terminals that do not forward Option, remap the queue actions to canonical Control chords (choose unclaimed chords appropriate for your terminal), for example:\n\n```json\n{\n \"app.message.queue\": \"ctrl+q\",\n \"app.message.dequeue\": [\"ctrl+pageup\", \"ctrl+pagedown\"]\n}\n```\nStatic onboarding and generated reference material describe shipped defaults and must stay host-independent. The active runtime surface is authoritative for effective bindings after user remaps and extensions load: use `/hotkeys` to see those bindings on the current platform.\n\nSet an action to an empty array to disable it:\n\n```json\n{\n \"app.stt.toggle\": []\n}\n```\n\n## Common action IDs\n\n| Action ID | Default | Meaning |\n| --- | --- | --- |\n| `app.commandPalette.open` | `ctrl+p` | Open the command palette |\n| `app.model.cycleForward` | `alt+n` | Cycle role models forward |\n| `app.model.cycleBackward` | `alt+shift+n` | Cycle role models backward |\n| `app.model.selectTemporary` | `alt+p` | Pick a model temporarily for this session |\n| `app.model.select` | `ctrl+l` | Open the model selector and set roles |\n| `app.plan.toggle` | `alt+shift+p` | Toggle plan mode |\n| `app.history.search` | `ctrl+r` | Search prompt history |\n| `app.tools.expand` | `ctrl+o` | Toggle tool-output expansion |\n| `app.thinking.toggle` | `ctrl+t` | Toggle thinking-block visibility |\n| `app.thinking.cycle` | `shift+tab` | Cycle thinking level |\n| `app.editor.external` | `ctrl+g` | Edit the draft in `$VISUAL` / `$EDITOR` |\n| `app.message.followUp` | _(none)_ | Optional remap for a follow-up message; `ctrl+enter` is reserved for editor newline |\n| `app.message.queue` | `alt+enter` (`alt+q` on darwin/win32) | Explicitly queue a message for the next turn |\n| `app.message.dequeue` | `alt+up`, `alt+down` | Open the queue and select a queued message to edit |\n\n| `app.clipboard.copyLine` | `alt+shift+l` | Copy the current line |\n| `app.clipboard.copyPrompt` | `alt+shift+c` | Copy the whole prompt |\n| `app.stt.toggle` | `alt+h` | Toggle speech-to-text recording |\n| `app.irc.sidebar.toggle` | `alt+i` | Toggle IRC sidebar |\n\nOlder unqualified action names are migrated when `keybindings.json` is loaded, but new docs and new configs should use the namespaced action IDs above.\n\nOn macOS, Option+Q queues a message for the next turn; on native Windows terminals, the equivalent default is Alt+Q. Windows Terminal and PowerShell commonly reserve Alt+Enter for fullscreen before SKC can receive it. Users who prefer another chord can remap `app.message.queue` in `~/.skc/agent/keybindings.json`.\n\nWhen messages are queued, use Option+Up/Down on macOS (Alt+Up/Down on Windows) to open the queue and select a message. In the queue, Return edits the selected message, Forward Delete (`⌦`; Fn+Delete on compact Mac keyboards) removes it, Control+Up/Down reorders it within its delivery group, and Escape closes the queue. Reordering does not convert compaction, steer, and follow-up messages into one another.\n\nIn the main SKC composer, plain `PageUp` / `PageDown` page the visible transcript lane instead of browsing prompt history; the status line and composer remain fixed at the bottom while manually scrolled. When SKC owns mouse input (`mouse.enabled: true`), the wheel moves the transcript by three rows per notch. Ordinary typing or paste keeps editor focus and returns to live output before editing; use `Up` / `Down` or `Ctrl+R` for prompt history. Autocomplete and selector surfaces still use `PageUp` / `PageDown` for list paging while they have focus.\n\n## Auditing default-key collisions\n\nSome default chords are intentionally reused across different UI contexts, where the focused component disambiguates them at dispatch time. For example `Enter` maps to both input submit and selection confirm, and `Ctrl+C` maps to both input copy and selection cancel. These are not conflicts — only one context is active at a time.\n\nTo audit the registry for keys whose default binding is claimed by more than one action, use `detectDefaultKeyCollisions(definitions)` from `@sayknow-cli/tui/keybindings`. It returns one entry per colliding key with the list of claiming action IDs, which is useful when adding new defaults or reviewing the surface. User-remap conflicts (multiple actions bound to the same chord in `keybindings.json`) continue to be reported separately by `KeybindingsManager.getConflicts()`.\n\nTwo audit clarifications for the current surface:\n\n- `app.clipboard.copyLine` is registry-backed and dispatched through the input controller's custom key handlers, not hardcoded.\n- `tui.input.copy` is declared in the registry but is not currently dispatched by `Editor.handleInput`.\n\nThe editor's configurable action defaults (including the platform-aware `app.clipboard.pasteImage` default) are derived directly from the central `KEYBINDINGS` registry, so there is a single source of truth for those defaults.\n\n## Current surface audit\n\nAuthoritative inventory of the keybinding registry, one row per action. Generated from `TUI_KEYBINDINGS` (`packages/tui/src/keybindings.ts`) and `KEYBINDINGS` (`packages/coding-agent/src/config/keybindings.ts`). Every action ID below is remappable via `~/.skc/agent/keybindings.json` unless noted. A drift test (`packages/coding-agent/test/keybindings-audit.test.ts`) asserts every registry action ID appears in this table.\n\n### Editor context (`tui.editor.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.editor.cursorUp` | `up` | |\n| `tui.editor.cursorDown` | `down` | |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | `ctrl+b` also `app.tool.backgroundFold` (other context) |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | `ctrl+left` also `app.tree.foldOrUp` |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | `ctrl+right` also `app.tree.unfoldOrDown` |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+a` | |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+e` | |\n| `tui.editor.jumpForward` | `ctrl+]` | |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | |\n| `tui.editor.pageUp` | `pageUp` | |\n| `tui.editor.pageDown` | `pageDown` | |\n| `tui.editor.deleteCharBackward` | `backspace` | |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | `ctrl+d` also `app.exit` / `app.session.delete` |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace`, `ctrl+backspace` | |\n| `tui.editor.deleteWordForward` | `alt+delete`, `alt+d` | |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | |\n| `tui.editor.yank` | `ctrl+y` | |\n| `tui.editor.yankPop` | `alt+y` | |\n| `tui.editor.undo` | `ctrl+-`, `ctrl+_` | |\n\n### Input context (`tui.input.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.input.newLine` | `Shift+Enter` | `Ctrl+Enter` and `Ctrl+Shift+Enter` are also accepted by the editor when the terminal encodes them distinctly |\n\n| `tui.input.submit` | `enter` | also `tui.select.confirm` (other context) |\n| `tui.input.tab` | `tab` | |\n| `tui.input.copy` | `ctrl+c` | declared but not dispatched by `Editor.handleInput` |\n\n### Selection context (`tui.select.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.select.up` | `up` | |\n| `tui.select.down` | `down` | |\n| `tui.select.pageUp` | `pageUp` | |\n| `tui.select.pageDown` | `pageDown` | |\n| `tui.select.confirm` | `enter` | |\n| `tui.select.cancel` | `escape`, `ctrl+c` | `escape` also `app.interrupt` |\n\n### Application context (`app.*`)\n\n| Action ID | Default | Domains |\n| --- | --- | --- |\n| `app.interrupt` | escape | global |\n| `app.clear` | ctrl+c | global |\n| `app.exit` | ctrl+d | global |\n| `app.suspend` | ctrl+z | global |\n| `app.thinking.cycle` | shift+tab | composer |\n| `app.thinking.toggle` | ctrl+t | composer |\n| `app.commandPalette.open` | ctrl+p | composer |\n| `app.model.cycleForward` | alt+n | composer |\n| `app.model.cycleBackward` | alt+shift+n | composer |\n| `app.model.select` | ctrl+l | composer |\n| `app.model.selectTemporary` | alt+p | composer |\n| `app.tools.expand` | ctrl+o | composer |\n| `app.todo.toggle` | alt+shift+t | composer |\n| `app.tool.backgroundFold` | ctrl+b | composer |\n| `app.editor.external` | ctrl+g | composer |\n| `app.message.followUp` | _(none)_ | composer |\n| `app.message.queue` | alt+q (darwin/win32) / alt+enter (linux) | composer |\n| `app.message.dequeue` | alt+up, alt+down | composer |\n| `app.clipboard.pasteImage` | ctrl+v (darwin/linux) / alt+v (win32) | composer |\n| `app.clipboard.copyLine` | alt+shift+l | composer |\n| `app.clipboard.copyPrompt` | alt+shift+c | composer |\n| `app.oauth.copyUrl` | alt+shift+u | composer |\n| `app.session.new` | ctrl+n | composer |\n| `app.session.tree` | _(none)_ | composer |\n| `app.session.fork` | _(none)_ | composer |\n| `app.session.resume` | alt+r | composer |\n| `app.session.observe` | ctrl+s | composer |\n| `app.session.dashboard` | _(none)_ | composer |\n| `app.jobs.open` | alt+j | composer |\n| `app.session.togglePath` | ctrl+p | selector |\n| `app.session.toggleSort` | ctrl+s | selector |\n| `app.session.rename` | ctrl+r | selector |\n| `app.session.delete` | ctrl+d | selector |\n| `app.session.deleteNoninvasive` | ctrl+backspace | selector |\n| `app.tree.foldOrUp` | ctrl+left, alt+left | selector |\n| `app.tree.unfoldOrDown` | ctrl+right, alt+right | selector |\n| `app.plan.toggle` | alt+shift+p | composer |\n| `app.history.search` | ctrl+r | composer |\n| `app.stt.toggle` | alt+h | composer |\n| `app.irc.sidebar.toggle` | alt+i | composer |\n| `app.transcript.browse` | _(none)_ | composer |\n| `app.transcript.prevTurn` | _(none)_ | composer |\n| `app.transcript.nextTurn` | _(none)_ | composer |\n| `app.mode.cycle` | _(none)_ | composer |\n| `app.tasks.toggle` | alt+t | composer |\n| `app.queue.togglePane` | _(none)_ | composer |\n| `app.message.sendNow` | _(none)_ | composer |\n\n### Global engine context (`tui.global.*`)\n\n| Action ID | Default | Notes |\n| --- | --- | --- |\n| `tui.global.debug` | `shift+ctrl+d` | Toggle debug overlay; resolved through the registry in `tui.ts` |\n\nCross-context default reuse (`ctrl+s`, `ctrl+r`, `ctrl+d`, `ctrl+b`, `ctrl+left`/`ctrl+right`, `enter`, `escape`, `ctrl+c`) is intentional: each pair is active in a different focused context and is disambiguated at dispatch time. Use `detectDefaultKeyCollisions()` (above) to re-derive this list from the registry.\n\n### Not yet registry-managed\n\nA few contexts still match chords directly instead of resolving through the registry, and are tracked for a later phase:\n\n- Tree selector (`tree-selector.ts`): up/down/left/right/enter, `ctrl+c`, filter cycling (`ctrl+o` / `ctrl+shift+o`), filter modes (`alt+d/t/u/l/a`), label edit (`shift+l`).\n- Parts of the model selector.\n",
39
39
  "lsp-config.md": "# LSP configuration in SKC\n\nThis guide explains how to configure language servers for the SKC coding agent.\n\nSource of truth in code:\n\n- Server config type: `packages/coding-agent/src/lsp/types.ts` (`ServerConfig`)\n- Config loader: `packages/coding-agent/src/lsp/config.ts`\n- Built-in server definitions: `packages/coding-agent/src/lsp/defaults.json`\n\n## Auto-detection\n\nWhen no LSP config file is present, SKC auto-detects servers by intersecting two conditions:\n\n1. The project directory contains at least one of the server's `rootMarkers`.\n2. The server binary is a trusted external executable. Project-local binaries, including paths reached through symlinks, are rejected.\n\nNo configuration is required for common setups. The built-in server list covers most popular languages; see [`defaults.json`](../packages/coding-agent/src/lsp/defaults.json) for the full set.\n\n## Config file locations\n\nSKC merges LSP config from multiple files, lowest to highest priority:\n\n| Priority | Location |\n|----------|----------|\n| 5 (lowest) | `~/lsp.json`, `~/.lsp.json`, `~/lsp.yaml`, `~/.lsp.yaml` |\n| 4 | Preloaded trusted external plugin LSP config outside the project (internal loader support; no current CLI/startup producer) |\n| 3 | `~/.skc/agent/lsp.json`, `~/.skc/agent/lsp.yaml`, `~/.gemini/lsp.*` |\n| 2 | `<project>/.skc/lsp.json`, `<project>/.skc/lsp.yaml`, `<project>/.gemini/lsp.*` |\n| 1 (highest) | `<project>/lsp.json`, `<project>/.lsp.json`, `<project>/lsp.yaml` |\n\nEach location accepts both `.json` and `.yaml` / `.yml` variants, as well as hidden-file versions (`.lsp.json`, `.lsp.yaml`). Configuration is merged in order, but project-controlled files can only control declarative server matching, activation, and capabilities. They cannot define or override a server's `command`, `args`, executable, client factory, `initOptions` / `initializationOptions`, or `settings`; opaque options that can instruct a trusted server belong to trusted user configuration.\n\nThe recommended trusted user configuration is `~/.skc/agent/lsp.json` (or YAML equivalent). Legacy user-wide `~/.gemini/lsp.*` and home-root `~/lsp.*` / `~/.lsp.*` files are also outside the project and may define launch settings and opaque server options, including custom servers. Project files may refine declarative matching and activation fields of built-in or user-defined servers.\n\n**Recommended locations:**\n\n- Trusted user launch settings, `initOptions`, and `settings` → `~/.skc/agent/lsp.json`\n- Project-specific matching and activation → `<project>/.skc/lsp.json`\n\n> **Note:** The presence of any LSP config file disables auto-detection. When at least one file is found, SKC skips the binary-scan phase and loads matching, available, non-disabled servers using trusted launch definitions.\n\n## File shape\n\nBoth JSON and YAML are accepted. The top-level object can use either a `servers` wrapper key or a flat map directly:\n\n```json\n{\n \"servers\": {\n \"server-name\": { ... }\n },\n \"idleTimeoutMs\": 300000\n}\n```\n\nor (flat, without the `servers` wrapper):\n\n```json\n{\n \"server-name\": { ... },\n \"idleTimeoutMs\": 300000\n}\n```\n\nTop-level keys:\n\n- `servers` — map of server name to `ServerConfig` (optional wrapper; flat form is equivalent)\n- `idleTimeoutMs` — shut down idle language servers after this many milliseconds; disabled by default\n\n## ServerConfig fields\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `command` | `string` | trusted user config only | Server executable name or absolute path; project configuration cannot set or override it |\n| `args` | `string[]` | no | Launch arguments; trusted user config only |\n| `fileTypes` | `string[]` | yes | File extensions this server handles, e.g. `[\".ts\", \".tsx\"]` |\n| `rootMarkers` | `string[]` | yes | Files/dirs that indicate a project root; glob patterns (e.g. `*.cabal`) are supported |\n| `initOptions` | `object` | trusted user config only | Sent as `initializationOptions` during LSP handshake |\n| `settings` | `object` | trusted user config only | Workspace settings pushed via `workspace/didChangeConfiguration` |\n| `disabled` | `boolean` | no | Set to `true` to disable this server entirely |\n| `warmupTimeoutMs` | `number` | no | Startup timeout in ms for this server (overrides the global default) |\n| `isLinter` | `boolean` | no | Mark server as linter/formatter only; excluded from type-intelligence operations (hover, go-to-definition, etc.) |\n| `capabilities` | `object` | no | Opt-in server-specific features; see [Capabilities](#capabilities) |\n\n`resolvedCommand` is populated automatically at runtime — do not set it manually.\n\n### Capabilities\n\nThe `capabilities` object enables optional server-specific features that SKC supports on a per-server basis:\n\n```json\n{\n \"capabilities\": {\n \"flycheck\": true,\n \"ssr\": true,\n \"expandMacro\": true,\n \"runnables\": true,\n \"relatedTests\": true\n }\n}\n```\n\nAll fields are boolean and optional. They are currently used by `rust-analyzer`.\n\n## Common recipes\n\n### Override a built-in server's settings from trusted user configuration\n\nOpaque server settings may contain process-affecting instructions, so place these partial overrides in trusted user configuration such as `~/.skc/agent/lsp.json`:\n\n```json\n{\n \"servers\": {\n \"typescript-language-server\": {\n \"settings\": {\n \"typescript\": {\n \"preferences\": {\n \"quoteStyle\": \"single\"\n }\n }\n }\n }\n }\n}\n```\n\n```yaml\nservers:\n gopls:\n settings:\n gopls:\n gofumpt: false\n staticcheck: false\n```\n\n### Disable a built-in server\n\n```json\n{\n \"servers\": {\n \"eslint\": {\n \"disabled\": true\n }\n }\n}\n```\n\n### Register a custom server\n\nRegister custom servers in the canonical trusted user configuration, `~/.skc/agent/lsp.json`. New servers require `command`, `fileTypes`, and `rootMarkers`; `args` is optional. Project configuration cannot register a launch definition or override a server's command, arguments, executable, or client factory.\n\n```json\n{\n \"servers\": {\n \"my-lsp\": {\n \"command\": \"my-lsp-server\",\n \"args\": [\"--stdio\"],\n \"fileTypes\": [\".xyz\"],\n \"rootMarkers\": [\".xyz-project\", \".git\"]\n }\n }\n}\n```\n\n### Set a global idle timeout\n\nShut down language servers that have been inactive for more than five minutes:\n\n```json\n{\n \"idleTimeoutMs\": 300000\n}\n```\n\n### Disable a server for one project, keep it globally\n\nPlace the override in `<project>/.skc/lsp.json`:\n\n```json\n{\n \"servers\": {\n \"pylsp\": {\n \"disabled\": true\n }\n }\n}\n```\n\nThe user-level config in `~/.skc/agent/lsp.json` is unaffected; pylsp is only suppressed in this project.\n\nWhen multiple built-in primary servers support the same file, a default server can list lower-precedence servers in `supersedes`. For example, `csharp-ls` supersedes `omnisharp` only when both C# servers are installed and detected; if `csharp-ls` is unavailable, `omnisharp` remains the fallback.\n\n## lspmux\n\n`SKC_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.\n\n## Built-in server list\n\nThe following servers ship in `defaults.json` and are eligible for auto-detection:\n\n| Server key | Language(s) | Binary |\n|---|---|---|\n| `rust-analyzer` | Rust | `rust-analyzer` |\n| `clangd` | C, C++, ObjC | `clangd` |\n| `zls` | Zig | `zls` |\n| `gopls` | Go | `gopls` |\n| `typescript-language-server` | TypeScript, JavaScript | `typescript-language-server` |\n| `denols` | TypeScript, JavaScript (Deno) | `deno` |\n| `biome` | TS/JS/JSON (linter) | `biome` |\n| `eslint` | TS/JS/Vue/Svelte (linter) | `vscode-eslint-language-server` |\n| `vscode-html-language-server` | HTML | `vscode-html-language-server` |\n| `vscode-css-language-server` | CSS, SCSS, Less | `vscode-css-language-server` |\n| `vscode-json-language-server` | JSON | `vscode-json-language-server` |\n| `tailwindcss` | HTML, CSS, TS/JS | `tailwindcss-language-server` |\n| `svelte` | Svelte | `svelteserver` |\n| `vue-language-server` | Vue | `vue-language-server` |\n| `astro` | Astro | `astro-ls` |\n| `pyright` | Python | `pyright-langserver` |\n| `basedpyright` | Python | `basedpyright-langserver` |\n| `pylsp` | Python | `pylsp` |\n| `ruff` | Python (linter) | `ruff` |\n| `jdtls` | Java | `jdtls` |\n| `kotlin-lsp` | Kotlin | `kotlin-lsp` |\n| `metals` | Scala | `metals` |\n| `hls` | Haskell | `haskell-language-server-wrapper` |\n| `ocamllsp` | OCaml | `ocamllsp` |\n| `elixirls` | Elixir | `elixir-ls` |\n| `erlangls` | Erlang | `erlang_ls` |\n| `gleam` | Gleam | `gleam` |\n| `solargraph` | Ruby | `solargraph` |\n| `ruby-lsp` | Ruby | `ruby-lsp` |\n| `rubocop` | Ruby (linter) | `rubocop` |\n| `bashls` | Bash, Zsh | `bash-language-server` |\n| `lua-language-server` | Lua | `lua-language-server` |\n| `intelephense` | PHP | `intelephense` |\n| `phpactor` | PHP | `phpactor` |\n| `csharp-ls` | C# | `csharp-ls` |\n| `omnisharp` | C# | `omnisharp` |\n| `yamlls` | YAML | `yaml-language-server` |\n| `terraformls` | Terraform | `terraform-ls` |\n| `dockerls` | Dockerfile | `docker-langserver` |\n| `helm-ls` | Helm | `helm_ls` |\n| `nixd` | Nix | `nixd` |\n| `nil` | Nix | `nil` |\n| `ols` | Odin | `ols` |\n| `dartls` | Dart | `dart` |\n| `marksman` | Markdown | `marksman` |\n| `texlab` | LaTeX | `texlab` |\n| `graphql` | GraphQL | `graphql-lsp` |\n| `prismals` | Prisma | `prisma-language-server` |\n| `vimls` | Vim script | `vim-language-server` |\n| `emmet-language-server` | HTML, CSS, JSX | `emmet-language-server` |\n| `sourcekit-lsp` | Swift | `sourcekit-lsp` |\n| `swiftlint` | Swift (linter) | `swiftlint` |\n| `tlaplus` | TLA+ | `tlapm_lsp` |\n",
40
40
  "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",
41
41
  "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 built-in + 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- `~/.skc/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- `bedrock-converse-stream`\n- `google-generative-ai`\n- `google-vertex`\n- `google-gemini-cli`\n- `ollama-chat`\n- `cursor-agent`\n\n\n### First-class Azure OpenAI and Amazon Bedrock examples\n\nAzure OpenAI uses canonical OpenAI model IDs in SKC 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\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### MiniMax and GLM custom provider examples\n\nFor common MiniMax and GLM/zAI setup, prefer the provider presets so the OpenAI-compatible API, base URL, env var, model id, and compatibility flags are written together:\n\n```sh\nskc setup provider --preset minimax\nskc setup provider --preset minimax-cn\nskc setup provider --preset glm\nskc setup provider --preset alibaba-token-plan\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```\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`, or `ALIBABA_TOKEN_PLAN_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 (qwen3.8-max-preview uses openai-responses; glm-5.2 and deepseek-v4-pro use openai-completions).\n\n## Model profiles (`--mpreset`)\n\nModel profiles are optional top-level `profiles:` entries in `~/.skc/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-4-6: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`). Each role maps to exactly one model selector in the form `provider/modelId[:effort]`; comma-separated fallback chains are not supported in a single role value.\n`required_providers` is the aggregate set of providers required across the profile's mapped roles, not a per-role fallback chain.\n\nBuilt-in profiles are grouped by provider mix and tier:\n\n- `codex-{eco,medium,pro}` — all roles on `openai-codex/gpt-5.5`, differing only by per-role reasoning effort\n- `opencodego` — single OpenCode Go preset (Kimi default, DeepSeek executor/architect, Qwen planner, MiMo critic)\n- `claude-opus` — Anthropic OAuth preset centered on `claude-opus-5`\n- `claude-opus-5-5` — Anthropic preset centered on `claude-opus-5-5` (`xhigh` default, `max` architect, `high` critic, `medium` planner, `claude-sonnet-5` executor). Recommended automatically after an Anthropic login; `claude-opus` stays available for the Opus 5 cost/effort shape.\n- Single-provider tiers: `glm-{eco,medium,pro}`, `kimi-coding-plan-{eco,medium,pro}`, `mimo-{eco,medium,pro}`, `grok-{eco,medium,pro}`, `cursor-{eco,medium,pro}`, `minimax-{eco,medium,pro}`\n- Combos: `opus-codex` (Claude main agent with Codex support roles), `codex-opencodego` (Codex orchestrator/architect with OpenCode Go workers)\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; and Pro assigns Sol medium/Terra medium/Sol high/Sol max/Sol xhigh. `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). 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-1.5`, `minimax-code/minimax-m3`). User-defined profiles override built-ins by exact profile name.\n\n\nUse `skc --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\nskc --mpreset codex-medium\nskc --mpreset opencodego --default\n```\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.\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### 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`, `sglang`, or `openai-models-list`\n- `cacheRetention`: `none`, `short`, or `long`; request-time options win over model/modelOverride values, then provider values, then `SKC_CACHE_RETENTION`, then the runtime default. The runtime default is `short` for most providers, but the Anthropic provider defaults to `long` (`ttl: \"1h\"`) because the ~5m default is too fragile for long-running subagent workflows. The 1h marker is only emitted on the canonical Anthropic API (`api.anthropic.com`) for models advertising `supportsLongCacheRetention`; proxies, gateways, and incapable models fall back to the default ephemeral (~5m) breakpoint. 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:\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 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\n`input` is the model modality list SKC uses to decide whether image content is forwarded. When a custom model omits `input`, SKC 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 SKC 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 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- `apiKey` unless `auth: none`\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## Merge and override order\n\nModelRegistry pipeline (on refresh):\n\n1. Load built-in providers/models from `@sayknow-cli/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 resolution behavior\n\nWhen multiple concrete variants share a canonical id, resolution uses:\n\n1. availability and auth\n2. `config.yml` `modelProviderOrder`\n3. existing registry/provider order if `modelProviderOrder` is unset\n\nDisabled or unauthenticated providers are skipped.\n\nSession state and transcripts continue to record the concrete provider/model that actually 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 OpenAI-compatible provider at `OMLX_BASE_URL` or `http://127.0.0.1:8080/v1`. Implicit discovery accepts only canonical HTTP(S) loopback URLs without userinfo, query, or fragment; redirects are refused. It discovers models through `GET /v1/models`, accepts `OMLX_API_KEY` when the loopback server requires one, and otherwise uses keyless local auth. Built-in macOS oMLX profiles select the reviewed served model ids and preserve role-specific thinking effort.\n\n### Implicit SGLang discovery\n\nIf `sglang` is not explicitly configured, its bundled OpenAI-compatible descriptor discovers models at trusted `SGLANG_BASE_URL` or `http://127.0.0.1:30000/v1`. Credentialless implicit discovery accepts only HTTP(S) loopback origins, including normalized IPv4, IPv6, and IPv4-mapped loopback forms. Remote SGLang servers require an explicit provider configuration or a trusted base URL plus `SGLANG_API_KEY`; all endpoint URLs must omit userinfo, query, and fragment components, and project-local environment files cannot silently redirect implicit authenticated discovery.\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. Stored API key credential in `agent.db`\n3. Stored OAuth credential in `agent.db` (with refresh)\n4. Environment variable mapping (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.)\n5. 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 `SKC_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 (`SKC_AUTH_BROKER_URL`, `SKC_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\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 canonical-provider precedence)\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### Assigning a model to a detailed use\n\nThe role rows in `/model` open a second level. The first level keeps its seven\nactions in their original order; picking `executor`, `architect`, `planner` or\n`critic` then offers **General (whole role)** plus the detailed uses that role\ncan actually run:\n\n| Role | Detailed uses |\n| --- | --- |\n| `planner`, `architect` | `backendArchitecture`, `frontendDesign` |\n| `executor` | `implementation`, `testing` |\n| `critic` | `review` |\n\n`default` has no detailed uses and still assigns in one keystroke. The two bulk\nrows write canonical role models only — they never touch detailed-use settings,\nbecause a \"set everything\" action that also rewrote five overrides could not be\nundone from the same menu.\n\nDetailed-use assignments persist to `task.modelRouting.specialtyModels`, a\nrecord whose keys are restricted to the five ids above; anything else is\nrejected by config validation. A row shows the model it already holds, and\n**Clear detailed-use overrides** removes only the entries belonging to that role.\n\nA saved detailed-use model is live immediately for any spawn that **declares**\nthat work (below). Saving one while `task.modelRouting.enabled` is `false` still\npersists it and says so; that switch only governs auto-detection.\n\n### How a subagent's model is chosen\n\nThere are two ways a detailed-use model reaches a spawn. The first is\ndeterministic and needs no switch; the second is a guess and only fires once\nthere are tiers configured for it to choose between.\n\n**Declared.** The `task` tool accepts `.specialty` per task — one of the five\nids above. When the user assigned a model to that specialty, the child runs on\nit. No classifier is consulted, `task.modelRouting.enabled` is not read, and no\nconfidence bar applies. The child leaves that model only when it **errors**:\nthe child session's fallback chain retries `fallback.maxAttempts` times on a\ntransport failure (429, 5xx, auth, quota) and then advances to the role's own\nchain composed behind it. The menu groups specialties under the roles that\nusually do that work, but the setting is one flat map, and a declared specialty\nignores that grouping on purpose: a frontend build delegated to `executor` with\n`specialty: \"frontendDesign\"` runs on the frontend model.\n\n**Auto-detected.** Without a declaration, routing needs\n`task.modelRouting.enabled` (on by default) **and** somewhere to route to: at\nleast two of `task.modelRouting.fastModel` / `balancedModel` / `deepModel`, or a\ndetailed-use entry, or the legacy `task.modelRouting.frontendModel`. The tiers\nare empty until you set them, so the default-on switch is a no-op for an\nunconfigured install. A single tier is not an axis — there is nowhere to move\nfrom it. It also needs typed decisions (`decisions.enabled`, on by default) and\na cheap model for the classifier to run on.\n\nOne classification runs per **child task**, not per `task` call. A batch shares\nan agent but not a workload, so an implementation slice and a test slice in the\nsame call are classified separately.\n\nEither way the result is dispatched as an ordered fallback chain, not a single\nmodel:\n\n1. the detailed-use model — declared, or picked by the classifier for a role that can run it\n2. the tier model, when the difficulty axis moved (auto-detected only)\n3. the role's own configured chain\n\nThe tail is always the role baseline, so a detailed-use model that cannot\nauthenticate still lands on something the role can actually run. A high-risk\nauto-detected assignment skips the detailed-use axis entirely and composes tier\nplus baseline only, because a lateral swap can move sideways into something\nweaker.\n\nReceipts record what the router **requested** separately from what the spawn ran\non. When the backend is an ordinary LLM it reports no probabilities at all, so\nthe receipt carries an ordinal clarity score and `calibrated: false` instead of\na fabricated confidence.\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- `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 `reasoning_effort`. Default: auto (off for zAI; on for Grok and OpenAI-compatible hosts that advertise it).\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 the runtime uses a separate `AnthropicCompat` shape (`packages/ai/src/types.ts`). The `models.yml` schema currently exposes only the strict-tools opt-out as a top-level provider field (see below); the remaining Anthropic-side knobs (`disableAdaptiveThinking`, `supportsEagerToolInputStreaming`, `supportsLongCacheRetention`) are set by built-in catalog metadata and are not user-configurable from `models.yml`.\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 `~/.skc/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",
@@ -69,12 +69,12 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
69
69
  "prompt-architect-reports/tool-prompts.raw.md": "# ToolPrompts recovered raw report\n\nThe original `agent://0-ToolPrompts` result surfaced as failed, but inspecting the subagent JSONL context recovered 34 structured `report_finding` entries before the session died on stalls/429.\n\nCanonical recovered artifacts:\n\n- `recovered-context/0-ToolPrompts.recovered.md`\n- `recovered-context/0-ToolPrompts.findings.json`\n- `recovery-summary.md`\n\nRecovered severity breakdown: P1 = 4, P2 = 18, P3 = 12. No final `yield` or grade was emitted.\n",
70
70
  "provider-streaming-internals.md": "# Provider streaming internals\n\nThis document explains how token/tool streaming is normalized in `@sayknow-cli/ai`, then propagated through `@sayknow-cli/agent-core` and `coding-agent` session events.\n\n## End-to-end flow\n\n1. `streamSimple()` (`packages/ai/src/stream.ts`) maps generic options and dispatches to a provider stream function.\n2. Provider stream functions translate provider-native stream events into the unified `AssistantMessageEvent` sequence. Current built-ins include Anthropic, OpenAI Responses/Completions/OpenAI code/Azure Responses, Google Gemini/Gemini CLI/Vertex, Bedrock Converse, Ollama, Cursorand GitLab Duo/Kimi wrappers.\n3. Each provider pushes events into `AssistantMessageEventStream` (`packages/ai/src/utils/event-stream.ts`), which throttles delta events and exposes:\n - async iteration for incremental updates\n - `result()` for final `AssistantMessage`\n4. `agentLoop` (`packages/agent/src/agent-loop.ts`) consumes those events, mutates in-flight assistant state, and emits `message_update` events carrying the raw `assistantMessageEvent`.\n5. `AgentSession` (`packages/coding-agent/src/session/agent-session.ts`) subscribes to agent events, persists messages, and applies session behaviors (retry, compaction, TTSR, streaming-edit abort checks).\n\n## Unified stream contract in `@sayknow-cli/ai`\n\nAll providers emit the same shape (`AssistantMessageEvent` in `packages/ai/src/types.ts`):\n\n- `start`\n- content block lifecycle triplets:\n - text: `text_start` → `text_delta`\\* → `text_end`\n - thinking: `thinking_start` → `thinking_delta`\\* → `thinking_end`\n - tool call: `toolcall_start` → `toolcall_delta`\\* → `toolcall_end`\n- terminal event:\n - `done` with `reason: \"stop\" | \"length\" | \"toolUse\"`\n - or `error` with `reason: \"aborted\" | \"error\"`\n\n`AssistantMessageEventStream` guarantees:\n\n- final result is resolved by terminal event (`done` or `error`)\n- deltas are batched/throttled (~50ms)\n- buffered deltas are flushed before non-delta events and before completion\n\n## Delta throttling and harmonization behavior\n\n`AssistantMessageEventStream` treats `text_delta`, `thinking_delta`, and `toolcall_delta` as mergeable events:\n\n- buffered deltas are merged only when **type + contentIndex** match\n- merge keeps the latest `partial` snapshot\n- non-delta events force immediate flush\n\nThis smooths high-frequency provider streams for TUI/event consumers, but is not provider backpressure: providers still produce at full speed, while the local stream buffers.\n\n## Provider normalization details\n\n## Anthropic (`anthropic-messages`)\n\nSource: `packages/ai/src/providers/anthropic.ts`\n\nNormalization points:\n\n- `message_start` initializes usage (input/output/cache tokens)\n- `content_block_start` maps to text/thinking/toolcall starts\n- `content_block_delta` maps:\n - `text_delta` → `text_delta`\n - `thinking_delta` → `thinking_delta`\n - `input_json_delta` → `toolcall_delta`\n - `signature_delta` updates `thinkingSignature` only (no event)\n- `content_block_stop` emits corresponding `*_end`\n- `message_delta.stop_reason` maps via `mapStopReason()`\n\nTool-call argument streaming:\n\n- each tool block carries internal `partialJson`\n- every JSON delta appends to `partialJson`\n- `arguments` are reparsed on each delta via `parseStreamingJson()`\n- `toolcall_end` reparses once more, then strips `partialJson`\n\n## OpenAI Responses family (`openai-responses`, `openai-code-responses`, `azure-openai-responses`)\n\nSources: `packages/ai/src/providers/openai-responses.ts`, `openai-code-responses.ts`, and `azure-openai-responses.ts`\n\nNormalization points:\n\n- `response.output_item.added` starts reasoning/text/function-call blocks\n- reasoning summary events (`response.reasoning_summary_text.delta`) become `thinking_delta`\n- output/refusal deltas become `text_delta`\n- `response.function_call_arguments.delta` becomes `toolcall_delta`\n- `response.output_item.done` emits `thinking_end` / `text_end` / `toolcall_end`\n- `response.completed` maps status to stop reason and usage\n\nTool-call argument streaming:\n\n- same `partialJson` accumulation pattern as Anthropic\n- providers that send only `response.function_call_arguments.done` still populate final args\n- tool call IDs are normalized as `\"<call_id>|<item_id>\"`\n\n## Google Generative AI (`google-generative-ai`)\n\nSource: `packages/ai/src/providers/google.ts`\n\nNormalization points:\n\n- iterates `candidate.content.parts`\n- text parts are split into thinking vs text by `isThinkingPart(part)`\n- block transitions close previous block before starting a new one\n- `part.functionCall` is treated as a complete tool call (start/delta/end emitted immediately)\n- finish reason mapped by `mapStopReason()` from `google-shared.ts`\n\nTool-call argument streaming:\n\n- function call args arrive as structured object, not incremental JSON text\n- implementation emits one synthetic `toolcall_delta` containing `JSON.stringify(arguments)`\n- no partial JSON parser needed for Google in this path\n\n## Partial tool-call JSON accumulation and recovery\n\nShared behavior for Anthropic/OpenAI Responses uses `parseStreamingJson()` (`packages/ai/src/utils/json-parse.ts`):\n\n1. try `JSON.parse`\n2. fallback to `partial-json` parser for incomplete fragments\n3. if both fail, return `{}`\n\nImplications:\n\n- malformed or truncated argument deltas do not crash stream processing immediately\n- in-progress `arguments` may temporarily be `{}`\n- later valid deltas can recover structured arguments because parsing is retried on every append\n- final `toolcall_end` performs one more parse attempt before emission\n\n## Stop reasons vs transport/runtime errors\n\nProvider stop reasons are mapped to normalized `stopReason`:\n\n- Anthropic: `end_turn`→`stop`, `max_tokens`→`length`, `tool_use`→`toolUse`, safety/refusal cases→`error`\n- OpenAI Responses: `completed`→`stop`, `incomplete`→`length`, `failed/cancelled`→`error`\n- Google: `STOP`→`stop`, `MAX_TOKENS`→`length`, safety/prohibited/malformed-function-call classes→`error`\n\nError semantics are split in two stages:\n\n1. **Model completion semantics** (provider reported finish reason/status)\n2. **Transport/runtime failure** (network/client/parser/abort exceptions)\n\nIf provider stream throws or signals failure, each provider wrapper catches and emits terminal `error` event with:\n\n- `stopReason = \"aborted\"` when abort signal is set\n- otherwise `stopReason = \"error\"`\n- `errorMessage = formatErrorMessageWithRetryAfter(error)`\n\n## Malformed chunk / SSE parse failure behavior\n\nFor these provider paths, chunk/SSE framing is handled by vendor SDK streams (Anthropic SDK, OpenAI SDK, Google SDK). This code does not implement a custom SSE decoder here.\n\nObserved behavior in current implementation:\n\n- malformed chunk/SSE parsing at SDK level surfaces as an exception or stream `error` event\n- provider wrapper converts that into unified terminal `error` event\n- no provider-specific resume/retry inside the stream function itself\n- higher-level retries are handled in `AgentSession` auto-retry logic (message-level retry, not stream-chunk replay)\n\n## Cancellation boundaries\n\nCancellation is layered:\n\n- AI provider request: `options.signal` is passed into provider client stream call.\n- Provider wrapper: after stream loop, aborted signal forces error path (`\"Request was aborted\"`).\n- Agent loop: checks `signal.aborted` before handling each provider event and can synthesize an aborted assistant message from the latest partial.\n- Session/agent controls: `AgentSession.abort()` -> `agent.abort()` -> shared abort controller cancellation.\n\nTool execution cancellation is separate from model stream cancellation:\n\n- tool runners use `AbortSignal.any([agentSignal, steeringAbortSignal])`\n- steering interrupts can abort remaining tool execution while preserving already-produced tool results\n\n## Backpressure boundaries\n\nThere is no hard backpressure mechanism between provider SDK stream and downstream consumers:\n\n- `EventStream` uses in-memory queues with no max size\n- throttling reduces UI update rate but does not slow provider intake\n- if consumers lag significantly, queued events can grow until completion\n\nCurrent design favors responsiveness and simple ordering over bounded-buffer flow control.\n\n## How stream events surface as agent/session events\n\n`agentLoop.streamAssistantResponse()` bridges `AssistantMessageEvent` to `AgentEvent`:\n\n- on `start`: pushes placeholder assistant message and emits `message_start`\n- on block events (`text_*`, `thinking_*`, `toolcall_*`): updates last assistant message, emits `message_update` with raw `assistantMessageEvent`\n- on terminal (`done`/`error`): resolves final message from `response.result()`, emits `message_end`\n\n`AgentSession` then consumes those events for session-level behaviors:\n\n- TTSR watches `message_update.assistantMessageEvent` for `text_delta`, `thinking_delta`, and `toolcall_delta`\n- streaming edit guard inspects `toolcall_delta`/`toolcall_end` on `edit` calls and can abort early\n- persistence writes finalized messages at `message_end`\n- auto-retry examines assistant `stopReason === \"error\"` plus `errorMessage` heuristics\n\n## Unified vs provider-specific responsibilities\n\nUnified (common contract):\n\n- event shape (`AssistantMessageEvent`)\n- final result extraction (`done`/`error`)\n- delta throttling + merge rules\n- agent/session event propagation model\n\nProvider-specific (not fully abstracted):\n\n- upstream event taxonomies and mapping logic\n- stop-reason translation tables\n- tool-call ID conventions\n- reasoning/thinking block semantics and signatures\n- usage token semantics and availability timing\n- message conversion constraints per API\n\n## Implementation files\n\n- [`../../ai/src/stream.ts`](../packages/ai/src/stream.ts) — provider dispatch, option mapping, API key/session plumbing, custom API dispatch, and provider-specific credential handling.\n- [`../../ai/src/utils/event-stream.ts`](../packages/ai/src/utils/event-stream.ts) — generic stream queue + assistant delta throttling.\n- [`../../ai/src/utils/json-parse.ts`](../packages/ai/src/utils/json-parse.ts) — partial JSON parsing for streamed tool arguments.\n- [`../../ai/src/providers/anthropic.ts`](../packages/ai/src/providers/anthropic.ts) — Anthropic event translation and tool JSON delta accumulation.\n- [`../../ai/src/providers/openai-responses.ts`](../packages/ai/src/providers/openai-responses.ts), [`openai-code-responses.ts`](../packages/ai/src/providers/openai-code-responses.ts), [`azure-openai-responses.ts`](../packages/ai/src/providers/azure-openai-responses.ts) — Responses-family event translation and status mapping.\n- [`../../ai/src/providers/google.ts`](../packages/ai/src/providers/google.ts), [`google-gemini-cli.ts`](../packages/ai/src/providers/google-gemini-cli.ts), [`google-vertex.ts`](../packages/ai/src/providers/google-vertex.ts) — Gemini stream chunk-to-block translation variants.\n- [`../../ai/src/providers/google-shared.ts`](../packages/ai/src/providers/google-shared.ts) — Gemini finish-reason mapping and shared conversion rules.\n- [`../../ai/src/providers/amazon-bedrock.ts`](../packages/ai/src/providers/amazon-bedrock.ts), [`openai-completions.ts`](../packages/ai/src/providers/openai-completions.ts), [`ollama.ts`](../packages/ai/src/providers/ollama.ts), [`cursor.ts`](../packages/ai/src/providers/cursor.ts) — additional built-in stream adapters using the same event contract.\n- [`../../agent/src/agent-loop.ts`](../packages/agent/src/agent-loop.ts) — provider stream consumption and `message_update` bridging.\n- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — session-level handling of streaming updates, abort, retry, and persistence.\n",
71
71
  "python-repl.md": "# Eval Tool Python Backend\n\nThis document describes the Python execution stack in `packages/coding-agent`.\nIt covers tool behavior, runner lifecycle, environment handling, execution semantics, output rendering, supported magics, and operational failure modes.\n\n## Scope and Key Files\n\n- Tool surface: `src/tools/eval.ts`\n- Session/per-call kernel orchestration: `src/eval/py/executor.ts`\n- Subprocess kernel client: `src/eval/py/kernel.ts`\n- Python wrapper / NDJSON server: `src/eval/py/runner.py`\n- Prelude helpers loaded into every kernel: `src/eval/py/prelude.py`\n- MIME bundle renderer (text + structured outputs): `src/eval/py/display.ts`\n- Interactive-mode renderer for user-triggered Python runs: `src/modes/components/eval-execution.ts`\n- Runtime/env filtering and Python resolution: `src/eval/py/runtime.ts`\n\n## What eval's Python backend is\n\nThe `eval` tool executes one or more Python cells inside a long-lived `python3` subprocess that speaks NDJSON over stdin/stdout. No Jupyter, no kernel gateway, no extra pip dependencies — a vanilla Python 3.8+ interpreter is enough. Rich `display()` output (PIL, pandas, plotly, matplotlib figures) keeps working because the wrapper reimplements the MIME-bundle dispatch that IPython previously provided.\n\nTool params:\n\n```ts\n{\n cells: Array<{ code: string; title?: string }>;\n timeout?: number; // seconds, clamped to 1..600, default 30\n reset?: boolean; // reset selected runtime before the first cell only\n}\n```\n\nThe tool is `concurrency = \"exclusive\"` for a session, so calls do not overlap.\n\n## Kernel lifecycle\n\nEach kernel is a single Python subprocess: `python -u <runner.py>`. The bundled runner is materialized once per SKC process in a process-private temporary directory and file, then reused only by subsequent spawns within that process.\n\nKernel startup sequence:\n\n1. Availability check (`checkPythonKernelAvailability`) — verifies that a Python interpreter resolves and runs.\n2. Spawn `python -u runner.py` with filtered env and `cwd`.\n3. Send an init request that runs `os.chdir(cwd)`, injects env entries, and adds `cwd` to `sys.path`.\n4. Execute `PYTHON_PRELUDE` (idempotent — only initializes once per process).\n\nKernel shutdown:\n\n- Send `{\"type\": \"exit\"}` over stdin.\n- Wait for process exit with `SHUTDOWN_GRACE_MS` budget.\n- Escalate to `SIGTERM` and finally `SIGKILL` if the process does not exit in time.\n\n## Wire protocol (NDJSON, host ↔ runner)\n\nOne JSON object per line, UTF-8, `\\n` terminated.\n\nHost → runner:\n\n```jsonc\n{\"id\": \"<reqId>\", \"code\": \"<source>\", \"silent\": false, \"storeHistory\": true}\n{\"type\": \"exit\"}\n```\n\nRunner → host:\n\n```jsonc\n{\"type\": \"started\", \"id\": \"<reqId>\"}\n{\"type\": \"stdout\", \"id\": \"<reqId>\", \"data\": \"...\"}\n{\"type\": \"stderr\", \"id\": \"<reqId>\", \"data\": \"...\"}\n{\"type\": \"display\", \"id\": \"<reqId>\", \"bundle\": {<mime>: <value>}}\n{\"type\": \"result\", \"id\": \"<reqId>\", \"bundle\": {<mime>: <value>}}\n{\"type\": \"error\", \"id\": \"<reqId>\", \"ename\": \"...\", \"evalue\": \"...\", \"traceback\": [\"...\"]}\n{\"type\": \"done\", \"id\": \"<reqId>\", \"status\": \"ok\"|\"error\", \"executionCount\": N, \"cancelled\": false}\n```\n\nStatus events the prelude emits (e.g. `_emit_status(\"find\", count=…)`) ship inside display bundles under `application/x-skc-status` so the existing TUI status renderer keeps working.\n\n## Magics\n\nThe runner's source transformer rewrites IPython-style magics to plain Python calls before parsing. Supported set:\n\n| Magic | Effect |\n| --- | --- |\n| `%pip <args>` | `python -m pip <args>` with live streaming output. Newly installed packages are evicted from `sys.modules` so the next `import` picks up the fresh install. |\n| `%cd <path>` | `os.chdir(path)` (with `~` expansion); emits status event. |\n| `%pwd` | Returns `os.getcwd()`. |\n| `%ls [path]` | Returns `sorted(os.listdir(path))`. |\n| `%env [KEY[=VAL]]` | List, read, or set env vars (matches prelude `env()` semantics). |\n| `%set_env KEY VALUE` | Set `os.environ[KEY]`. |\n| `%time <expr>` / `%timeit <expr>` | Time the expression; emits status event with elapsed ms. |\n| `%who` / `%whos` | List user-namespace names. |\n| `%reset` | Clear user globals and re-inject prelude. |\n| `%load <path>` | Read a file into a fresh cell and execute. |\n| `%run <path>` | `runpy.run_path` and merge globals back. |\n| `%%bash` / `%%sh` | Run the cell body via `bash`/`sh`. |\n| `%%capture [name]` | Run body with stdout/stderr captured into `name`. |\n| `%%timeit` | Time the cell body. |\n| `%%writefile <path>` | Write body to file. |\n| `!cmd` / `var = !cmd` | Run command via subprocess shell; returns an SList-style result with `.n` / `.s` helpers. |\n| `var = %name args` | Assignment forms work for line magics and `!cmd`. |\n\nUnknown magic names raise `NameError: UsageError: ...` inside the cell.\n\n## Session persistence semantics\n\n`python.kernelMode` controls retained kernel reuse:\n\n- `session` (default)\n - Reuses kernel sessions keyed by session file plus cwd when a session file exists; otherwise by cwd.\n - Execution is serialized per session via a queue.\n - Idle sessions are evicted after 5 minutes.\n - At most 4 sessions; oldest is evicted on overflow.\n - Heartbeat checks detect dead kernels.\n - Auto-restart allowed once; repeated crash ⇒ hard failure.\n- `per-call`\n - Spawns a fresh subprocess for each request.\n - Shuts the subprocess down after the request.\n - No cross-call state persistence.\n\n### Multi-cell behavior in a single tool call\n\nCells run sequentially in the same kernel instance for that tool call.\n\nIf an intermediate cell fails:\n\n- Earlier cell state remains in memory.\n- Tool returns a targeted error indicating which cell failed.\n- Later cells are not executed.\n\n`reset=true` only applies to the first cell execution in that call.\n\n## Environment filtering and runtime resolution\n\nEnvironment is filtered before launching the runner:\n\n- Allowlist includes core vars like `PATH`, `HOME`, locale vars, `VIRTUAL_ENV`, `PYTHONPATH`, etc.\n- Allow-prefixes: `LC_`, `XDG_`, `SKC_`\n- Denylist strips common API keys (OpenAI/Anthropic/Gemini/etc.)\n\nRuntime selection order:\n\n1. Active/located venv (`VIRTUAL_ENV`, then `<cwd>/.venv`, `<cwd>/venv`)\n2. Managed venv at `~/.skc/python-env`\n3. `python` or `python3` on PATH\n\nWhen a venv is selected, its bin/Scripts path is prepended to `PATH`.\n\nThe runner additionally receives `PYTHONUNBUFFERED=1` and `PYTHONIOENCODING=utf-8` so streamed output reaches the host promptly.\n\n## Tool availability and mode selection\n\n`eval.py` / `eval.js` (both default `true`) plus optional `SKC_PY` override controls eval backend exposure:\n\n- Python backend only (`eval.py=true`, `eval.js=false`)\n- JavaScript backend only (`eval.py=false`, `eval.js=true`)\n- both backends\n\n`SKC_PY` accepted values:\n\n- `0` / `bash` → JavaScript backend only\n- `1` / `py` → Python backend only\n- `mix` / `both` → both backends\n\nIf Python preflight fails and `eval.js` is enabled, `eval` remains available and dispatches to JavaScript unless `language: \"python\"` is explicitly requested.\n\n## Execution flow and cancellation/timeout\n\n### Tool-level timeout\n\n`eval` timeout is in seconds, default 30, clamped to `1..600`. The tool combines caller abort signal and timeout signal with `AbortSignal.any(...)`.\n\n### Kernel execution cancellation\n\nOn abort/timeout:\n\n- The host sends `kill(\"SIGINT\")` to the runner subprocess.\n- The runner's exec-time signal handler raises `KeyboardInterrupt` inside the user code.\n- Result includes `cancelled=true`; timeout path annotates output as `Command timed out after <n> seconds`.\n- Between requests the runner installs `SIG_IGN` for SIGINT so a stray cancel does not tear down the kernel.\n\nIf a second cancel is required (runner stuck in C code), the host escalates to `SIGTERM` and the session restarts on the next call.\n\n### stdin behavior\n\nInteractive stdin is not supported. The runner does not forward `input()` prompts; user code that calls `input()` blocks until cancellation.\n\n## Output capture and rendering\n\n### Captured output classes\n\nFrom runner frames:\n\n- `stdout` / `stderr` → plain text chunks\n- `display` / `result` → rich display handling (MIME bundle)\n- `error` → traceback text\n- `application/x-skc-status` MIME inside `display` → structured status events\n\nDisplay MIME precedence:\n\n1. `text/markdown`\n2. `text/plain`\n3. `text/html` (converted to basic markdown)\n\nAdditionally captured as structured outputs:\n\n- `application/json` → JSON tree data\n- `image/png` / `image/jpeg` → image payloads\n- `application/x-skc-status` → status events\n\n### Matplotlib\n\nThe runner sets `MPLBACKEND=Agg` as an environ default so figures render off-screen. After every cell, `pyplot.get_fignums()` is iterated; each figure is saved to PNG, emitted as an `image/png` display, and closed.\n\n### Storage and truncation\n\nOutput is streamed through `OutputSink` and may be persisted to artifact storage. Tool results can include truncation metadata and `artifact://<id>` for full output recovery.\n\n### Renderer behavior\n\n- Tool renderer (`eval.ts`):\n - shows code-cell blocks with per-cell status\n - collapsed preview defaults to 10 lines\n - supports expanded mode for full output and richer status detail\n- Interactive renderer (`eval-execution.ts`):\n - used for user-triggered Python execution in TUI\n - collapsed preview defaults to 20 lines\n - clamps very long individual lines to 4000 chars for display safety\n - shows cancellation/error/truncation notices\n\n## Operational troubleshooting\n\n- **Python backend not available** — Check `eval.py`, `SKC_PY`, and that `python`/`python3` is on PATH. If preflight fails and `eval.js` is enabled, omit `language` or pass `language: \"js\"` to use JavaScript.\n- **No Python on PATH** — Install a system Python 3.8+ or place a venv at `~/.skc/python-env`. `skc setup python --check` reports the resolved interpreter.\n- **Execution hangs then times out** — Increase tool `timeout` (max 600s) if workload is legitimate. For stuck native code, cancellation triggers `SIGINT` first then escalates; the session restarts on the next request.\n- **stdin/input prompts in Python code** — `input()` is not supported; pass data programmatically.\n- **Working directory errors** — Tool validates `cwd` exists and is a directory before execution.\n\n## Relevant environment variables\n\n- `SKC_PY` — tool exposure override\n- `SKC_PYTHON_SKIP_CHECK=1` — bypass Python preflight/warm checks\n- `SKC_PYTHON_INTEGRATION=1` — enable gated integration tests that spawn a real Python\n- `SKC_PYTHON_IPC_TRACE=1` — log NDJSON frames exchanged with the runner subprocess\n",
72
- "readme/README.de.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI autonomous coding-agent hero illustration\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>Programmieren sollte sich wie Denken anfühlen.</strong><br />\n Ein fokussierter Coding-Agent-Runner für Interviews, geprüfte Pläne, tmux-native Ausführung und dauerhafte Verifizierung.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <b>Deutsch</b>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI character mascot\" width=\"320\" />\n</p>\n\n> Sayknow-CLI ist ein experimentelles Projekt im Beta-Stadium. Rechnen Sie mit Ecken und Kanten und überprüfen Sie die Ausgaben, bevor Sie sich bei wichtiger Arbeit darauf verlassen.\n\n## Languages\n\nDie Oberfläche ist in **7 Sprachen** lokalisiert — English, 한국어 (Koreanisch),\n中文 (简体 / Vereinfachtes Chinesisch), 日本語 (Japanisch), Español (Spanisch),\nFrançais (Französisch) und Deutsch. Beim ersten Start erkennt sie automatisch\nIhre System-Locale; wechseln Sie jederzeit unter **Settings → Appearance → Language**\noder starten Sie z. B. mit `LANG=ja_JP.UTF-8 skc`. Nicht übersetzte Zeichenketten\nfallen auf Englisch zurück, und Marken-/Fachbegriffe (Claude, OpenAI, MCP, …)\nbleiben in allen Locales unverändert.\n\n## Was ist Sayknow-CLI?\n\nSayknow-CLI (`skc`) ist ein externes Coding-Agent-Harness. Es läuft aus dem von Ihnen gewählten Repository oder Worktree und gibt dem Agenten dann eine kleine, explizite Workflow-Oberfläche:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nEs ist bewusst kein verstecktes Plugin für Codex CLI, Claude Code, OpenCode oder Claw Code. Starten Sie `skc` neben diesen Tools, wenn Sie strukturierte Planung, dauerhafte Nachweise, tmux-gestützte Worker oder einen isolierten Worktree wünschen.\n\n## Installation\n\n```sh\nnpm install -g sayknow-cli # oder: bun install -g sayknow-cli\nskc --version\n```\n\nDas Paket enthält vorgefertigte native Addons für macOS, Linux und Windows – keine Rust-Toolchain und kein Build-Schritt nötig. Aktualisieren: `npm install -g sayknow-cli@latest` oder `skc update` im Terminal.\n\n> Früher aus dem Quellcode (git clone) installiert? Einmalig umsteigen: `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`. Für die Installation aus dem Quellcode (Entwicklung) siehe die [englische README](../../README.md#install-from-source-development).\n\n## Schnellstart\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nVerwenden Sie innerhalb einer SKC-Sitzung die öffentliche Workflow-Oberfläche:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\nFügen Sie `skc team ...` nur hinzu, wenn koordinierte tmux-Worker spürbar helfen.\n\n## Kernfunktionen\n\n- **Interview vor dem Raten**: `deep-interview` verwandelt vage Anfragen in konkrete Anforderungen.\n- **Plan vor der Veränderung**: `ralplan` prüft den Ansatz vor Codeänderungen.\n- **Ausführen mit Nachweisen**: `ultragoal` verfolgt Ziele, Revisionen, Prüfungen und Abschlussnachweise.\n- **Parallelisieren, wenn sinnvoll**: `team` koordiniert tmux-gestützte Worker für größere Aufgaben.\n- **Extern und überprüfbar bleiben**: Laufen Sie aus einem gewählten Repo oder Worktree, ohne eine andere Agent-Runtime zu patchen.\n\n## Workflow-Oberfläche\n\nSayknow-CLI liefert vier Standard-Workflow-Skills:\n\n| Skill | Was es tut |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | Klärt mehrdeutige Anforderungen vor Planung oder Codeänderungen. |\n| `ralplan` | Erstellt und kritisiert einen Implementierungsplan vor der Veränderung. |\n| `ultragoal` | Verfolgt Ziele durch Ausführung, Revision, Verifizierung und Nachweise. |\n| `team` | Koordiniert tmux-gestützte Worker, wenn parallele Ausführung sich lohnt. |\n\nUnd vier mitgelieferte Rollen-Agenten:\n\n| Agent | Was es tut |\n| ----------- | -------------------------------------------------- |\n| `executor` | Begrenzte Implementierung, Fixes und Refactorings. |\n| `architect` | Schreibgeschützte Architektur- und Code-Review-Bewertung. |\n| `planner` | Schreibgeschützte Sequenzierung und Abnahmekriterien. |\n| `critic` | Schreibgeschützte Plan-Kritik und Umsetzbarkeitsprüfung. |\n\nKein wucherndes Standard-Skill-Zoo: SKC verbessert sich, indem es diese kleine Methode besser macht.\n\n## Funktioniert neben Ihrem bestehenden Agenten oder Bot\n\n| Tool oder Bot | Empfohlener SKC-Befehl | Grenze |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` oder `skc` | `--worktree` benennt einen SKC-verwalteten Geschwister-Worktree; für einen bestehenden Pfad wechseln Sie zuerst mit `cd` dorthin. |\n| Claude Code | `skc --tmux` oder `skc --tmux --worktree <name>` | SKC wird keine Claude-Code-Erweiterung. |\n| OpenCode | `skc` oder `skc --tmux` | Heute nur External-Runner-Workflow. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC installiert sich nicht in Claw Code und ersetzt es nicht. |\n| Externer Controller / Bot | `skc mcp-serve coordinator` plus `skc setup hermes` für kompatible Konfiguration oder `skc --mode rpc` für einen Subprozess-Worker | Jeder MCP-/RPC-fähige Bot steuert SKC über den generischen Coordinator-/RPC-Vertrag, nicht durch Scrollback-Scraping. |\n\nFür generisches Drittanbieter-Bot-Setup und anbieterunabhängige Smokes siehe [`docs/bot-integration.md`](docs/bot-integration.md). Für die Reife-Klassifizierung über MCP-, RPC-, ACP- und Bridge/HTTPS-Oberflächen siehe [`docs/external-control-readiness.md`](docs/external-control-readiness.md). Für tiefergehende Protokolldetails siehe [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md) und [`docs/bridge.md`](docs/bridge.md). Für die Roadmap der Remote-Operator-Oberflächen siehe [`docs/sayknow-remote.md`](docs/sayknow-remote.md) (Web-Steuerrad) und [`docs/telegram-remote.md`](docs/telegram-remote.md) (Telegram-Lifecycle-Button).\n\n## Konfiguration\n\nProvider-Retry-Budgets liegen in `~/.skc/config.yml`:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` gilt, bevor ein Stream aufgebaut wird. `streamMaxRetries` gilt nur für replay-sichere, vorübergehende Stream-Fehler. Ungültige Authentifizierung, nicht unterstützte Modelle/Provider, fehlerhafte Requests, Kontextüberlauf, Benutzerabbrüche und dauerhafte Kontingentfehler bleiben fail-fast.\n\n## TUI-Identität\n\nDie Standard-TUI-Identität ist das SKC-**blue-octopus**-Theme — das blaue Kopffüßer-Maskottchen — sowohl für dunkle als auch für helle Terminals. Eine warme **red-octopus**-Variante ist ebenfalls dabei für alle, die eine dunklere, kontrastreiche Palette bevorzugen. Drei zusätzliche Migrations-Themes — `claude-code`, `codex` und `opencode` — spiegeln das Aussehen dieser Tools für einen einfachen Augen-Umstieg wider und sind über Settings oder `/theme` auswählbar. Explizite Benutzer-Theme-Einstellungen gewinnen weiterhin.\n\n### Raster der mitgelieferten Themes\n\nWählen Sie über Settings (`Appearance -> Dark theme` / `Light theme`) oder `/theme`.\n\n| Theme | Visueller Eindruck | Beste Eignung |\n| --- | --- | --- |\n| `blue-octopus` | Standard-SKC-Identität — blaue Oktopus-Palette mit tentakelblauen Akzenten. | Standard für dunkle und helle Terminals. |\n| `red-octopus` | Warme rote Oktopus-Variante mit starkem Status-Kontrast. | Kontrastreiche dunkle Alternative. |\n| `claude-code` | Von Claude Code inspirierte dunkle Palette mit terrakotta- und pinkfarbenen Highlights. | Claude-Code-Muskelgedächtnis, ohne SKC zu verlassen. |\n| `codex` | Klare dunkle blaugraue Palette mit schärferem Coding-Session-Kontrast. | Ein Codex-ähnlicher dunkler Arbeitsbereich. |\n| `opencode` | Von OpenCode inspirierte dunkle Palette mit kräftigeren Terminal-Akzenten. | OpenCode-Muskelgedächtnis im mitgelieferten Picker. |\n\n## Entwicklung\n\nAbhängigkeiten installieren, native Bindings bauen und lokale Standardwerte einrichten:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\nDie `.node`-Binärdatei für `@sayknow-cli/natives` ist gitignored und vor jeder CLI-Ausführung erforderlich (`install:defaults`, `dev:link`, Tests).\n\n### Kanonisch: Entwickler-`skc` bauen und verlinken\n\nDamit der globale Befehl `skc` **den TypeScript-Quellcode dieses Checkouts** ausführt (live bei jeder Bearbeitung, mit funktionierenden Skills/Natives), verlinken Sie ihn in Ihren `PATH`:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` legt einen Symlink `skc` → `packages/coding-agent/src/cli.ts` nach `~/.local/bin` an (überschreibbar mit `SKC_DEV_LINK_DIR`), ersetzt dieses verwaltete Ziel, warnt und schlägt fehl, falls ein anderes `skc` es weiter vorne im `PATH` überschattet, und führt `--smoke-test` aus, um zu bestätigen, dass `@sayknow-cli/natives` geladen wird. Verwenden Sie `bun run install:dev` für das vollständige Bootstrap (Installation + Link + `setup defaults`).\n\nPrüfen Sie jederzeit, ob Ihr `skc` abgedriftet ist (falsche Quelle oder eine kompilierte Binärdatei, die keine Skills laden kann):\n\n```sh\nbun run dev:doctor\n```\n\n> Verwenden Sie für die tägliche Entwicklung **nicht** die kompilierte Binärdatei. `bun --cwd=packages/coding-agent run build` erzeugt ein eigenständiges `dist/skc`, aber eine mit `bun build --compile` erstellte Binärdatei kann `@sayknow-cli/natives` nicht dynamisch laden, sodass Skills mit `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'` fehlschlagen. Die Ausführung aus dem Quellcode über `dev:link` vermeidet dies. Bauen Sie die Binärdatei nur, wenn Sie ein Release validieren.\n\nFühren Sie die CLI direkt aus dem Quellcode ohne Verlinkung aus:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nStandard-Workflow-Definitionen liegen im Quellcode, nicht in committeten `.skc`-Kopien:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nFür Änderungen an Workflow-Definitionen oder Rebrand-Oberflächen führen Sie die Projekt-Gates aus:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nFür eine Paket-für-Paket-Übersicht siehe [`docs/codebase-overview.md`](docs/codebase-overview.md).\n\n## Mitwirkende\n\nBeiträge, Fehlerberichte und Release-Validierung sind über GitHub Issues und Pull Requests willkommen.\n\n## Inspirationen und Herkunft\n\nDie Standard-TUI-Identität von Sayknow-CLI ist das Kopffüßer-Paar: blue-octopus als Standard mit einem warmen red-octopus als Alternative. Es liefert außerdem die Migrations-Themes `claude-code`, `codex` und `opencode`, deren Paletten von diesen Tools inspiriert sind, damit Benutzer, die von ihnen wechseln, einen vertrauten Look erhalten. Es baut auf Erkenntnissen aus einer kleinen Familie von Agent-Harnesses auf und hält die öffentliche SKC-Oberfläche bewusst fokussiert. Die historische Zuordnung wird in [`NOTICE.md`](NOTICE.md) geführt.\n\n## Lizenz\n\nMIT. Siehe [`LICENSE`](LICENSE).\n",
73
- "readme/README.es.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Ilustración principal del agente de codificación autónomo Sayknow-CLI\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>Programar debería sentirse como pensar.</strong><br />\n Un ejecutor de agentes de codificación enfocado en entrevistas, planes revisados, ejecución nativa en tmux y verificación duradera.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <b>Español</b> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Mascota personaje de Sayknow-CLI\" width=\"320\" />\n</p>\n\n> Sayknow-CLI es un proyecto experimental en fase beta. Espera asperezas y verifica los resultados antes de confiar en él para trabajos importantes.\n\n## Languages\n\nLa interfaz está localizada en **7 idiomas** — English, 한국어 (coreano),\n中文 (简体 / chino simplificado), 日本語 (japonés), Español,\nFrançais (francés) y Deutsch (alemán). Detecta automáticamente la configuración regional de tu sistema en\nel primer arranque; cámbiala en cualquier momento en **Settings → Appearance → Language**, o inícialo\ncon, por ejemplo, `LANG=ja_JP.UTF-8 skc`. Las cadenas no traducidas recurren al inglés, y\nlos nombres de marca/técnicos (Claude, OpenAI, MCP, …) se mantienen literales en todas las configuraciones regionales.\n\n## ¿Qué es Sayknow-CLI?\n\nSayknow-CLI (`skc`) es un arnés externo de agentes de codificación. Se ejecuta desde el repositorio o worktree que elijas y luego le da al agente una superficie de flujo de trabajo pequeña y explícita:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nIntencionadamente no es un plugin oculto para Codex CLI, Claude Code, OpenCode o Claw Code. Inicia `skc` junto a esas herramientas cuando quieras planificación estructurada, evidencia persistente, workers respaldados por tmux o un worktree aislado.\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # o: bun install -g sayknow-cli\nskc --version\n```\n\nEl paquete incluye binarios nativos precompilados para macOS, Linux y Windows, así que no necesitas Rust ni paso de compilación. Para actualizar: `npm install -g sayknow-cli@latest` o ejecuta `skc update` en la terminal.\n\n> ¿Vienes de una instalación desde el código fuente (git clone)? Cambia una sola vez: `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`. Para la instalación desde el código (desarrollo), consulta el [README en inglés](../../README.md#install-from-source-development).\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nDentro de una sesión de SKC, usa la superficie pública del flujo de trabajo:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\nAñade `skc team ...` solo cuando los workers coordinados de tmux ayuden de forma significativa.\n\n## Capacidades principales\n\n- **Entrevistar antes de suponer**: `deep-interview` convierte solicitudes vagas en requisitos concretos.\n- **Planificar antes de mutar**: `ralplan` revisa el enfoque antes de los cambios de código.\n- **Ejecutar con evidencia**: `ultragoal` rastrea objetivos, revisiones, comprobaciones y evidencia de finalización.\n- **Paralelizar cuando sea útil**: `team` coordina workers respaldados por tmux para tareas más grandes.\n- **Mantenerse externo y revisable**: ejecútalo desde un repositorio o worktree elegido sin parchear otro runtime de agente.\n\n## Superficie del flujo de trabajo\n\nSayknow-CLI incluye cuatro skills de flujo de trabajo predeterminadas:\n\n| Skill | Qué hace |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | Aclara requisitos ambiguos antes de planificar o cambiar código. |\n| `ralplan` | Construye y critica un plan de implementación antes de mutar. |\n| `ultragoal` | Rastrea objetivos a través de ejecución, revisión, verificación y evidencia. |\n| `team` | Coordina workers respaldados por tmux cuando vale la pena la ejecución paralela. |\n\nY cuatro agentes de rol incluidos:\n\n| Agent | Qué hace |\n| ----------- | -------------------------------------------------- |\n| `executor` | Implementación acotada, correcciones y refactorizaciones. |\n| `architect` | Evaluación de arquitectura y revisión de código de solo lectura. |\n| `planner` | Secuenciación y criterios de aceptación de solo lectura. |\n| `critic` | Crítica de planes y revisión de accionabilidad de solo lectura. |\n\nSin un zoológico de skills predeterminadas desbordante: SKC mejora haciendo mejor este pequeño método.\n\n## Funciona junto a tu agente o bot existente\n\n| Herramienta o bot | Comando SKC recomendado | Límite |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` o `skc` | `--worktree` nombra un worktree hermano gestionado por SKC; para una ruta existente, haz `cd` allí primero. |\n| Claude Code | `skc --tmux` o `skc --tmux --worktree <name>` | SKC no se convierte en una extensión de Claude Code. |\n| OpenCode | `skc` o `skc --tmux` | Solo flujo de trabajo de ejecutor externo por ahora. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC no se instala dentro de Claw Code ni lo reemplaza. |\n| Controlador / bot externo | `skc mcp-serve coordinator` más `skc setup hermes` para una configuración compatible, o `skc --mode rpc` para un worker en subproceso | Cualquier bot con capacidad MCP/RPC controla SKC mediante el contrato genérico coordinator/RPC, no mediante scraping del scrollback. |\n\nPara la configuración genérica de bots de terceros y pruebas de humo independientes del proveedor, consulta [`docs/bot-integration.md`](docs/bot-integration.md). Para la clasificación de preparación a través de las superficies MCP, RPC, ACP y Bridge/HTTPS, consulta [`docs/external-control-readiness.md`](docs/external-control-readiness.md). Para los detalles de protocolo de más bajo nivel, consulta [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md) y [`docs/bridge.md`](docs/bridge.md). Para la hoja de ruta de las superficies de operador remoto, consulta [`docs/sayknow-remote.md`](docs/sayknow-remote.md) (volante web) y [`docs/telegram-remote.md`](docs/telegram-remote.md) (botón de ciclo de vida de Telegram).\n\n## Configuration\n\nLos presupuestos de reintento del proveedor viven en `~/.skc/config.yml`:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` se aplica antes de que se establezca un stream. `streamMaxRetries` se aplica solo a fallos transitorios de stream que son seguros de reproducir. La autenticación inválida, los modelos/proveedores no compatibles, las solicitudes malformadas, el desbordamiento de contexto, las cancelaciones del usuario y los fallos permanentes de cuota siguen siendo de fallo rápido.\n\n## Identidad de la TUI\n\nLa identidad predeterminada de la TUI es el tema **blue-octopus** de SKC — la mascota del cefalópodo azul — tanto para terminales oscuras como claras. También se incluye una variante cálida **red-octopus** para quienes prefieren una paleta más oscura y de alto contraste. Tres temas de migración adicionales — `claude-code`, `codex` y `opencode` — reflejan el aspecto de esas herramientas para facilitar la migración visual y se pueden seleccionar desde Settings o `/theme`. Los ajustes de tema explícitos del usuario siguen prevaleciendo.\n\n### Cuadrícula de temas incluidos\n\nElige desde Settings (`Appearance -> Dark theme` / `Light theme`) o `/theme`.\n\n| Tema | Sensación visual | Mejor uso |\n| --- | --- | --- |\n| `blue-octopus` | Identidad predeterminada de SKC — paleta de pulpo azul con acentos azul-tentáculo. | Predeterminado para terminales oscuras y claras. |\n| `red-octopus` | Variante cálida de pulpo rojo con fuerte contraste de estado. | Alternativa oscura de alto contraste. |\n| `claude-code` | Paleta oscura inspirada en Claude Code con resaltados terracota y rosa. | Memoria muscular de Claude Code sin salir de SKC. |\n| `codex` | Paleta nítida azul-gris oscuro con un contraste de sesión de codificación más marcado. | Un espacio de trabajo oscuro al estilo Codex. |\n| `opencode` | Paleta oscura inspirada en OpenCode con acentos de terminal más vibrantes. | Memoria muscular de OpenCode en el selector incluido. |\n\n## Development\n\nInstala las dependencias, compila los bindings nativos y configura los valores predeterminados locales:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\nEl binario `.node` para `@sayknow-cli/natives` está en gitignore y es necesario antes de cualquier invocación del CLI (`install:defaults`, `dev:link`, tests).\n\n### Canónico: compilar y enlazar el `skc` de desarrollo\n\nPara hacer que el comando global `skc` ejecute **el código fuente TypeScript de esta copia** (sensible a cada edición, con skills/natives funcionando), enlázalo a tu `PATH`:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` crea un symlink de `skc` → `packages/coding-agent/src/cli.ts` en `~/.local/bin` (sobrescríbelo con `SKC_DEV_LINK_DIR`), reemplaza ese objetivo gestionado, advierte y falla si otro `skc` aún lo oculta antes en `PATH`, y ejecuta `--smoke-test` para confirmar que `@sayknow-cli/natives` carga. Usa `bun run install:dev` para el bootstrap completo (install + link + `setup defaults`).\n\nComprueba en cualquier momento si tu `skc` se ha desviado (fuente incorrecta, o un binario compilado que no puede cargar skills):\n\n```sh\nbun run dev:doctor\n```\n\n> **No** uses el binario compilado para el desarrollo diario. `bun --cwd=packages/coding-agent run build` produce un `dist/skc` independiente, pero un binario `bun build --compile` no puede cargar dinámicamente `@sayknow-cli/natives`, por lo que las skills fallan con `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'`. Ejecutar desde el código fuente mediante `dev:link` evita esto. Compila el binario solo al validar una release.\n\nEjecuta el CLI directamente desde el código fuente sin enlazarlo:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nLas definiciones de flujo de trabajo predeterminadas viven en el código fuente, no en copias `.skc` comprometidas:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nPara cambios en las definiciones de flujo de trabajo o en la superficie de rebranding, ejecuta las puertas del proyecto:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nPara un mapa paquete por paquete, consulta [`docs/codebase-overview.md`](docs/codebase-overview.md).\n\n## Contributors\n\nLas contribuciones, los informes de errores y la validación de releases son bienvenidos a través de GitHub Issues y Pull Requests.\n\n## Inspiraciones y linaje\n\nLa identidad predeterminada de la TUI de Sayknow-CLI es la pareja de cefalópodos: blue-octopus como predeterminado con un red-octopus cálido como alternativa. También incluye los temas de migración `claude-code`, `codex` y `opencode`, cuyas paletas están inspiradas en esas herramientas para que los usuarios que migran de ellas obtengan un aspecto familiar. Se basa en las lecciones de una pequeña familia de arneses de agentes mientras mantiene la superficie pública de SKC intencionadamente enfocada. La atribución histórica se conserva en [`NOTICE.md`](NOTICE.md).\n\n## License\n\nMIT. Consulta [`LICENSE`](LICENSE).\n",
74
- "readme/README.fr.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Illustration héros de l'agent de codage autonome Sayknow-CLI\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>Coder devrait ressembler à réfléchir.</strong><br />\n Un exécuteur d'agent de codage ciblé pour les entretiens, les plans révisés, l'exécution native tmux et la vérification durable.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <b>Français</b> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Mascotte personnage de Sayknow-CLI\" width=\"320\" />\n</p>\n\n> Sayknow-CLI est un projet expérimental en phase bêta. Attendez-vous à des aspérités et vérifiez les résultats avant de vous y fier pour un travail important.\n\n## Languages\n\nL'interface est localisée en **7 langues** — English, 한국어 (coréen),\n中文 (简体 / chinois simplifié), 日本語 (japonais), Español (espagnol),\nFrançais (français) et Deutsch (allemand). Elle détecte automatiquement la locale de votre système au\npremier lancement ; changez-en à tout moment dans **Settings → Appearance → Language**, ou lancez\navec par exemple `LANG=ja_JP.UTF-8 skc`. Les chaînes non traduites se rabattent sur l'anglais, et\nles noms de marque/techniques (Claude, OpenAI, MCP, …) restent verbatim dans toutes les locales.\n\n## What is Sayknow-CLI?\n\nSayknow-CLI (`skc`) est un harnais d'agent de codage externe. Il s'exécute depuis le dépôt ou le worktree que vous choisissez, puis donne à l'agent une surface de workflow réduite et explicite :\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nCe n'est volontairement pas un plugin caché pour Codex CLI, Claude Code, OpenCode ou Claw Code. Lancez `skc` à côté de ces outils lorsque vous voulez une planification structurée, des preuves persistantes, des workers adossés à tmux, ou un worktree isolé.\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # ou : bun install -g sayknow-cli\nskc --version\n```\n\nLe paquet embarque des binaires natifs précompilés pour macOS, Linux et Windows : aucune chaîne d'outils Rust ni étape de compilation. Pour mettre à jour : `npm install -g sayknow-cli@latest` ou lancez `skc update` dans le terminal.\n\n> Vous veniez d'une installation depuis les sources (git clone) ? Basculez une seule fois : `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`. Pour l'installation depuis les sources (développement), voir le [README en anglais](../../README.md#install-from-source-development).\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nÀ l'intérieur d'une session SKC, utilisez la surface de workflow publique :\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\nAjoutez `skc team ...` uniquement lorsque des workers tmux coordonnés aident concrètement.\n\n## Core capabilities\n\n- **Interviewer avant de deviner** : `deep-interview` transforme des demandes vagues en exigences concrètes.\n- **Planifier avant de muter** : `ralplan` révise l'approche avant les changements de code.\n- **Exécuter avec des preuves** : `ultragoal` suit les objectifs, les révisions, les vérifications et les preuves de complétion.\n- **Paralléliser quand c'est utile** : `team` coordonne des workers adossés à tmux pour les tâches plus importantes.\n- **Rester externe et révisable** : exécutez depuis un dépôt ou un worktree choisi sans patcher un autre runtime d'agent.\n\n## Workflow surface\n\nSayknow-CLI fournit quatre skills de workflow par défaut :\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | Clarifie les exigences ambiguës avant la planification ou les changements de code. |\n| `ralplan` | Construit et critique un plan d'implémentation avant la mutation. |\n| `ultragoal` | Suit les objectifs à travers l'exécution, la révision, la vérification et les preuves. |\n| `team` | Coordonne des workers adossés à tmux lorsque l'exécution parallèle en vaut la peine. |\n\nEt quatre agents de rôle inclus :\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | Implémentation bornée, correctifs et refactorisations. |\n| `architect` | Évaluation d'architecture et de revue de code en lecture seule. |\n| `planner` | Séquençage et critères d'acceptation en lecture seule. |\n| `critic` | Critique de plan et revue d'actionnabilité en lecture seule. |\n\nPas de ménagerie tentaculaire de skills par défaut : SKC s'améliore en rendant cette petite méthode meilleure.\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree` nomme un worktree frère géré par SKC ; pour un chemin existant, faites d'abord `cd` à cet endroit. |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC ne devient pas une extension de Claude Code. |\n| OpenCode | `skc` or `skc --tmux` | Workflow d'exécuteur externe uniquement aujourd'hui. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC ne s'installe pas dans Claw Code et ne le remplace pas. |\n| External controller / bot | `skc mcp-serve coordinator` plus `skc setup hermes` for compatible config, or `skc --mode rpc` for a subprocess worker | Tout bot capable de MCP/RPC pilote SKC via le contrat générique coordinator/RPC, et non par grattage de scrollback. |\n\nPour la configuration générique d'un bot tiers et les smokes indépendants du provider, voir [`docs/bot-integration.md`](docs/bot-integration.md). Pour la classification de readiness à travers les surfaces MCP, RPC, ACP et Bridge/HTTPS, voir [`docs/external-control-readiness.md`](docs/external-control-readiness.md). Pour les détails de protocole de plus bas niveau, voir [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md) et [`docs/bridge.md`](docs/bridge.md). Pour la roadmap des surfaces d'opérateur distant, voir [`docs/sayknow-remote.md`](docs/sayknow-remote.md) (volant de direction web) et [`docs/telegram-remote.md`](docs/telegram-remote.md) (bouton de cycle de vie Telegram).\n\n## Configuration\n\nLes budgets de retry du provider se trouvent dans `~/.skc/config.yml` :\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` s'applique avant qu'un stream ne soit établi. `streamMaxRetries` ne s'applique qu'aux échecs de stream transitoires sûrs pour le replay. L'authentification invalide, les modèles/providers non pris en charge, les requêtes malformées, le débordement de contexte, les abandons par l'utilisateur et les échecs de quota permanents restent en fail-fast.\n\n## TUI identity\n\nL'identité TUI par défaut est le thème SKC **blue-octopus** — la mascotte céphalopode bleue — pour les terminaux sombres comme clairs. Une variante chaleureuse **red-octopus** est également incluse pour ceux qui préfèrent une palette plus sombre et à fort contraste. Trois thèmes de migration supplémentaires — `claude-code`, `codex` et `opencode` — reflètent l'apparence de ces outils pour faciliter la migration visuelle et sont sélectionnables depuis Settings ou `/theme`. Les réglages de thème explicites de l'utilisateur l'emportent toujours.\n\n### Bundled theme grid\n\nChoisissez depuis Settings (`Appearance -> Dark theme` / `Light theme`) ou `/theme`.\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | Identité SKC par défaut — palette poulpe bleu avec des accents bleu tentacule. | Par défaut pour les terminaux sombres et clairs. |\n| `red-octopus` | Variante chaleureuse poulpe rouge avec un fort contraste d'état. | Alternative sombre à fort contraste. |\n| `claude-code` | Palette sombre inspirée de Claude Code avec des touches terracotta et rose. | La mémoire musculaire de Claude Code sans quitter SKC. |\n| `codex` | Palette bleu-gris sombre et nette avec un contraste de session de codage plus marqué. | Un espace de travail sombre à la manière de Codex. |\n| `opencode` | Palette sombre inspirée d'OpenCode avec des accents de terminal plus percutants. | La mémoire musculaire d'OpenCode dans le sélecteur inclus. |\n\n## Development\n\nInstallez les dépendances, compilez les bindings natifs et configurez les valeurs par défaut locales :\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\nLe binaire `.node` pour `@sayknow-cli/natives` est gitignored et requis avant toute invocation de la CLI (`install:defaults`, `dev:link`, tests).\n\n### Canonical: build and link the dev `skc`\n\nPour que la commande globale `skc` exécute **la source TypeScript de ce checkout** (sensible à chaque édition, avec skills/natives fonctionnels), liez-la à votre `PATH` :\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` crée un lien symbolique `skc` → `packages/coding-agent/src/cli.ts` dans `~/.local/bin` (à surcharger avec `SKC_DEV_LINK_DIR`), remplace cette cible gérée, avertit et échoue si un autre `skc` le masque encore plus tôt sur le `PATH`, et exécute `--smoke-test` pour confirmer que `@sayknow-cli/natives` se charge. Utilisez `bun run install:dev` pour le bootstrap complet (install + link + `setup defaults`).\n\nVérifiez à tout moment si votre `skc` a dérivé (mauvaise source, ou un binaire compilé qui ne peut pas charger les skills) :\n\n```sh\nbun run dev:doctor\n```\n\n> N'utilisez **pas** le binaire compilé pour le développement quotidien. `bun --cwd=packages/coding-agent run build` produit un `dist/skc` autonome, mais un binaire `bun build --compile` ne peut pas charger dynamiquement `@sayknow-cli/natives`, donc les skills échouent avec `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'`. L'exécution depuis la source via `dev:link` évite cela. Ne compilez le binaire que lors de la validation d'une release.\n\nExécutez la CLI depuis la source directement sans lier :\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nLes définitions de workflow par défaut résident dans la source, et non dans des copies `.skc` commitées :\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nPour les changements de définition de workflow ou de surface de rebrand, exécutez les portes du projet :\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nPour une carte package par package, voir [`docs/codebase-overview.md`](docs/codebase-overview.md).\n\n## Contributors\n\nLes contributions, les rapports de bugs et la validation de release sont les bienvenus via les GitHub Issues et les Pull Requests.\n\n## Inspirations and lineage\n\nL'identité TUI par défaut de Sayknow-CLI est la paire de céphalopodes : blue-octopus comme valeur par défaut avec une alternative chaleureuse red-octopus. Il inclut aussi les thèmes de migration `claude-code`, `codex` et `opencode` dont les palettes sont inspirées de ces outils afin que les utilisateurs qui en proviennent retrouvent une apparence familière. Il s'appuie sur les leçons d'une petite famille de harnais d'agents tout en gardant la surface publique SKC volontairement ciblée. L'attribution historique est conservée dans [`NOTICE.md`](NOTICE.md).\n\n## License\n\nMIT. Voir [`LICENSE`](LICENSE).\n",
75
- "readme/README.ja.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI 自律型コーディングエージェントのヒーローイラスト\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>コーディングは、考えることのように感じられるべきだ。</strong><br />\n インタビュー、レビュー済みプラン、tmux ネイティブ実行、そして永続的な検証のための、集中型コーディングエージェントランナー。\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <b>日本語</b> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI キャラクターマスコット\" width=\"320\" />\n</p>\n\n> Sayknow-CLI は実験的なベータ段階のプロジェクトです。粗削りな部分があることを想定し、重要な作業で頼る前には出力を検証してください。\n\n## Languages\n\nインターフェースは **7 言語** にローカライズされています — English、한국어 (韓国語)、\n中文 (简体 / 簡体字中国語)、日本語 (Japanese)、Español (スペイン語)、\nFrançais (フランス語)、そして Deutsch (ドイツ語)。初回起動時にシステムロケールを\n自動検出します。**Settings → Appearance → Language** でいつでも切り替えられるほか、\nたとえば `LANG=ja_JP.UTF-8 skc` のように起動することもできます。未翻訳の文字列は英語に\nフォールバックし、ブランド名や技術名 (Claude、OpenAI、MCP、…) はすべてのロケールで\nそのまま表示されます。\n\n## Sayknow-CLI とは?\n\nSayknow-CLI (`skc`) は外部コーディングエージェントのハーネスです。選択したリポジトリまたは worktree から実行され、エージェントに対して小さく明示的なワークフロー面を提供します:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nこれは意図的に、Codex CLI、Claude Code、OpenCode、Claw Code 向けの隠しプラグインにはなっていません。構造化されたプランニング、永続的なエビデンス、tmux ベースのワーカー、または分離された worktree が欲しいときに、それらのツールと並べて `skc` を起動してください。\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # または: bun install -g sayknow-cli\nskc --version\n```\n\nmacOS・Linux・Windows 向けのビルド済みネイティブを同梱しているため、Rust ツールチェーンやビルド手順は不要です。更新は `npm install -g sayknow-cli@latest`、またはターミナルで `skc update`。\n\n> 以前ソース(git clone)からインストールした場合は、一度だけ `rm -f ~/.local/bin/skc && npm install -g sayknow-cli` で切り替えてください。ソース/開発インストールは[英語版 README](../../README.md#install-from-source-development)を参照。\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nSKC セッション内では、公開されているワークフロー面を使用してください:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\n`skc team ...` は、協調する tmux ワーカーが実質的に役立つときにのみ追加してください。\n\n## Core capabilities\n\n- **推測する前にインタビューする**: `deep-interview` は曖昧なリクエストを具体的な要件に変えます。\n- **変更する前にプランニングする**: `ralplan` はコード変更の前にアプローチをレビューします。\n- **エビデンスとともに実行する**: `ultragoal` はゴール、リビジョン、チェック、完了エビデンスを追跡します。\n- **役立つときに並列化する**: `team` はより大きなタスクのために tmux ベースのワーカーを協調させます。\n- **外部かつレビュー可能であり続ける**: 別のエージェントランタイムにパッチを当てることなく、選択したリポジトリまたは worktree から実行します。\n\n## Workflow surface\n\nSayknow-CLI は 4 つのデフォルトワークフロースキルを同梱しています:\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | プランニングやコード変更の前に、曖昧な要件を明確化します。 |\n| `ralplan` | 変更の前に実装プランを構築し批評します。 |\n| `ultragoal` | 実行、リビジョン、検証、エビデンスを通じてゴールを追跡します。 |\n| `team` | 並列実行に価値があるときに tmux ベースのワーカーを協調させます。 |\n\nそして 4 つの同梱ロールエージェント:\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | 範囲を限定した実装、修正、リファクタリング。 |\n| `architect` | 読み取り専用のアーキテクチャおよびコードレビュー評価。 |\n| `planner` | 読み取り専用のシーケンシングと受け入れ基準。 |\n| `critic` | 読み取り専用のプラン批評と実行可能性レビュー。 |\n\n肥大化したデフォルトスキルの動物園はありません: SKC はこの小さなメソッドをより良くすることで改善されます。\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree` は SKC が管理する兄弟 worktree に名前を付けます。既存のパスを使う場合は、まずそこへ `cd` してください。 |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC は Claude Code の拡張機能にはなりません。 |\n| OpenCode | `skc` or `skc --tmux` | 現時点では外部ランナーのワークフローのみです。 |\n| Claw Code | `skc --tmux --worktree <name>` | SKC は Claw Code にインストールされたり、置き換えたりはしません。 |\n| External controller / bot | `skc mcp-serve coordinator` plus `skc setup hermes` for compatible config, or `skc --mode rpc` for a subprocess worker | MCP/RPC 対応のボットはどれも、スクロールバックのスクレイピングではなく、汎用のコーディネーター/RPC コントラクトを通じて SKC を駆動します。 |\n\n汎用的なサードパーティボットのセットアップとプロバイダー非依存のスモークテストについては、[`docs/bot-integration.md`](docs/bot-integration.md) を参照してください。MCP、RPC、ACP、Bridge/HTTPS 各面にわたる準備状況の分類については、[`docs/external-control-readiness.md`](docs/external-control-readiness.md) を参照してください。より低レベルのプロトコル詳細については、[`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md)、[`docs/rpc.md`](docs/rpc.md)、および [`docs/bridge.md`](docs/bridge.md) を参照してください。リモートオペレーター面のロードマップについては、[`docs/sayknow-remote.md`](docs/sayknow-remote.md) (web steering wheel) と [`docs/telegram-remote.md`](docs/telegram-remote.md) (Telegram lifecycle button) を参照してください。\n\n## Configuration\n\nプロバイダーのリトライバジェットは `~/.skc/config.yml` にあります:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` はストリームが確立される前に適用されます。`streamMaxRetries` はリプレイ安全な一時的ストリーム障害にのみ適用されます。無効な認証、サポートされていないモデル/プロバイダー、不正な形式のリクエスト、コンテキストオーバーフロー、ユーザーによる中断、および恒久的なクォータ障害は、引き続きフェイルファストのままです。\n\n## TUI identity\n\nデフォルトの TUI アイデンティティは SKC の **blue-octopus** テーマ — 青い頭足類のマスコット — で、ダークおよびライトの両方のターミナルに対応します。より暗めでハイコントラストなパレットを好む人のために、温かみのある **red-octopus** バリアントも同梱されています。さらに 3 つの移行用テーマ — `claude-code`、`codex`、`opencode` — がそれらのツールの見た目を再現しており、視覚的な移行を容易にし、Settings または `/theme` から選択できます。ユーザーが明示的に設定したテーマは引き続き優先されます。\n\n### Bundled theme grid\n\nSettings (`Appearance -> Dark theme` / `Light theme`) または `/theme` から選択してください。\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | デフォルトの SKC アイデンティティ — テンタクルブルーのアクセントを持つ青いタコのパレット。 | ダークおよびライトのターミナルのデフォルト。 |\n| `red-octopus` | 強いステータスコントラストを持つ温かみのある赤いタコのバリアント。 | ハイコントラストなダークの代替。 |\n| `claude-code` | テラコッタとピンクのハイライトを持つ Claude Code 風のダークパレット。 | SKC を離れずに Claude Code の体に染み付いた操作感を。 |\n| `codex` | よりシャープなコーディングセッションのコントラストを持つ、くっきりしたダークブルーグレーのパレット。 | Codex ライクなダークワークスペース。 |\n| `opencode` | よりパンチの効いたターミナルアクセントを持つ OpenCode 風のダークパレット。 | 同梱のピッカーで OpenCode の体に染み付いた操作感を。 |\n\n## Development\n\n依存関係をインストールし、ネイティブバインディングをビルドし、ローカルのデフォルトをセットアップします:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\n`@sayknow-cli/natives` 用の `.node` バイナリは gitignore されており、あらゆる CLI 呼び出し (`install:defaults`、`dev:link`、テスト) の前に必要です。\n\n### Canonical: build and link the dev `skc`\n\nグローバルの `skc` コマンドが **このチェックアウトの TypeScript ソース** を実行するようにする (すべての編集に即座に反映され、スキル/ネイティブが動作する) には、それをあなたの `PATH` にリンクします:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` は `skc` → `packages/coding-agent/src/cli.ts` を `~/.local/bin` にシンボリックリンクし (`SKC_DEV_LINK_DIR` で上書き可能)、その管理対象ターゲットを置き換え、別の `skc` が `PATH` 上でより前にそれをシャドウしている場合は警告して失敗し、`--smoke-test` を実行して `@sayknow-cli/natives` がロードされることを確認します。完全なブートストラップ (install + link + `setup defaults`) には `bun run install:dev` を使用してください。\n\nあなたの `skc` がドリフトしていないか (誤ったソース、またはスキルをロードできないコンパイル済みバイナリ) は、いつでも確認できます:\n\n```sh\nbun run dev:doctor\n```\n\n> 日常の開発にコンパイル済みバイナリを **使わないでください**。`bun --cwd=packages/coding-agent run build` はスタンドアロンの `dist/skc` を生成しますが、`bun build --compile` のバイナリは `@sayknow-cli/natives` を動的にロードできないため、スキルは `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'` で失敗します。`dev:link` を通じてソースから実行すれば、これを回避できます。バイナリのビルドはリリースを検証するときのみ行ってください。\n\nリンクせずに CLI をソースから直接実行します:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nデフォルトのワークフロー定義はソースにあり、コミットされた `.skc` のコピーにはありません:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nワークフロー定義またはリブランド面の変更については、プロジェクトのゲートを実行してください:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nパッケージごとのマップについては、[`docs/codebase-overview.md`](docs/codebase-overview.md) を参照してください。\n\n## Contributors\n\nコントリビューション、バグレポート、リリース検証は GitHub の Issues と Pull Request を通じて歓迎しています。\n\n## Inspirations and lineage\n\nSayknow-CLI のデフォルト TUI アイデンティティは頭足類のペアです: デフォルトの blue-octopus と、温かみのある red-octopus の代替。また、`claude-code`、`codex`、`opencode` の移行用テーマも同梱しており、これらのパレットはそれらのツールにインスパイアされているため、移行してくるユーザーが見慣れた見た目を得られます。これは、公開された SKC 面を意図的に集中させたまま、小さなエージェントハーネス一族からの教訓の上に構築されています。歴史的な帰属表示は [`NOTICE.md`](NOTICE.md) に保持されています。\n\n## License\n\nMIT。[`LICENSE`](LICENSE) を参照してください。\n",
76
- "readme/README.ko.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI autonomous coding-agent hero illustration\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>코딩은 사고처럼 느껴져야 합니다.</strong><br />\n 인터뷰, 검토된 계획, tmux 네이티브 실행, 견고한 검증을 위한 집중형 코딩 에이전트 러너.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <b>한국어</b> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI character mascot\" width=\"320\" />\n</p>\n\n> Sayknow-CLI는 실험적인 베타 단계 프로젝트입니다. 거친 부분이 있을 수 있으니 중요한 작업에 의존하기 전에 출력 결과를 검증하세요.\n\n## Languages\n\n인터페이스는 **7개 언어** — English, 한국어 (Korean),\n中文 (简体 / Simplified Chinese), 日本語 (Japanese), Español (Spanish),\nFrançais (French), Deutsch (German) — 로 현지화되어 있습니다. 첫 실행 시\n시스템 로케일을 자동으로 감지하며, 언제든지 **Settings → Appearance → Language** 에서\n전환하거나 예를 들어 `LANG=ja_JP.UTF-8 skc` 로 실행할 수 있습니다. 번역되지 않은 문자열은\nEnglish로 대체되며, 브랜드/기술 이름(Claude, OpenAI, MCP, …)은 모든 로케일에서 그대로 유지됩니다.\n\n## What is Sayknow-CLI?\n\nSayknow-CLI(`skc`)는 외부 코딩 에이전트 하니스입니다. 선택한 저장소나 워크트리에서 실행되며, 에이전트에게 작고 명시적인 워크플로 표면을 제공합니다:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\n이것은 의도적으로 Codex CLI, Claude Code, OpenCode, Claw Code의 숨겨진 플러그인이 아닙니다. 구조화된 계획, 지속적인 증거, tmux 기반 워커, 또는 격리된 워크트리를 원할 때 이러한 도구들 옆에서 `skc`를 시작하세요.\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # 또는: bun install -g sayknow-cli\nskc --version\n```\n\n프리빌드 네이티브(macOS·Linux·Windows)가 포함돼 있어 Rust 툴체인이나 빌드가 필요 없습니다. 업데이트는 `npm install -g sayknow-cli@latest` 또는 터미널에서 `skc update`.\n\n> 이전에 소스(git clone)로 설치했다면 한 번만 `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`로 전환하세요. 소스/개발 설치는 [영문 README](../../README.md#install-from-source-development) 참고.\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nSKC 세션 내부에서는, 공개 워크플로 표면을 사용하세요:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\n조율된 tmux 워커가 실질적으로 도움이 될 때만 `skc team ...`을 추가하세요.\n\n## Core capabilities\n\n- **추측하기 전에 인터뷰**: `deep-interview`는 모호한 요청을 구체적인 요구사항으로 바꿉니다.\n- **변경하기 전에 계획**: `ralplan`은 코드 변경 전에 접근 방식을 검토합니다.\n- **증거와 함께 실행**: `ultragoal`은 목표, 수정, 점검, 완료 증거를 추적합니다.\n- **유용할 때 병렬화**: `team`은 더 큰 작업을 위해 tmux 기반 워커를 조율합니다.\n- **외부에서 검토 가능하게 유지**: 다른 에이전트 런타임을 패치하지 않고 선택한 저장소나 워크트리에서 실행합니다.\n\n## Workflow surface\n\nSayknow-CLI는 네 가지 기본 워크플로 스킬을 제공합니다:\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | 계획이나 코드 변경 전에 모호한 요구사항을 명확히 합니다. |\n| `ralplan` | 변경 전에 구현 계획을 구축하고 비평합니다. |\n| `ultragoal` | 실행, 수정, 검증, 증거를 거쳐 목표를 추적합니다. |\n| `team` | 병렬 실행이 가치가 있을 때 tmux 기반 워커를 조율합니다. |\n\n그리고 네 가지 번들 역할 에이전트:\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | 범위가 정해진 구현, 수정, 리팩터. |\n| `architect` | 읽기 전용 아키텍처 및 코드 리뷰 평가. |\n| `planner` | 읽기 전용 순서 결정 및 수용 기준. |\n| `critic` | 읽기 전용 계획 비평 및 실행 가능성 검토. |\n\n광범위한 기본 스킬 동물원은 없습니다: SKC는 이 작은 방법을 더 좋게 만들어 개선됩니다.\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree`는 SKC가 관리하는 형제 워크트리의 이름을 지정합니다. 기존 경로의 경우 먼저 그곳으로 `cd` 하세요. |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC는 Claude Code 확장이 되지 않습니다. |\n| OpenCode | `skc` or `skc --tmux` | 현재로서는 외부 러너 워크플로만 지원합니다. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC는 Claw Code에 설치되거나 그것을 대체하지 않습니다. |\n| External controller / bot | 호환 가능한 구성을 위한 `skc mcp-serve coordinator` 및 `skc setup hermes`, 또는 서브프로세스 워커를 위한 `skc --mode rpc` | MCP/RPC 지원 봇이라면 무엇이든 스크롤백 스크래핑이 아니라 일반 coordinator/RPC 계약을 통해 SKC를 구동합니다. |\n\n일반 서드파티 봇 설정 및 공급자 독립적 스모크에 대해서는 [`docs/bot-integration.md`](docs/bot-integration.md)를 참조하세요. MCP, RPC, ACP, Bridge/HTTPS 표면 전반의 준비도 분류에 대해서는 [`docs/external-control-readiness.md`](docs/external-control-readiness.md)를 참조하세요. 더 낮은 수준의 프로토콜 세부 사항에 대해서는 [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md), [`docs/bridge.md`](docs/bridge.md)를 참조하세요. 원격 운영자 표면 로드맵에 대해서는 [`docs/sayknow-remote.md`](docs/sayknow-remote.md)(웹 스티어링 휠) 및 [`docs/telegram-remote.md`](docs/telegram-remote.md)(Telegram 라이프사이클 버튼)를 참조하세요.\n\n## Configuration\n\n공급자 재시도 예산은 `~/.skc/config.yml`에 있습니다:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries`는 스트림이 설정되기 전에 적용됩니다. `streamMaxRetries`는 재생 안전한 일시적 스트림 실패에만 적용됩니다. 잘못된 인증, 지원되지 않는 모델/공급자, 잘못된 형식의 요청, 컨텍스트 오버플로, 사용자 중단, 영구적인 할당량 실패는 즉시 실패(fail-fast)로 유지됩니다.\n\n## TUI identity\n\n기본 TUI 정체성은 SKC **blue-octopus** 테마 — 파란 두족류 마스코트 — 로, 다크 및 라이트 터미널 모두에 적용됩니다. 더 어둡고 고대비 팔레트를 선호하는 사람들을 위해 따뜻한 **red-octopus** 변형도 번들로 제공됩니다. 세 가지 추가 마이그레이션 테마 — `claude-code`, `codex`, `opencode` — 는 쉬운 눈 마이그레이션을 위해 해당 도구들의 모습을 그대로 따르며 Settings 또는 `/theme`에서 선택할 수 있습니다. 명시적인 사용자 테마 설정이 여전히 우선합니다.\n\n### Bundled theme grid\n\nSettings (`Appearance -> Dark theme` / `Light theme`) 또는 `/theme`에서 선택하세요.\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | 기본 SKC 정체성 — 촉수 블루 액센트가 있는 파란 문어 팔레트. | 다크 및 라이트 터미널의 기본값. |\n| `red-octopus` | 강한 상태 대비를 가진 따뜻한 빨간 문어 변형. | 고대비 다크 대안. |\n| `claude-code` | 테라코타와 핑크 하이라이트가 있는 Claude Code 영감 다크 팔레트. | SKC를 떠나지 않고 Claude Code 근육 기억을 유지. |\n| `codex` | 더 날카로운 코딩 세션 대비를 가진 선명한 다크 블루그레이 팔레트. | Codex 같은 다크 작업 공간. |\n| `opencode` | 더 강렬한 터미널 액센트를 가진 OpenCode 영감 다크 팔레트. | 번들 선택기에서의 OpenCode 근육 기억. |\n\n## Development\n\n의존성을 설치하고, 네이티브 바인딩을 빌드하고, 로컬 기본값을 설정하세요:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\n`@sayknow-cli/natives`용 `.node` 바이너리는 gitignore되어 있으며 모든 CLI 호출(`install:defaults`, `dev:link`, 테스트) 전에 필요합니다.\n\n### Canonical: build and link the dev `skc`\n\n전역 `skc` 명령이 **이 체크아웃의 TypeScript 소스**(모든 편집에 즉시 반영되며, 스킬/네이티브가 작동함)를 실행하도록 하려면, `PATH`에 링크하세요:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link`는 `skc` → `packages/coding-agent/src/cli.ts`를 `~/.local/bin`에 심볼릭 링크하고(`SKC_DEV_LINK_DIR`로 재정의 가능), 그 관리되는 대상을 교체하며, 다른 `skc`가 여전히 `PATH`에서 더 앞쪽에 있어 그것을 가린다면 경고하고 실패하며, `--smoke-test`를 실행하여 `@sayknow-cli/natives`가 로드되는지 확인합니다. 전체 부트스트랩(install + link + `setup defaults`)을 위해서는 `bun run install:dev`를 사용하세요.\n\n당신의 `skc`가 표류했는지(잘못된 소스, 또는 스킬을 로드할 수 없는 컴파일된 바이너리) 언제든지 확인하세요:\n\n```sh\nbun run dev:doctor\n```\n\n> 일상적인 개발에는 컴파일된 바이너리를 **사용하지 마세요**. `bun --cwd=packages/coding-agent run build`는 독립 실행형 `dist/skc`를 생성하지만, `bun build --compile` 바이너리는 `@sayknow-cli/natives`를 동적으로 로드할 수 없으므로 스킬이 `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'`로 실패합니다. `dev:link`를 통해 소스에서 실행하면 이를 피할 수 있습니다. 릴리스를 검증할 때만 바이너리를 빌드하세요.\n\n링크 없이 소스에서 직접 CLI를 실행하세요:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\n기본 워크플로 정의는 커밋된 `.skc` 사본이 아니라 소스에 있습니다:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\n워크플로 정의 또는 리브랜드 표면 변경의 경우, 프로젝트 게이트를 실행하세요:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\n패키지별 맵에 대해서는 [`docs/codebase-overview.md`](docs/codebase-overview.md)를 참조하세요.\n\n## Contributors\n\n기여, 버그 보고, 릴리스 검증은 GitHub Issues와 Pull Request를 통해 환영합니다.\n\n## Inspirations and lineage\n\nSayknow-CLI의 기본 TUI 정체성은 두족류 쌍입니다: 기본값인 blue-octopus와 따뜻한 red-octopus 대안. 또한 해당 도구들에서 옮겨오는 사용자들이 익숙한 모습을 얻도록 팔레트가 그 도구들에서 영감을 받은 `claude-code`, `codex`, `opencode` 마이그레이션 테마를 번들로 제공합니다. 공개 SKC 표면을 의도적으로 집중된 상태로 유지하면서, 작은 에이전트 하니스 계열에서 얻은 교훈을 바탕으로 만들어졌습니다. 역사적 출처 표기는 [`NOTICE.md`](NOTICE.md)에 보관되어 있습니다.\n\n## License\n\nMIT. [`LICENSE`](LICENSE)를 참조하세요.\n",
77
- "readme/README.zh.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI autonomous coding-agent hero illustration\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>编码应当如思考般自然。</strong><br />\n 一个专注的编码智能体运行器,面向访谈式需求澄清、经评审的计划、tmux 原生执行与持久化验证。\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <b>中文</b> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI character mascot\" width=\"320\" />\n</p>\n\n> Sayknow-CLI 是一个实验性的、处于 beta 阶段的项目。请预期会有粗糙之处,并在依赖其结果完成重要工作之前先行验证输出。\n\n## Languages\n\n界面已本地化为 **7 种语言**——English、한국어(韩语)、\n中文(简体)、日本語(日语)、Español(西班牙语)、\nFrançais(法语)以及 Deutsch(德语)。首次运行时它会自动检测你的系统区域设置;\n你可以随时在 **Settings → Appearance → Language** 中切换,或者使用例如\n`LANG=ja_JP.UTF-8 skc` 的方式启动。未翻译的字符串会回退到英文,而\n品牌/技术名称(Claude、OpenAI、MCP……)在所有语言环境中均保持原样。\n\n## What is Sayknow-CLI?\n\nSayknow-CLI(`skc`)是一个外部编码智能体框架(harness)。它从你选择的仓库或工作树(worktree)中运行,然后为智能体提供一个精简、明确的工作流界面:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\n它有意不做成 Codex CLI、Claude Code、OpenCode 或 Claw Code 的隐藏插件。当你需要结构化的规划、持久化的证据、tmux 支持的工作进程或一个隔离的工作树时,就在这些工具旁边启动 `skc`。\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # 或:bun install -g sayknow-cli\nskc --version\n```\n\n已内置 macOS·Linux·Windows 的预编译原生模块,无需 Rust 工具链或构建步骤。更新:`npm install -g sayknow-cli@latest` 或在终端运行 `skc update`。\n\n> 如果你之前是从源码(git clone)安装的,只需一次性切换:`rm -f ~/.local/bin/skc && npm install -g sayknow-cli`。源码/开发安装请参见[英文 README](../../README.md#install-from-source-development)。\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\n在 SKC 会话内部,使用公共工作流界面:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\n仅当协同的 tmux 工作进程能带来实质性帮助时,才加上 `skc team ...`。\n\n## Core capabilities\n\n- **先访谈,不靠猜**:`deep-interview` 把模糊的请求转化为具体的需求。\n- **先规划,再变更**:`ralplan` 在代码改动之前评审方案。\n- **带证据地执行**:`ultragoal` 跟踪目标、修订、检查以及完成证据。\n- **在有用时并行化**:`team` 为较大的任务协调 tmux 支持的工作进程。\n- **保持外部化且可评审**:从所选的仓库或工作树中运行,无需给另一个智能体运行时打补丁。\n\n## Workflow surface\n\nSayknow-CLI 内置四项默认工作流技能:\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | 在规划或代码改动之前澄清模糊的需求。 |\n| `ralplan` | 在变更之前构建并评审实现计划。 |\n| `ultragoal` | 在执行、修订、验证与证据收集的全过程中跟踪目标。 |\n| `team` | 当并行执行值得时,协调 tmux 支持的工作进程。 |\n\n以及四个捆绑的角色智能体:\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | 有边界的实现、修复与重构。 |\n| `architect` | 只读的架构与代码评审评估。 |\n| `planner` | 只读的排序与验收标准。 |\n| `critic` | 只读的计划评审与可执行性审查。 |\n\n没有庞杂的默认技能堆砌:SKC 通过把这一精简方法做得更好来持续改进。\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree` 指定一个由 SKC 管理的同级工作树;对于已存在的路径,请先 `cd` 到那里。 |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC 不会成为 Claude Code 的扩展。 |\n| OpenCode | `skc` or `skc --tmux` | 目前仅支持外部运行器(external-runner)工作流。 |\n| Claw Code | `skc --tmux --worktree <name>` | SKC 不会安装到 Claw Code 中,也不会替代它。 |\n| External controller / bot | `skc mcp-serve coordinator` plus `skc setup hermes` for compatible config, or `skc --mode rpc` for a subprocess worker | 任何具备 MCP/RPC 能力的 bot 都通过通用的 coordinator/RPC 契约来驱动 SKC,而非抓取滚动回显(scrollback scraping)。 |\n\n关于通用第三方 bot 的设置以及与提供商无关的冒烟测试,请参阅 [`docs/bot-integration.md`](docs/bot-integration.md)。关于在 MCP、RPC、ACP 与 Bridge/HTTPS 各界面上的就绪度分级,请参阅 [`docs/external-control-readiness.md`](docs/external-control-readiness.md)。关于更底层的协议细节,请参阅 [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md)、[`docs/rpc.md`](docs/rpc.md) 以及 [`docs/bridge.md`](docs/bridge.md)。关于远程操作员界面的路线图,请参阅 [`docs/sayknow-remote.md`](docs/sayknow-remote.md)(网页方向盘)以及 [`docs/telegram-remote.md`](docs/telegram-remote.md)(Telegram 生命周期按钮)。\n\n## Configuration\n\n提供商重试预算位于 `~/.skc/config.yml`:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` 在流(stream)建立之前生效。`streamMaxRetries` 仅适用于可安全重放的瞬时流失败。无效的认证、不受支持的模型/提供商、格式错误的请求、上下文溢出、用户中止以及永久性配额失败仍然保持快速失败(fail-fast)。\n\n## TUI identity\n\n默认的 TUI 标识是 SKC 的 **blue-octopus**(蓝章鱼)主题——蓝色头足类吉祥物——同时适用于深色和浅色终端。还捆绑了一个暖色调的 **red-octopus**(红章鱼)变体,供偏好更深、高对比度配色的用户使用。另有三个迁移主题——`claude-code`、`codex` 和 `opencode`——分别复刻了这些工具的外观,以便于视觉迁移,可从 Settings 或 `/theme` 中选择。显式的用户主题设置仍然优先生效。\n\n### Bundled theme grid\n\n从 Settings(`Appearance -> Dark theme` / `Light theme`)或 `/theme` 中选择。\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | 默认 SKC 标识——蓝章鱼配色,带触手蓝点缀。 | 深色和浅色终端的默认主题。 |\n| `red-octopus` | 暖色调红章鱼变体,状态对比强烈。 | 高对比度的深色替代方案。 |\n| `claude-code` | 受 Claude Code 启发的深色配色,带赤陶色和粉色高光。 | 在不离开 SKC 的情况下保留 Claude Code 的肌肉记忆。 |\n| `codex` | 清爽的深蓝灰配色,编码会话对比更锐利。 | 类似 Codex 的深色工作区。 |\n| `opencode` | 受 OpenCode 启发的深色配色,终端点缀更鲜明。 | 在捆绑选择器中保留 OpenCode 的肌肉记忆。 |\n\n## Development\n\n安装依赖、构建原生绑定,并设置本地默认值:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\n`@sayknow-cli/natives` 的 `.node` 二进制文件已被 gitignore,且在任何 CLI 调用(`install:defaults`、`dev:link`、测试)之前都是必需的。\n\n### Canonical: build and link the dev `skc`\n\n要让全局 `skc` 命令运行**此检出的 TypeScript 源码**(对每一次编辑都即时生效,且技能/原生绑定均可用),请把它链接到你的 `PATH`:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` 会把 `skc` → `packages/coding-agent/src/cli.ts` 软链接到 `~/.local/bin`(可用 `SKC_DEV_LINK_DIR` 覆盖),替换该受管目标,如果另一个 `skc` 仍在 `PATH` 上更靠前地遮蔽它则会发出警告并失败,并运行 `--smoke-test` 以确认 `@sayknow-cli/natives` 能够加载。使用 `bun run install:dev` 进行完整的引导(install + link + `setup defaults`)。\n\n随时检查你的 `skc` 是否已经漂移(源码错误,或一个无法加载技能的已编译二进制文件):\n\n```sh\nbun run dev:doctor\n```\n\n> 在日常开发中**不要**使用已编译的二进制文件。`bun --cwd=packages/coding-agent run build` 会产出一个独立的 `dist/skc`,但 `bun build --compile` 生成的二进制无法动态加载 `@sayknow-cli/natives`,因此技能会以 `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'` 失败。通过 `dev:link` 从源码运行可避免此问题。仅在验证发布版本时才构建该二进制文件。\n\n不进行链接,直接从源码运行 CLI:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\n默认工作流定义存放在源码中,而非已提交的 `.skc` 副本:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\n对于工作流定义或品牌重塑界面(rebrand-surface)的改动,请运行项目门禁(gates):\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\n关于逐包(package-by-package)的对照图,请参阅 [`docs/codebase-overview.md`](docs/codebase-overview.md)。\n\n## Contributors\n\n欢迎通过 GitHub Issues 和 Pull Requests 进行贡献、提交错误报告以及参与发布验证。\n\n## Inspirations and lineage\n\nSayknow-CLI 默认的 TUI 标识是这对头足类:blue-octopus 作为默认,搭配一个暖色调的 red-octopus 备选。它还捆绑了 `claude-code`、`codex` 和 `opencode` 迁移主题,其配色受这些工具启发,以便从它们迁移过来的用户能获得熟悉的外观。它在一个小型智能体框架家族的经验之上构建,同时有意保持公共 SKC 界面的专注。历史归属保留在 [`NOTICE.md`](NOTICE.md) 中。\n\n## License\n\nMIT。参见 [`LICENSE`](LICENSE)。\n",
72
+ "readme/README.de.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI autonomous coding-agent hero illustration\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>Programmieren sollte sich wie Denken anfühlen.</strong><br />\n Ein fokussierter Coding-Agent-Runner für Interviews, geprüfte Pläne, tmux-native Ausführung und dauerhafte Verifizierung.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <b>Deutsch</b>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI character mascot\" width=\"320\" />\n</p>\n\n> Sayknow-CLI ist ein experimentelles Projekt im Beta-Stadium. Rechnen Sie mit Ecken und Kanten und überprüfen Sie die Ausgaben, bevor Sie sich bei wichtiger Arbeit darauf verlassen.\n\n## Languages\n\nDie Oberfläche ist in **7 Sprachen** lokalisiert — English, 한국어 (Koreanisch),\n中文 (简体 / Vereinfachtes Chinesisch), 日本語 (Japanisch), Español (Spanisch),\nFrançais (Französisch) und Deutsch. Beim ersten Start erkennt sie automatisch\nIhre System-Locale; wechseln Sie jederzeit unter **Settings → Appearance → Language**\noder starten Sie z. B. mit `LANG=ja_JP.UTF-8 skc`. Nicht übersetzte Zeichenketten\nfallen auf Englisch zurück, und Marken-/Fachbegriffe (Claude, OpenAI, MCP, …)\nbleiben in allen Locales unverändert.\n\n## Was ist Sayknow-CLI?\n\nSayknow-CLI (`skc`) ist ein externes Coding-Agent-Harness. Es läuft aus dem von Ihnen gewählten Repository oder Worktree und gibt dem Agenten dann eine kleine, explizite Workflow-Oberfläche:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nEs ist bewusst kein verstecktes Plugin für Codex CLI, Claude Code, OpenCode oder Claw Code. Starten Sie `skc` neben diesen Tools, wenn Sie strukturierte Planung, dauerhafte Nachweise, tmux-gestützte Worker oder einen isolierten Worktree wünschen.\n\n## Installation\n\n```sh\nnpm install -g sayknow-cli # oder: bun install -g sayknow-cli\nskc --version\n```\n\nDas Paket enthält vorgefertigte native Addons für macOS, Linux und Windows – keine Rust-Toolchain und kein Build-Schritt nötig. Aktualisieren: `npm install -g sayknow-cli@latest` oder `skc update` im Terminal.\n\n> Früher aus dem Quellcode (git clone) installiert? Einmalig umsteigen: `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`. Für die Installation aus dem Quellcode (Entwicklung) siehe die [englische README](../../README.md#install-from-source-development).\n\n## Schnellstart\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nVerwenden Sie innerhalb einer SKC-Sitzung die öffentliche Workflow-Oberfläche:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\nFügen Sie `skc team ...` nur hinzu, wenn koordinierte tmux-Worker spürbar helfen.\n\n## Kernfunktionen\n\n- **Interview vor dem Raten**: `deep-interview` verwandelt vage Anfragen in konkrete Anforderungen.\n- **Plan vor der Veränderung**: `ralplan` prüft den Ansatz vor Codeänderungen.\n- **Ausführen mit Nachweisen**: `ultragoal` verfolgt Ziele, Revisionen, Prüfungen und Abschlussnachweise.\n- **Parallelisieren, wenn sinnvoll**: `team` koordiniert tmux-gestützte Worker für größere Aufgaben.\n- **Extern und überprüfbar bleiben**: Laufen Sie aus einem gewählten Repo oder Worktree, ohne eine andere Agent-Runtime zu patchen.\n\n## Workflow-Oberfläche\n\nSayknow-CLI liefert vier Standard-Workflow-Skills:\n\n| Skill | Was es tut |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | Klärt mehrdeutige Anforderungen vor Planung oder Codeänderungen. |\n| `ralplan` | Erstellt und kritisiert einen Implementierungsplan vor der Veränderung. |\n| `ultragoal` | Verfolgt Ziele durch Ausführung, Revision, Verifizierung und Nachweise. |\n| `team` | Koordiniert tmux-gestützte Worker, wenn parallele Ausführung sich lohnt. |\n\nUnd vier mitgelieferte Rollen-Agenten:\n\n| Agent | Was es tut |\n| ----------- | -------------------------------------------------- |\n| `executor` | Begrenzte Implementierung, Fixes und Refactorings. |\n| `architect` | Schreibgeschützte Architektur- und Code-Review-Bewertung. |\n| `planner` | Schreibgeschützte Sequenzierung und Abnahmekriterien. |\n| `critic` | Schreibgeschützte Plan-Kritik und Umsetzbarkeitsprüfung. |\n\nKein wucherndes Standard-Skill-Zoo: SKC verbessert sich, indem es diese kleine Methode besser macht.\n\n## Funktioniert neben Ihrem bestehenden Agenten oder Bot\n\n| Tool oder Bot | Empfohlener SKC-Befehl | Grenze |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` oder `skc` | `--worktree` benennt einen SKC-verwalteten Geschwister-Worktree; für einen bestehenden Pfad wechseln Sie zuerst mit `cd` dorthin. |\n| Claude Code | `skc --tmux` oder `skc --tmux --worktree <name>` | SKC wird keine Claude-Code-Erweiterung. |\n| OpenCode | `skc` oder `skc --tmux` | Heute nur External-Runner-Workflow. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC installiert sich nicht in Claw Code und ersetzt es nicht. |\n| Externer Controller / Bot | `skc mcp-serve coordinator` plus `skc setup hermes` für kompatible Konfiguration oder `skc --mode rpc` für einen Subprozess-Worker | Jeder MCP-/RPC-fähige Bot steuert SKC über den generischen Coordinator-/RPC-Vertrag, nicht durch Scrollback-Scraping. |\n\nFür generisches Drittanbieter-Bot-Setup und anbieterunabhängige Smokes siehe [`docs/bot-integration.md`](docs/bot-integration.md). Für die Reife-Klassifizierung über MCP-, RPC-, ACP- und Bridge/HTTPS-Oberflächen siehe [`docs/external-control-readiness.md`](docs/external-control-readiness.md). Für tiefergehende Protokolldetails siehe [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md) und [`docs/bridge.md`](docs/bridge.md). Für die Roadmap der Remote-Operator-Oberflächen siehe [`docs/sayknow-remote.md`](docs/sayknow-remote.md) (Web-Steuerrad) und [`docs/telegram-remote.md`](docs/telegram-remote.md) (Telegram-Lifecycle-Button).\n\n## Konfiguration\n\nProvider-Retry-Budgets liegen in `~/.skc/config.yml`:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` gilt, bevor ein Stream aufgebaut wird. `streamMaxRetries` gilt nur für replay-sichere, vorübergehende Stream-Fehler. Ungültige Authentifizierung, nicht unterstützte Modelle/Provider, fehlerhafte Requests, Kontextüberlauf, Benutzerabbrüche und dauerhafte Kontingentfehler bleiben fail-fast.\n\n## TUI-Identität\n\nDie Standard-TUI-Identität ist das SKC-**ink-octopus**-Theme — Oktopus-Tinte: warmer Graphit, papierfarbener Text, ein Bernstein-Akzent — für dunkle Terminals, mit **blue-octopus** für helle Terminals. Eine warme **red-octopus**-Variante ist ebenfalls dabei für alle, die eine dunklere, kontrastreiche Palette bevorzugen. Drei zusätzliche Migrations-Themes — `claude-code`, `codex` und `opencode` — spiegeln das Aussehen dieser Tools für einen einfachen Augen-Umstieg wider und sind über Settings oder `/theme` auswählbar. Explizite Benutzer-Theme-Einstellungen gewinnen weiterhin.\n\n### Raster der mitgelieferten Themes\n\nWählen Sie über Settings (`Appearance -> Dark theme` / `Light theme`) oder `/theme`.\n\n| Theme | Visueller Eindruck | Beste Eignung |\n| --- | --- | --- |\n| `blue-octopus` | Blaue Oktopus-Palette mit tentakelblauen Akzenten. | Standard für helle Terminals. |\n| `red-octopus` | Warme rote Oktopus-Variante mit starkem Status-Kontrast. | Kontrastreiche dunkle Alternative. |\n| `ink-octopus` | Oktopus-Tinte — warmer Graphit-Hintergrund, papierfarbener Text, ein einziger Bernstein-Akzent. | Standard für dunkle Terminals. |\n| `glow-octopus` | Tiefsee-Biolumineszenz — Petrol-Schwarz mit leuchtendem Grün und Violett als Zweitfarbe. | Dunkle Terminals mit kräftigem Akzent. |\n| `violet-octopus` | Pflaumendunkel mit Lavendel-Akzenten und Aprikose als Zweitfarbe. | Weiche, farbige dunkle Alternative. |\n| `claude-code` | Von Claude Code inspirierte dunkle Palette mit terrakotta- und pinkfarbenen Highlights. | Claude-Code-Muskelgedächtnis, ohne SKC zu verlassen. |\n| `codex` | Klare dunkle blaugraue Palette mit schärferem Coding-Session-Kontrast. | Ein Codex-ähnlicher dunkler Arbeitsbereich. |\n| `opencode` | Von OpenCode inspirierte dunkle Palette mit kräftigeren Terminal-Akzenten. | OpenCode-Muskelgedächtnis im mitgelieferten Picker. |\n\n## Entwicklung\n\nAbhängigkeiten installieren, native Bindings bauen und lokale Standardwerte einrichten:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\nDie `.node`-Binärdatei für `@sayknow-cli/natives` ist gitignored und vor jeder CLI-Ausführung erforderlich (`install:defaults`, `dev:link`, Tests).\n\n### Kanonisch: Entwickler-`skc` bauen und verlinken\n\nDamit der globale Befehl `skc` **den TypeScript-Quellcode dieses Checkouts** ausführt (live bei jeder Bearbeitung, mit funktionierenden Skills/Natives), verlinken Sie ihn in Ihren `PATH`:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` legt einen Symlink `skc` → `packages/coding-agent/src/cli.ts` nach `~/.local/bin` an (überschreibbar mit `SKC_DEV_LINK_DIR`), ersetzt dieses verwaltete Ziel, warnt und schlägt fehl, falls ein anderes `skc` es weiter vorne im `PATH` überschattet, und führt `--smoke-test` aus, um zu bestätigen, dass `@sayknow-cli/natives` geladen wird. Verwenden Sie `bun run install:dev` für das vollständige Bootstrap (Installation + Link + `setup defaults`).\n\nPrüfen Sie jederzeit, ob Ihr `skc` abgedriftet ist (falsche Quelle oder eine kompilierte Binärdatei, die keine Skills laden kann):\n\n```sh\nbun run dev:doctor\n```\n\n> Verwenden Sie für die tägliche Entwicklung **nicht** die kompilierte Binärdatei. `bun --cwd=packages/coding-agent run build` erzeugt ein eigenständiges `dist/skc`, aber eine mit `bun build --compile` erstellte Binärdatei kann `@sayknow-cli/natives` nicht dynamisch laden, sodass Skills mit `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'` fehlschlagen. Die Ausführung aus dem Quellcode über `dev:link` vermeidet dies. Bauen Sie die Binärdatei nur, wenn Sie ein Release validieren.\n\nFühren Sie die CLI direkt aus dem Quellcode ohne Verlinkung aus:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nStandard-Workflow-Definitionen liegen im Quellcode, nicht in committeten `.skc`-Kopien:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nFür Änderungen an Workflow-Definitionen oder Rebrand-Oberflächen führen Sie die Projekt-Gates aus:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nFür eine Paket-für-Paket-Übersicht siehe [`docs/codebase-overview.md`](docs/codebase-overview.md).\n\n## Mitwirkende\n\nBeiträge, Fehlerberichte und Release-Validierung sind über GitHub Issues und Pull Requests willkommen.\n\n## Inspirationen und Herkunft\n\nDie Standard-TUI-Identität von Sayknow-CLI ist die Oktopus-Familie: ink-octopus als dunkler Standard, blue-octopus als heller Standard sowie red-octopus, glow-octopus und violet-octopus als Alternativen. Es liefert außerdem die Migrations-Themes `claude-code`, `codex` und `opencode`, deren Paletten von diesen Tools inspiriert sind, damit Benutzer, die von ihnen wechseln, einen vertrauten Look erhalten. Es baut auf Erkenntnissen aus einer kleinen Familie von Agent-Harnesses auf und hält die öffentliche SKC-Oberfläche bewusst fokussiert. Die historische Zuordnung wird in [`NOTICE.md`](NOTICE.md) geführt.\n\n## Lizenz\n\nMIT. Siehe [`LICENSE`](LICENSE).\n",
73
+ "readme/README.es.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Ilustración principal del agente de codificación autónomo Sayknow-CLI\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>Programar debería sentirse como pensar.</strong><br />\n Un ejecutor de agentes de codificación enfocado en entrevistas, planes revisados, ejecución nativa en tmux y verificación duradera.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <b>Español</b> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Mascota personaje de Sayknow-CLI\" width=\"320\" />\n</p>\n\n> Sayknow-CLI es un proyecto experimental en fase beta. Espera asperezas y verifica los resultados antes de confiar en él para trabajos importantes.\n\n## Languages\n\nLa interfaz está localizada en **7 idiomas** — English, 한국어 (coreano),\n中文 (简体 / chino simplificado), 日本語 (japonés), Español,\nFrançais (francés) y Deutsch (alemán). Detecta automáticamente la configuración regional de tu sistema en\nel primer arranque; cámbiala en cualquier momento en **Settings → Appearance → Language**, o inícialo\ncon, por ejemplo, `LANG=ja_JP.UTF-8 skc`. Las cadenas no traducidas recurren al inglés, y\nlos nombres de marca/técnicos (Claude, OpenAI, MCP, …) se mantienen literales en todas las configuraciones regionales.\n\n## ¿Qué es Sayknow-CLI?\n\nSayknow-CLI (`skc`) es un arnés externo de agentes de codificación. Se ejecuta desde el repositorio o worktree que elijas y luego le da al agente una superficie de flujo de trabajo pequeña y explícita:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nIntencionadamente no es un plugin oculto para Codex CLI, Claude Code, OpenCode o Claw Code. Inicia `skc` junto a esas herramientas cuando quieras planificación estructurada, evidencia persistente, workers respaldados por tmux o un worktree aislado.\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # o: bun install -g sayknow-cli\nskc --version\n```\n\nEl paquete incluye binarios nativos precompilados para macOS, Linux y Windows, así que no necesitas Rust ni paso de compilación. Para actualizar: `npm install -g sayknow-cli@latest` o ejecuta `skc update` en la terminal.\n\n> ¿Vienes de una instalación desde el código fuente (git clone)? Cambia una sola vez: `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`. Para la instalación desde el código (desarrollo), consulta el [README en inglés](../../README.md#install-from-source-development).\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nDentro de una sesión de SKC, usa la superficie pública del flujo de trabajo:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\nAñade `skc team ...` solo cuando los workers coordinados de tmux ayuden de forma significativa.\n\n## Capacidades principales\n\n- **Entrevistar antes de suponer**: `deep-interview` convierte solicitudes vagas en requisitos concretos.\n- **Planificar antes de mutar**: `ralplan` revisa el enfoque antes de los cambios de código.\n- **Ejecutar con evidencia**: `ultragoal` rastrea objetivos, revisiones, comprobaciones y evidencia de finalización.\n- **Paralelizar cuando sea útil**: `team` coordina workers respaldados por tmux para tareas más grandes.\n- **Mantenerse externo y revisable**: ejecútalo desde un repositorio o worktree elegido sin parchear otro runtime de agente.\n\n## Superficie del flujo de trabajo\n\nSayknow-CLI incluye cuatro skills de flujo de trabajo predeterminadas:\n\n| Skill | Qué hace |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | Aclara requisitos ambiguos antes de planificar o cambiar código. |\n| `ralplan` | Construye y critica un plan de implementación antes de mutar. |\n| `ultragoal` | Rastrea objetivos a través de ejecución, revisión, verificación y evidencia. |\n| `team` | Coordina workers respaldados por tmux cuando vale la pena la ejecución paralela. |\n\nY cuatro agentes de rol incluidos:\n\n| Agent | Qué hace |\n| ----------- | -------------------------------------------------- |\n| `executor` | Implementación acotada, correcciones y refactorizaciones. |\n| `architect` | Evaluación de arquitectura y revisión de código de solo lectura. |\n| `planner` | Secuenciación y criterios de aceptación de solo lectura. |\n| `critic` | Crítica de planes y revisión de accionabilidad de solo lectura. |\n\nSin un zoológico de skills predeterminadas desbordante: SKC mejora haciendo mejor este pequeño método.\n\n## Funciona junto a tu agente o bot existente\n\n| Herramienta o bot | Comando SKC recomendado | Límite |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` o `skc` | `--worktree` nombra un worktree hermano gestionado por SKC; para una ruta existente, haz `cd` allí primero. |\n| Claude Code | `skc --tmux` o `skc --tmux --worktree <name>` | SKC no se convierte en una extensión de Claude Code. |\n| OpenCode | `skc` o `skc --tmux` | Solo flujo de trabajo de ejecutor externo por ahora. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC no se instala dentro de Claw Code ni lo reemplaza. |\n| Controlador / bot externo | `skc mcp-serve coordinator` más `skc setup hermes` para una configuración compatible, o `skc --mode rpc` para un worker en subproceso | Cualquier bot con capacidad MCP/RPC controla SKC mediante el contrato genérico coordinator/RPC, no mediante scraping del scrollback. |\n\nPara la configuración genérica de bots de terceros y pruebas de humo independientes del proveedor, consulta [`docs/bot-integration.md`](docs/bot-integration.md). Para la clasificación de preparación a través de las superficies MCP, RPC, ACP y Bridge/HTTPS, consulta [`docs/external-control-readiness.md`](docs/external-control-readiness.md). Para los detalles de protocolo de más bajo nivel, consulta [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md) y [`docs/bridge.md`](docs/bridge.md). Para la hoja de ruta de las superficies de operador remoto, consulta [`docs/sayknow-remote.md`](docs/sayknow-remote.md) (volante web) y [`docs/telegram-remote.md`](docs/telegram-remote.md) (botón de ciclo de vida de Telegram).\n\n## Configuration\n\nLos presupuestos de reintento del proveedor viven en `~/.skc/config.yml`:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` se aplica antes de que se establezca un stream. `streamMaxRetries` se aplica solo a fallos transitorios de stream que son seguros de reproducir. La autenticación inválida, los modelos/proveedores no compatibles, las solicitudes malformadas, el desbordamiento de contexto, las cancelaciones del usuario y los fallos permanentes de cuota siguen siendo de fallo rápido.\n\n## Identidad de la TUI\n\nLa identidad predeterminada de la TUI es el tema **ink-octopus** de SKC — tinta de pulpo: grafito cálido, texto color papel y un único acento ámbar — para terminales oscuras, con **blue-octopus** para terminales claras. También se incluye una variante cálida **red-octopus** para quienes prefieren una paleta más oscura y de alto contraste. Tres temas de migración adicionales — `claude-code`, `codex` y `opencode` — reflejan el aspecto de esas herramientas para facilitar la migración visual y se pueden seleccionar desde Settings o `/theme`. Los ajustes de tema explícitos del usuario siguen prevaleciendo.\n\n### Cuadrícula de temas incluidos\n\nElige desde Settings (`Appearance -> Dark theme` / `Light theme`) o `/theme`.\n\n| Tema | Sensación visual | Mejor uso |\n| --- | --- | --- |\n| `blue-octopus` | Paleta de pulpo azul con acentos azul-tentáculo. | Predeterminado para terminales claras. |\n| `red-octopus` | Variante cálida de pulpo rojo con fuerte contraste de estado. | Alternativa oscura de alto contraste. |\n| `ink-octopus` | Tinta de pulpo — fondo grafito cálido, texto color papel y un único acento ámbar. | Predeterminado para terminales oscuras. |\n| `glow-octopus` | Bioluminiscencia abisal — negro verdiazulado con verde brillante y violeta como secundario. | Terminales oscuras que quieren un acento vivo. |\n| `violet-octopus` | Oscuro ciruela con acentos lavanda y albaricoque como secundario. | Alternativa oscura suave y colorida. |\n| `claude-code` | Paleta oscura inspirada en Claude Code con resaltados terracota y rosa. | Memoria muscular de Claude Code sin salir de SKC. |\n| `codex` | Paleta nítida azul-gris oscuro con un contraste de sesión de codificación más marcado. | Un espacio de trabajo oscuro al estilo Codex. |\n| `opencode` | Paleta oscura inspirada en OpenCode con acentos de terminal más vibrantes. | Memoria muscular de OpenCode en el selector incluido. |\n\n## Development\n\nInstala las dependencias, compila los bindings nativos y configura los valores predeterminados locales:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\nEl binario `.node` para `@sayknow-cli/natives` está en gitignore y es necesario antes de cualquier invocación del CLI (`install:defaults`, `dev:link`, tests).\n\n### Canónico: compilar y enlazar el `skc` de desarrollo\n\nPara hacer que el comando global `skc` ejecute **el código fuente TypeScript de esta copia** (sensible a cada edición, con skills/natives funcionando), enlázalo a tu `PATH`:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` crea un symlink de `skc` → `packages/coding-agent/src/cli.ts` en `~/.local/bin` (sobrescríbelo con `SKC_DEV_LINK_DIR`), reemplaza ese objetivo gestionado, advierte y falla si otro `skc` aún lo oculta antes en `PATH`, y ejecuta `--smoke-test` para confirmar que `@sayknow-cli/natives` carga. Usa `bun run install:dev` para el bootstrap completo (install + link + `setup defaults`).\n\nComprueba en cualquier momento si tu `skc` se ha desviado (fuente incorrecta, o un binario compilado que no puede cargar skills):\n\n```sh\nbun run dev:doctor\n```\n\n> **No** uses el binario compilado para el desarrollo diario. `bun --cwd=packages/coding-agent run build` produce un `dist/skc` independiente, pero un binario `bun build --compile` no puede cargar dinámicamente `@sayknow-cli/natives`, por lo que las skills fallan con `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'`. Ejecutar desde el código fuente mediante `dev:link` evita esto. Compila el binario solo al validar una release.\n\nEjecuta el CLI directamente desde el código fuente sin enlazarlo:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nLas definiciones de flujo de trabajo predeterminadas viven en el código fuente, no en copias `.skc` comprometidas:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nPara cambios en las definiciones de flujo de trabajo o en la superficie de rebranding, ejecuta las puertas del proyecto:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nPara un mapa paquete por paquete, consulta [`docs/codebase-overview.md`](docs/codebase-overview.md).\n\n## Contributors\n\nLas contribuciones, los informes de errores y la validación de releases son bienvenidos a través de GitHub Issues y Pull Requests.\n\n## Inspiraciones y linaje\n\nLa identidad predeterminada de la TUI de Sayknow-CLI es la familia de pulpos: ink-octopus como predeterminado oscuro, blue-octopus como predeterminado claro y red-octopus, glow-octopus y violet-octopus como alternativas. También incluye los temas de migración `claude-code`, `codex` y `opencode`, cuyas paletas están inspiradas en esas herramientas para que los usuarios que migran de ellas obtengan un aspecto familiar. Se basa en las lecciones de una pequeña familia de arneses de agentes mientras mantiene la superficie pública de SKC intencionadamente enfocada. La atribución histórica se conserva en [`NOTICE.md`](NOTICE.md).\n\n## License\n\nMIT. Consulta [`LICENSE`](LICENSE).\n",
74
+ "readme/README.fr.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Illustration héros de l'agent de codage autonome Sayknow-CLI\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>Coder devrait ressembler à réfléchir.</strong><br />\n Un exécuteur d'agent de codage ciblé pour les entretiens, les plans révisés, l'exécution native tmux et la vérification durable.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <b>Français</b> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Mascotte personnage de Sayknow-CLI\" width=\"320\" />\n</p>\n\n> Sayknow-CLI est un projet expérimental en phase bêta. Attendez-vous à des aspérités et vérifiez les résultats avant de vous y fier pour un travail important.\n\n## Languages\n\nL'interface est localisée en **7 langues** — English, 한국어 (coréen),\n中文 (简体 / chinois simplifié), 日本語 (japonais), Español (espagnol),\nFrançais (français) et Deutsch (allemand). Elle détecte automatiquement la locale de votre système au\npremier lancement ; changez-en à tout moment dans **Settings → Appearance → Language**, ou lancez\navec par exemple `LANG=ja_JP.UTF-8 skc`. Les chaînes non traduites se rabattent sur l'anglais, et\nles noms de marque/techniques (Claude, OpenAI, MCP, …) restent verbatim dans toutes les locales.\n\n## What is Sayknow-CLI?\n\nSayknow-CLI (`skc`) est un harnais d'agent de codage externe. Il s'exécute depuis le dépôt ou le worktree que vous choisissez, puis donne à l'agent une surface de workflow réduite et explicite :\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nCe n'est volontairement pas un plugin caché pour Codex CLI, Claude Code, OpenCode ou Claw Code. Lancez `skc` à côté de ces outils lorsque vous voulez une planification structurée, des preuves persistantes, des workers adossés à tmux, ou un worktree isolé.\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # ou : bun install -g sayknow-cli\nskc --version\n```\n\nLe paquet embarque des binaires natifs précompilés pour macOS, Linux et Windows : aucune chaîne d'outils Rust ni étape de compilation. Pour mettre à jour : `npm install -g sayknow-cli@latest` ou lancez `skc update` dans le terminal.\n\n> Vous veniez d'une installation depuis les sources (git clone) ? Basculez une seule fois : `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`. Pour l'installation depuis les sources (développement), voir le [README en anglais](../../README.md#install-from-source-development).\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nÀ l'intérieur d'une session SKC, utilisez la surface de workflow publique :\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\nAjoutez `skc team ...` uniquement lorsque des workers tmux coordonnés aident concrètement.\n\n## Core capabilities\n\n- **Interviewer avant de deviner** : `deep-interview` transforme des demandes vagues en exigences concrètes.\n- **Planifier avant de muter** : `ralplan` révise l'approche avant les changements de code.\n- **Exécuter avec des preuves** : `ultragoal` suit les objectifs, les révisions, les vérifications et les preuves de complétion.\n- **Paralléliser quand c'est utile** : `team` coordonne des workers adossés à tmux pour les tâches plus importantes.\n- **Rester externe et révisable** : exécutez depuis un dépôt ou un worktree choisi sans patcher un autre runtime d'agent.\n\n## Workflow surface\n\nSayknow-CLI fournit quatre skills de workflow par défaut :\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | Clarifie les exigences ambiguës avant la planification ou les changements de code. |\n| `ralplan` | Construit et critique un plan d'implémentation avant la mutation. |\n| `ultragoal` | Suit les objectifs à travers l'exécution, la révision, la vérification et les preuves. |\n| `team` | Coordonne des workers adossés à tmux lorsque l'exécution parallèle en vaut la peine. |\n\nEt quatre agents de rôle inclus :\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | Implémentation bornée, correctifs et refactorisations. |\n| `architect` | Évaluation d'architecture et de revue de code en lecture seule. |\n| `planner` | Séquençage et critères d'acceptation en lecture seule. |\n| `critic` | Critique de plan et revue d'actionnabilité en lecture seule. |\n\nPas de ménagerie tentaculaire de skills par défaut : SKC s'améliore en rendant cette petite méthode meilleure.\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree` nomme un worktree frère géré par SKC ; pour un chemin existant, faites d'abord `cd` à cet endroit. |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC ne devient pas une extension de Claude Code. |\n| OpenCode | `skc` or `skc --tmux` | Workflow d'exécuteur externe uniquement aujourd'hui. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC ne s'installe pas dans Claw Code et ne le remplace pas. |\n| External controller / bot | `skc mcp-serve coordinator` plus `skc setup hermes` for compatible config, or `skc --mode rpc` for a subprocess worker | Tout bot capable de MCP/RPC pilote SKC via le contrat générique coordinator/RPC, et non par grattage de scrollback. |\n\nPour la configuration générique d'un bot tiers et les smokes indépendants du provider, voir [`docs/bot-integration.md`](docs/bot-integration.md). Pour la classification de readiness à travers les surfaces MCP, RPC, ACP et Bridge/HTTPS, voir [`docs/external-control-readiness.md`](docs/external-control-readiness.md). Pour les détails de protocole de plus bas niveau, voir [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md) et [`docs/bridge.md`](docs/bridge.md). Pour la roadmap des surfaces d'opérateur distant, voir [`docs/sayknow-remote.md`](docs/sayknow-remote.md) (volant de direction web) et [`docs/telegram-remote.md`](docs/telegram-remote.md) (bouton de cycle de vie Telegram).\n\n## Configuration\n\nLes budgets de retry du provider se trouvent dans `~/.skc/config.yml` :\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` s'applique avant qu'un stream ne soit établi. `streamMaxRetries` ne s'applique qu'aux échecs de stream transitoires sûrs pour le replay. L'authentification invalide, les modèles/providers non pris en charge, les requêtes malformées, le débordement de contexte, les abandons par l'utilisateur et les échecs de quota permanents restent en fail-fast.\n\n## TUI identity\n\nL'identité TUI par défaut est le thème SKC **ink-octopus** — encre de poulpe : graphite chaud, texte couleur papier, un seul accent ambre — pour les terminaux sombres, avec **blue-octopus** pour les terminaux clairs. Une variante chaleureuse **red-octopus** est également incluse pour ceux qui préfèrent une palette plus sombre et à fort contraste. Trois thèmes de migration supplémentaires — `claude-code`, `codex` et `opencode` — reflètent l'apparence de ces outils pour faciliter la migration visuelle et sont sélectionnables depuis Settings ou `/theme`. Les réglages de thème explicites de l'utilisateur l'emportent toujours.\n\n### Bundled theme grid\n\nChoisissez depuis Settings (`Appearance -> Dark theme` / `Light theme`) ou `/theme`.\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | Palette poulpe bleu avec des accents bleu tentacule. | Par défaut pour les terminaux clairs. |\n| `red-octopus` | Variante chaleureuse poulpe rouge avec un fort contraste d'état. | Alternative sombre à fort contraste. |\n| `ink-octopus` | Encre de poulpe — fond graphite chaud, texte couleur papier, un seul accent ambre. | Par défaut pour les terminaux sombres. |\n| `glow-octopus` | Bioluminescence abyssale — noir sarcelle avec un vert lumineux et un violet secondaire. | Terminaux sombres qui veulent un accent vif. |\n| `violet-octopus` | Sombre prune avec des accents lavande et un abricot secondaire. | Alternative sombre douce et colorée. |\n| `claude-code` | Palette sombre inspirée de Claude Code avec des touches terracotta et rose. | La mémoire musculaire de Claude Code sans quitter SKC. |\n| `codex` | Palette bleu-gris sombre et nette avec un contraste de session de codage plus marqué. | Un espace de travail sombre à la manière de Codex. |\n| `opencode` | Palette sombre inspirée d'OpenCode avec des accents de terminal plus percutants. | La mémoire musculaire d'OpenCode dans le sélecteur inclus. |\n\n## Development\n\nInstallez les dépendances, compilez les bindings natifs et configurez les valeurs par défaut locales :\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\nLe binaire `.node` pour `@sayknow-cli/natives` est gitignored et requis avant toute invocation de la CLI (`install:defaults`, `dev:link`, tests).\n\n### Canonical: build and link the dev `skc`\n\nPour que la commande globale `skc` exécute **la source TypeScript de ce checkout** (sensible à chaque édition, avec skills/natives fonctionnels), liez-la à votre `PATH` :\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` crée un lien symbolique `skc` → `packages/coding-agent/src/cli.ts` dans `~/.local/bin` (à surcharger avec `SKC_DEV_LINK_DIR`), remplace cette cible gérée, avertit et échoue si un autre `skc` le masque encore plus tôt sur le `PATH`, et exécute `--smoke-test` pour confirmer que `@sayknow-cli/natives` se charge. Utilisez `bun run install:dev` pour le bootstrap complet (install + link + `setup defaults`).\n\nVérifiez à tout moment si votre `skc` a dérivé (mauvaise source, ou un binaire compilé qui ne peut pas charger les skills) :\n\n```sh\nbun run dev:doctor\n```\n\n> N'utilisez **pas** le binaire compilé pour le développement quotidien. `bun --cwd=packages/coding-agent run build` produit un `dist/skc` autonome, mais un binaire `bun build --compile` ne peut pas charger dynamiquement `@sayknow-cli/natives`, donc les skills échouent avec `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'`. L'exécution depuis la source via `dev:link` évite cela. Ne compilez le binaire que lors de la validation d'une release.\n\nExécutez la CLI depuis la source directement sans lier :\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nLes définitions de workflow par défaut résident dans la source, et non dans des copies `.skc` commitées :\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nPour les changements de définition de workflow ou de surface de rebrand, exécutez les portes du projet :\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nPour une carte package par package, voir [`docs/codebase-overview.md`](docs/codebase-overview.md).\n\n## Contributors\n\nLes contributions, les rapports de bugs et la validation de release sont les bienvenus via les GitHub Issues et les Pull Requests.\n\n## Inspirations and lineage\n\nL'identité TUI par défaut de Sayknow-CLI est la famille de poulpes : ink-octopus par défaut en sombre, blue-octopus par défaut en clair, et red-octopus, glow-octopus et violet-octopus en alternatives. Il inclut aussi les thèmes de migration `claude-code`, `codex` et `opencode` dont les palettes sont inspirées de ces outils afin que les utilisateurs qui en proviennent retrouvent une apparence familière. Il s'appuie sur les leçons d'une petite famille de harnais d'agents tout en gardant la surface publique SKC volontairement ciblée. L'attribution historique est conservée dans [`NOTICE.md`](NOTICE.md).\n\n## License\n\nMIT. Voir [`LICENSE`](LICENSE).\n",
75
+ "readme/README.ja.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI 自律型コーディングエージェントのヒーローイラスト\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>コーディングは、考えることのように感じられるべきだ。</strong><br />\n インタビュー、レビュー済みプラン、tmux ネイティブ実行、そして永続的な検証のための、集中型コーディングエージェントランナー。\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <a href=\"README.zh.md\">中文</a> ·\n <b>日本語</b> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI キャラクターマスコット\" width=\"320\" />\n</p>\n\n> Sayknow-CLI は実験的なベータ段階のプロジェクトです。粗削りな部分があることを想定し、重要な作業で頼る前には出力を検証してください。\n\n## Languages\n\nインターフェースは **7 言語** にローカライズされています — English、한국어 (韓国語)、\n中文 (简体 / 簡体字中国語)、日本語 (Japanese)、Español (スペイン語)、\nFrançais (フランス語)、そして Deutsch (ドイツ語)。初回起動時にシステムロケールを\n自動検出します。**Settings → Appearance → Language** でいつでも切り替えられるほか、\nたとえば `LANG=ja_JP.UTF-8 skc` のように起動することもできます。未翻訳の文字列は英語に\nフォールバックし、ブランド名や技術名 (Claude、OpenAI、MCP、…) はすべてのロケールで\nそのまま表示されます。\n\n## Sayknow-CLI とは?\n\nSayknow-CLI (`skc`) は外部コーディングエージェントのハーネスです。選択したリポジトリまたは worktree から実行され、エージェントに対して小さく明示的なワークフロー面を提供します:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\nこれは意図的に、Codex CLI、Claude Code、OpenCode、Claw Code 向けの隠しプラグインにはなっていません。構造化されたプランニング、永続的なエビデンス、tmux ベースのワーカー、または分離された worktree が欲しいときに、それらのツールと並べて `skc` を起動してください。\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # または: bun install -g sayknow-cli\nskc --version\n```\n\nmacOS・Linux・Windows 向けのビルド済みネイティブを同梱しているため、Rust ツールチェーンやビルド手順は不要です。更新は `npm install -g sayknow-cli@latest`、またはターミナルで `skc update`。\n\n> 以前ソース(git clone)からインストールした場合は、一度だけ `rm -f ~/.local/bin/skc && npm install -g sayknow-cli` で切り替えてください。ソース/開発インストールは[英語版 README](../../README.md#install-from-source-development)を参照。\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nSKC セッション内では、公開されているワークフロー面を使用してください:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\n`skc team ...` は、協調する tmux ワーカーが実質的に役立つときにのみ追加してください。\n\n## Core capabilities\n\n- **推測する前にインタビューする**: `deep-interview` は曖昧なリクエストを具体的な要件に変えます。\n- **変更する前にプランニングする**: `ralplan` はコード変更の前にアプローチをレビューします。\n- **エビデンスとともに実行する**: `ultragoal` はゴール、リビジョン、チェック、完了エビデンスを追跡します。\n- **役立つときに並列化する**: `team` はより大きなタスクのために tmux ベースのワーカーを協調させます。\n- **外部かつレビュー可能であり続ける**: 別のエージェントランタイムにパッチを当てることなく、選択したリポジトリまたは worktree から実行します。\n\n## Workflow surface\n\nSayknow-CLI は 4 つのデフォルトワークフロースキルを同梱しています:\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | プランニングやコード変更の前に、曖昧な要件を明確化します。 |\n| `ralplan` | 変更の前に実装プランを構築し批評します。 |\n| `ultragoal` | 実行、リビジョン、検証、エビデンスを通じてゴールを追跡します。 |\n| `team` | 並列実行に価値があるときに tmux ベースのワーカーを協調させます。 |\n\nそして 4 つの同梱ロールエージェント:\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | 範囲を限定した実装、修正、リファクタリング。 |\n| `architect` | 読み取り専用のアーキテクチャおよびコードレビュー評価。 |\n| `planner` | 読み取り専用のシーケンシングと受け入れ基準。 |\n| `critic` | 読み取り専用のプラン批評と実行可能性レビュー。 |\n\n肥大化したデフォルトスキルの動物園はありません: SKC はこの小さなメソッドをより良くすることで改善されます。\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree` は SKC が管理する兄弟 worktree に名前を付けます。既存のパスを使う場合は、まずそこへ `cd` してください。 |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC は Claude Code の拡張機能にはなりません。 |\n| OpenCode | `skc` or `skc --tmux` | 現時点では外部ランナーのワークフローのみです。 |\n| Claw Code | `skc --tmux --worktree <name>` | SKC は Claw Code にインストールされたり、置き換えたりはしません。 |\n| External controller / bot | `skc mcp-serve coordinator` plus `skc setup hermes` for compatible config, or `skc --mode rpc` for a subprocess worker | MCP/RPC 対応のボットはどれも、スクロールバックのスクレイピングではなく、汎用のコーディネーター/RPC コントラクトを通じて SKC を駆動します。 |\n\n汎用的なサードパーティボットのセットアップとプロバイダー非依存のスモークテストについては、[`docs/bot-integration.md`](docs/bot-integration.md) を参照してください。MCP、RPC、ACP、Bridge/HTTPS 各面にわたる準備状況の分類については、[`docs/external-control-readiness.md`](docs/external-control-readiness.md) を参照してください。より低レベルのプロトコル詳細については、[`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md)、[`docs/rpc.md`](docs/rpc.md)、および [`docs/bridge.md`](docs/bridge.md) を参照してください。リモートオペレーター面のロードマップについては、[`docs/sayknow-remote.md`](docs/sayknow-remote.md) (web steering wheel) と [`docs/telegram-remote.md`](docs/telegram-remote.md) (Telegram lifecycle button) を参照してください。\n\n## Configuration\n\nプロバイダーのリトライバジェットは `~/.skc/config.yml` にあります:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` はストリームが確立される前に適用されます。`streamMaxRetries` はリプレイ安全な一時的ストリーム障害にのみ適用されます。無効な認証、サポートされていないモデル/プロバイダー、不正な形式のリクエスト、コンテキストオーバーフロー、ユーザーによる中断、および恒久的なクォータ障害は、引き続きフェイルファストのままです。\n\n## TUI identity\n\nデフォルトの TUI アイデンティティは SKC の **ink-octopus** テーマ — タコの墨: 温かみのあるグラファイト、紙色の文字、琥珀色のアクセント 1 色 — でダークターミナルに適用され、ライトターミナルには **blue-octopus** が適用されます。より暗めでハイコントラストなパレットを好む人のために、温かみのある **red-octopus** バリアントも同梱されています。さらに 3 つの移行用テーマ — `claude-code`、`codex`、`opencode` — がそれらのツールの見た目を再現しており、視覚的な移行を容易にし、Settings または `/theme` から選択できます。ユーザーが明示的に設定したテーマは引き続き優先されます。\n\n### Bundled theme grid\n\nSettings (`Appearance -> Dark theme` / `Light theme`) または `/theme` から選択してください。\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | テンタクルブルーのアクセントを持つ青いタコのパレット。 | ライトターミナルのデフォルト。 |\n| `red-octopus` | 強いステータスコントラストを持つ温かみのある赤いタコのバリアント。 | ハイコントラストなダークの代替。 |\n| `ink-octopus` | タコの墨 — 温かみのあるグラファイト背景、紙色の文字、琥珀色のアクセント 1 色。 | ダークターミナルのデフォルト。 |\n| `glow-octopus` | 深海の生物発光 — ティールがかった黒に発光グリーン、サブにバイオレット。 | 鮮やかなアクセントが欲しいダークターミナル。 |\n| `violet-octopus` | プラム色のダークにラベンダーのアクセント、サブにアプリコット。 | 柔らかくカラフルなダークの代替。 |\n| `claude-code` | テラコッタとピンクのハイライトを持つ Claude Code 風のダークパレット。 | SKC を離れずに Claude Code の体に染み付いた操作感を。 |\n| `codex` | よりシャープなコーディングセッションのコントラストを持つ、くっきりしたダークブルーグレーのパレット。 | Codex ライクなダークワークスペース。 |\n| `opencode` | よりパンチの効いたターミナルアクセントを持つ OpenCode 風のダークパレット。 | 同梱のピッカーで OpenCode の体に染み付いた操作感を。 |\n\n## Development\n\n依存関係をインストールし、ネイティブバインディングをビルドし、ローカルのデフォルトをセットアップします:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\n`@sayknow-cli/natives` 用の `.node` バイナリは gitignore されており、あらゆる CLI 呼び出し (`install:defaults`、`dev:link`、テスト) の前に必要です。\n\n### Canonical: build and link the dev `skc`\n\nグローバルの `skc` コマンドが **このチェックアウトの TypeScript ソース** を実行するようにする (すべての編集に即座に反映され、スキル/ネイティブが動作する) には、それをあなたの `PATH` にリンクします:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` は `skc` → `packages/coding-agent/src/cli.ts` を `~/.local/bin` にシンボリックリンクし (`SKC_DEV_LINK_DIR` で上書き可能)、その管理対象ターゲットを置き換え、別の `skc` が `PATH` 上でより前にそれをシャドウしている場合は警告して失敗し、`--smoke-test` を実行して `@sayknow-cli/natives` がロードされることを確認します。完全なブートストラップ (install + link + `setup defaults`) には `bun run install:dev` を使用してください。\n\nあなたの `skc` がドリフトしていないか (誤ったソース、またはスキルをロードできないコンパイル済みバイナリ) は、いつでも確認できます:\n\n```sh\nbun run dev:doctor\n```\n\n> 日常の開発にコンパイル済みバイナリを **使わないでください**。`bun --cwd=packages/coding-agent run build` はスタンドアロンの `dist/skc` を生成しますが、`bun build --compile` のバイナリは `@sayknow-cli/natives` を動的にロードできないため、スキルは `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'` で失敗します。`dev:link` を通じてソースから実行すれば、これを回避できます。バイナリのビルドはリリースを検証するときのみ行ってください。\n\nリンクせずに CLI をソースから直接実行します:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\nデフォルトのワークフロー定義はソースにあり、コミットされた `.skc` のコピーにはありません:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nワークフロー定義またはリブランド面の変更については、プロジェクトのゲートを実行してください:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\nパッケージごとのマップについては、[`docs/codebase-overview.md`](docs/codebase-overview.md) を参照してください。\n\n## Contributors\n\nコントリビューション、バグレポート、リリース検証は GitHub の Issues と Pull Request を通じて歓迎しています。\n\n## Inspirations and lineage\n\nSayknow-CLI のデフォルト TUI アイデンティティはタコのファミリーです: ダークのデフォルト ink-octopus、ライトのデフォルト blue-octopus、そして red-octopus・glow-octopus・violet-octopus の代替。また、`claude-code`、`codex`、`opencode` の移行用テーマも同梱しており、これらのパレットはそれらのツールにインスパイアされているため、移行してくるユーザーが見慣れた見た目を得られます。これは、公開された SKC 面を意図的に集中させたまま、小さなエージェントハーネス一族からの教訓の上に構築されています。歴史的な帰属表示は [`NOTICE.md`](NOTICE.md) に保持されています。\n\n## License\n\nMIT。[`LICENSE`](LICENSE) を参照してください。\n",
76
+ "readme/README.ko.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI autonomous coding-agent hero illustration\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>코딩은 사고처럼 느껴져야 합니다.</strong><br />\n 인터뷰, 검토된 계획, tmux 네이티브 실행, 견고한 검증을 위한 집중형 코딩 에이전트 러너.\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <b>한국어</b> ·\n <a href=\"README.zh.md\">中文</a> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI character mascot\" width=\"320\" />\n</p>\n\n> Sayknow-CLI는 실험적인 베타 단계 프로젝트입니다. 거친 부분이 있을 수 있으니 중요한 작업에 의존하기 전에 출력 결과를 검증하세요.\n\n## Languages\n\n인터페이스는 **7개 언어** — English, 한국어 (Korean),\n中文 (简体 / Simplified Chinese), 日本語 (Japanese), Español (Spanish),\nFrançais (French), Deutsch (German) — 로 현지화되어 있습니다. 첫 실행 시\n시스템 로케일을 자동으로 감지하며, 언제든지 **Settings → Appearance → Language** 에서\n전환하거나 예를 들어 `LANG=ja_JP.UTF-8 skc` 로 실행할 수 있습니다. 번역되지 않은 문자열은\nEnglish로 대체되며, 브랜드/기술 이름(Claude, OpenAI, MCP, …)은 모든 로케일에서 그대로 유지됩니다.\n\n## What is Sayknow-CLI?\n\nSayknow-CLI(`skc`)는 외부 코딩 에이전트 하니스입니다. 선택한 저장소나 워크트리에서 실행되며, 에이전트에게 작고 명시적인 워크플로 표면을 제공합니다:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\n이것은 의도적으로 Codex CLI, Claude Code, OpenCode, Claw Code의 숨겨진 플러그인이 아닙니다. 구조화된 계획, 지속적인 증거, tmux 기반 워커, 또는 격리된 워크트리를 원할 때 이러한 도구들 옆에서 `skc`를 시작하세요.\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # 또는: bun install -g sayknow-cli\nskc --version\n```\n\n프리빌드 네이티브(macOS·Linux·Windows)가 포함돼 있어 Rust 툴체인이나 빌드가 필요 없습니다. 업데이트는 `npm install -g sayknow-cli@latest` 또는 터미널에서 `skc update`.\n\n> 이전에 소스(git clone)로 설치했다면 한 번만 `rm -f ~/.local/bin/skc && npm install -g sayknow-cli`로 전환하세요. 소스/개발 설치는 [영문 README](../../README.md#install-from-source-development) 참고.\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\nSKC 세션 내부에서는, 공개 워크플로 표면을 사용하세요:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\n조율된 tmux 워커가 실질적으로 도움이 될 때만 `skc team ...`을 추가하세요.\n\n## Core capabilities\n\n- **추측하기 전에 인터뷰**: `deep-interview`는 모호한 요청을 구체적인 요구사항으로 바꿉니다.\n- **변경하기 전에 계획**: `ralplan`은 코드 변경 전에 접근 방식을 검토합니다.\n- **증거와 함께 실행**: `ultragoal`은 목표, 수정, 점검, 완료 증거를 추적합니다.\n- **유용할 때 병렬화**: `team`은 더 큰 작업을 위해 tmux 기반 워커를 조율합니다.\n- **외부에서 검토 가능하게 유지**: 다른 에이전트 런타임을 패치하지 않고 선택한 저장소나 워크트리에서 실행합니다.\n\n## Workflow surface\n\nSayknow-CLI는 네 가지 기본 워크플로 스킬을 제공합니다:\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | 계획이나 코드 변경 전에 모호한 요구사항을 명확히 합니다. |\n| `ralplan` | 변경 전에 구현 계획을 구축하고 비평합니다. |\n| `ultragoal` | 실행, 수정, 검증, 증거를 거쳐 목표를 추적합니다. |\n| `team` | 병렬 실행이 가치가 있을 때 tmux 기반 워커를 조율합니다. |\n\n그리고 네 가지 번들 역할 에이전트:\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | 범위가 정해진 구현, 수정, 리팩터. |\n| `architect` | 읽기 전용 아키텍처 및 코드 리뷰 평가. |\n| `planner` | 읽기 전용 순서 결정 및 수용 기준. |\n| `critic` | 읽기 전용 계획 비평 및 실행 가능성 검토. |\n\n광범위한 기본 스킬 동물원은 없습니다: SKC는 이 작은 방법을 더 좋게 만들어 개선됩니다.\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree`는 SKC가 관리하는 형제 워크트리의 이름을 지정합니다. 기존 경로의 경우 먼저 그곳으로 `cd` 하세요. |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC는 Claude Code 확장이 되지 않습니다. |\n| OpenCode | `skc` or `skc --tmux` | 현재로서는 외부 러너 워크플로만 지원합니다. |\n| Claw Code | `skc --tmux --worktree <name>` | SKC는 Claw Code에 설치되거나 그것을 대체하지 않습니다. |\n| External controller / bot | 호환 가능한 구성을 위한 `skc mcp-serve coordinator` 및 `skc setup hermes`, 또는 서브프로세스 워커를 위한 `skc --mode rpc` | MCP/RPC 지원 봇이라면 무엇이든 스크롤백 스크래핑이 아니라 일반 coordinator/RPC 계약을 통해 SKC를 구동합니다. |\n\n일반 서드파티 봇 설정 및 공급자 독립적 스모크에 대해서는 [`docs/bot-integration.md`](docs/bot-integration.md)를 참조하세요. MCP, RPC, ACP, Bridge/HTTPS 표면 전반의 준비도 분류에 대해서는 [`docs/external-control-readiness.md`](docs/external-control-readiness.md)를 참조하세요. 더 낮은 수준의 프로토콜 세부 사항에 대해서는 [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md), [`docs/rpc.md`](docs/rpc.md), [`docs/bridge.md`](docs/bridge.md)를 참조하세요. 원격 운영자 표면 로드맵에 대해서는 [`docs/sayknow-remote.md`](docs/sayknow-remote.md)(웹 스티어링 휠) 및 [`docs/telegram-remote.md`](docs/telegram-remote.md)(Telegram 라이프사이클 버튼)를 참조하세요.\n\n## Configuration\n\n공급자 재시도 예산은 `~/.skc/config.yml`에 있습니다:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries`는 스트림이 설정되기 전에 적용됩니다. `streamMaxRetries`는 재생 안전한 일시적 스트림 실패에만 적용됩니다. 잘못된 인증, 지원되지 않는 모델/공급자, 잘못된 형식의 요청, 컨텍스트 오버플로, 사용자 중단, 영구적인 할당량 실패는 즉시 실패(fail-fast)로 유지됩니다.\n\n## TUI identity\n\n기본 TUI 정체성은 SKC **ink-octopus** 테마 — 문어 먹물: 따뜻한 흑연색, 종이색 글자, 호박색 강조 하나 — 로 다크 터미널에 적용되며, 라이트 터미널에는 **blue-octopus**가 적용됩니다. 더 어둡고 고대비 팔레트를 선호하는 사람들을 위해 따뜻한 **red-octopus** 변형도 번들로 제공됩니다. 세 가지 추가 마이그레이션 테마 — `claude-code`, `codex`, `opencode` — 는 쉬운 눈 마이그레이션을 위해 해당 도구들의 모습을 그대로 따르며 Settings 또는 `/theme`에서 선택할 수 있습니다. 명시적인 사용자 테마 설정이 여전히 우선합니다.\n\n### Bundled theme grid\n\nSettings (`Appearance -> Dark theme` / `Light theme`) 또는 `/theme`에서 선택하세요.\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | 촉수 블루 액센트가 있는 파란 문어 팔레트. | 라이트 터미널의 기본값. |\n| `red-octopus` | 강한 상태 대비를 가진 따뜻한 빨간 문어 변형. | 고대비 다크 대안. |\n| `ink-octopus` | 문어 먹물 — 따뜻한 흑연색 배경, 종이색 글자, 호박색 강조 하나. | 다크 터미널의 기본값. |\n| `glow-octopus` | 심해 생물발광 — 틸빛 어둠에 발광 초록, 보조로 보라. | 선명한 강조색을 원하는 다크 터미널. |\n| `violet-octopus` | 자줏빛 어둠에 라벤더 강조, 보조로 살구색. | 부드럽고 화사한 다크 대안. |\n| `claude-code` | 테라코타와 핑크 하이라이트가 있는 Claude Code 영감 다크 팔레트. | SKC를 떠나지 않고 Claude Code 근육 기억을 유지. |\n| `codex` | 더 날카로운 코딩 세션 대비를 가진 선명한 다크 블루그레이 팔레트. | Codex 같은 다크 작업 공간. |\n| `opencode` | 더 강렬한 터미널 액센트를 가진 OpenCode 영감 다크 팔레트. | 번들 선택기에서의 OpenCode 근육 기억. |\n\n## Development\n\n의존성을 설치하고, 네이티브 바인딩을 빌드하고, 로컬 기본값을 설정하세요:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\n`@sayknow-cli/natives`용 `.node` 바이너리는 gitignore되어 있으며 모든 CLI 호출(`install:defaults`, `dev:link`, 테스트) 전에 필요합니다.\n\n### Canonical: build and link the dev `skc`\n\n전역 `skc` 명령이 **이 체크아웃의 TypeScript 소스**(모든 편집에 즉시 반영되며, 스킬/네이티브가 작동함)를 실행하도록 하려면, `PATH`에 링크하세요:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link`는 `skc` → `packages/coding-agent/src/cli.ts`를 `~/.local/bin`에 심볼릭 링크하고(`SKC_DEV_LINK_DIR`로 재정의 가능), 그 관리되는 대상을 교체하며, 다른 `skc`가 여전히 `PATH`에서 더 앞쪽에 있어 그것을 가린다면 경고하고 실패하며, `--smoke-test`를 실행하여 `@sayknow-cli/natives`가 로드되는지 확인합니다. 전체 부트스트랩(install + link + `setup defaults`)을 위해서는 `bun run install:dev`를 사용하세요.\n\n당신의 `skc`가 표류했는지(잘못된 소스, 또는 스킬을 로드할 수 없는 컴파일된 바이너리) 언제든지 확인하세요:\n\n```sh\nbun run dev:doctor\n```\n\n> 일상적인 개발에는 컴파일된 바이너리를 **사용하지 마세요**. `bun --cwd=packages/coding-agent run build`는 독립 실행형 `dist/skc`를 생성하지만, `bun build --compile` 바이너리는 `@sayknow-cli/natives`를 동적으로 로드할 수 없으므로 스킬이 `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'`로 실패합니다. `dev:link`를 통해 소스에서 실행하면 이를 피할 수 있습니다. 릴리스를 검증할 때만 바이너리를 빌드하세요.\n\n링크 없이 소스에서 직접 CLI를 실행하세요:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\n기본 워크플로 정의는 커밋된 `.skc` 사본이 아니라 소스에 있습니다:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\n워크플로 정의 또는 리브랜드 표면 변경의 경우, 프로젝트 게이트를 실행하세요:\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\n패키지별 맵에 대해서는 [`docs/codebase-overview.md`](docs/codebase-overview.md)를 참조하세요.\n\n## Contributors\n\n기여, 버그 보고, 릴리스 검증은 GitHub Issues와 Pull Request를 통해 환영합니다.\n\n## Inspirations and lineage\n\nSayknow-CLI의 기본 TUI 정체성은 문어 가족입니다: 다크 기본값 ink-octopus, 라이트 기본값 blue-octopus, 그리고 red-octopus·glow-octopus·violet-octopus 대안. 또한 해당 도구들에서 옮겨오는 사용자들이 익숙한 모습을 얻도록 팔레트가 그 도구들에서 영감을 받은 `claude-code`, `codex`, `opencode` 마이그레이션 테마를 번들로 제공합니다. 공개 SKC 표면을 의도적으로 집중된 상태로 유지하면서, 작은 에이전트 하니스 계열에서 얻은 교훈을 바탕으로 만들어졌습니다. 역사적 출처 표기는 [`NOTICE.md`](NOTICE.md)에 보관되어 있습니다.\n\n## License\n\nMIT. [`LICENSE`](LICENSE)를 참조하세요.\n",
77
+ "readme/README.zh.md": "<p align=\"center\">\n <img src=\"../../assets/hero.png\" alt=\"Sayknow-CLI autonomous coding-agent hero illustration\" width=\"100%\" />\n</p>\n\n<h1 align=\"center\">Sayknow-CLI</h1>\n\n<p align=\"center\">\n <strong>编码应当如思考般自然。</strong><br />\n 一个专注的编码智能体运行器,面向访谈式需求澄清、经评审的计划、tmux 原生执行与持久化验证。\n</p>\n\n<p align=\"center\">\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/releases\"><img alt=\"Release\" src=\"https://img.shields.io/github/v/tag/jaybeyond/Sayknow_CLI?sort=semver&label=release&style=flat-square&color=2f9bff\"></a>\n <a href=\"LICENSE\"><img alt=\"License: MIT\" src=\"https://img.shields.io/github/license/jaybeyond/Sayknow_CLI?style=flat-square&color=green\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/stargazers\"><img alt=\"Stars\" src=\"https://img.shields.io/github/stars/jaybeyond/Sayknow_CLI?style=flat-square&color=f5c518\"></a>\n <a href=\"https://github.com/jaybeyond/Sayknow_CLI/issues\"><img alt=\"Issues\" src=\"https://img.shields.io/github/issues/jaybeyond/Sayknow_CLI?style=flat-square\"></a>\n <a href=\"https://bun.sh\"><img alt=\"Built with Bun\" src=\"https://img.shields.io/badge/built%20with-Bun-fbf0df?style=flat-square&logo=bun&logoColor=black\"></a>\n <a href=\"#languages\"><img alt=\"i18n\" src=\"https://img.shields.io/badge/i18n-7%20languages-2f9bff?style=flat-square\"></a>\n</p>\n\n<p align=\"center\">\n <a href=\"../../README.md\">English</a> ·\n <a href=\"README.ko.md\">한국어</a> ·\n <b>中文</b> ·\n <a href=\"README.ja.md\">日本語</a> ·\n <a href=\"README.es.md\">Español</a> ·\n <a href=\"README.fr.md\">Français</a> ·\n <a href=\"README.de.md\">Deutsch</a>\n</p>\n\n<p align=\"center\">\n <img src=\"../../assets/character.png\" alt=\"Sayknow-CLI character mascot\" width=\"320\" />\n</p>\n\n> Sayknow-CLI 是一个实验性的、处于 beta 阶段的项目。请预期会有粗糙之处,并在依赖其结果完成重要工作之前先行验证输出。\n\n## Languages\n\n界面已本地化为 **7 种语言**——English、한국어(韩语)、\n中文(简体)、日本語(日语)、Español(西班牙语)、\nFrançais(法语)以及 Deutsch(德语)。首次运行时它会自动检测你的系统区域设置;\n你可以随时在 **Settings → Appearance → Language** 中切换,或者使用例如\n`LANG=ja_JP.UTF-8 skc` 的方式启动。未翻译的字符串会回退到英文,而\n品牌/技术名称(Claude、OpenAI、MCP……)在所有语言环境中均保持原样。\n\n## What is Sayknow-CLI?\n\nSayknow-CLI(`skc`)是一个外部编码智能体框架(harness)。它从你选择的仓库或工作树(worktree)中运行,然后为智能体提供一个精简、明确的工作流界面:\n\n```text\ndeep-interview -> ralplan -> ultragoal\n └─ optional team execution when parallel tmux workers help\n```\n\n它有意不做成 Codex CLI、Claude Code、OpenCode 或 Claw Code 的隐藏插件。当你需要结构化的规划、持久化的证据、tmux 支持的工作进程或一个隔离的工作树时,就在这些工具旁边启动 `skc`。\n\n## Install\n\n```sh\nnpm install -g sayknow-cli # 或:bun install -g sayknow-cli\nskc --version\n```\n\n已内置 macOS·Linux·Windows 的预编译原生模块,无需 Rust 工具链或构建步骤。更新:`npm install -g sayknow-cli@latest` 或在终端运行 `skc update`。\n\n> 如果你之前是从源码(git clone)安装的,只需一次性切换:`rm -f ~/.local/bin/skc && npm install -g sayknow-cli`。源码/开发安装请参见[英文 README](../../README.md#install-from-source-development)。\n\n## Quick start\n\n```sh\n# Run directly in the current checkout\nskc\n\n# Use a tmux-backed leader session\nskc --tmux\n\n# Use an isolated worktree for risky or reviewable work\n# --worktree takes an optional branch-like name, not a filesystem path.\nskc --tmux --worktree my-task-branch\n\n# If you already created a worktree directory, launch from that directory instead.\ncd ../my-task-worktree && skc --tmux\n```\n\n在 SKC 会话内部,使用公共工作流界面:\n\n```text\n/skill:deep-interview clarify ambiguous requirements\n/skill:ralplan build and critique the implementation plan\nskc ultragoal create-goals --brief-file <approved-plan>\nskc ultragoal complete-goals\n```\n\n仅当协同的 tmux 工作进程能带来实质性帮助时,才加上 `skc team ...`。\n\n## Core capabilities\n\n- **先访谈,不靠猜**:`deep-interview` 把模糊的请求转化为具体的需求。\n- **先规划,再变更**:`ralplan` 在代码改动之前评审方案。\n- **带证据地执行**:`ultragoal` 跟踪目标、修订、检查以及完成证据。\n- **在有用时并行化**:`team` 为较大的任务协调 tmux 支持的工作进程。\n- **保持外部化且可评审**:从所选的仓库或工作树中运行,无需给另一个智能体运行时打补丁。\n\n## Workflow surface\n\nSayknow-CLI 内置四项默认工作流技能:\n\n| Skill | What it does |\n| ---------------- | --------------------------------------------------------------------- |\n| `deep-interview` | 在规划或代码改动之前澄清模糊的需求。 |\n| `ralplan` | 在变更之前构建并评审实现计划。 |\n| `ultragoal` | 在执行、修订、验证与证据收集的全过程中跟踪目标。 |\n| `team` | 当并行执行值得时,协调 tmux 支持的工作进程。 |\n\n以及四个捆绑的角色智能体:\n\n| Agent | What it does |\n| ----------- | -------------------------------------------------- |\n| `executor` | 有边界的实现、修复与重构。 |\n| `architect` | 只读的架构与代码评审评估。 |\n| `planner` | 只读的排序与验收标准。 |\n| `critic` | 只读的计划评审与可执行性审查。 |\n\n没有庞杂的默认技能堆砌:SKC 通过把这一精简方法做得更好来持续改进。\n\n## Works beside your existing agent or bot\n\n| Tool or bot | Recommended SKC command | Boundary |\n| ----------- | ----------------------- | -------- |\n| Codex CLI | `skc --tmux --worktree <name>` or `skc` | `--worktree` 指定一个由 SKC 管理的同级工作树;对于已存在的路径,请先 `cd` 到那里。 |\n| Claude Code | `skc --tmux` or `skc --tmux --worktree <name>` | SKC 不会成为 Claude Code 的扩展。 |\n| OpenCode | `skc` or `skc --tmux` | 目前仅支持外部运行器(external-runner)工作流。 |\n| Claw Code | `skc --tmux --worktree <name>` | SKC 不会安装到 Claw Code 中,也不会替代它。 |\n| External controller / bot | `skc mcp-serve coordinator` plus `skc setup hermes` for compatible config, or `skc --mode rpc` for a subprocess worker | 任何具备 MCP/RPC 能力的 bot 都通过通用的 coordinator/RPC 契约来驱动 SKC,而非抓取滚动回显(scrollback scraping)。 |\n\n关于通用第三方 bot 的设置以及与提供商无关的冒烟测试,请参阅 [`docs/bot-integration.md`](docs/bot-integration.md)。关于在 MCP、RPC、ACP 与 Bridge/HTTPS 各界面上的就绪度分级,请参阅 [`docs/external-control-readiness.md`](docs/external-control-readiness.md)。关于更底层的协议细节,请参阅 [`docs/hermes-mcp-bridge.md`](docs/hermes-mcp-bridge.md)、[`docs/rpc.md`](docs/rpc.md) 以及 [`docs/bridge.md`](docs/bridge.md)。关于远程操作员界面的路线图,请参阅 [`docs/sayknow-remote.md`](docs/sayknow-remote.md)(网页方向盘)以及 [`docs/telegram-remote.md`](docs/telegram-remote.md)(Telegram 生命周期按钮)。\n\n## Configuration\n\n提供商重试预算位于 `~/.skc/config.yml`:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n maxRetries: 3\n maxDelayMs: 300000\n```\n\n`requestMaxRetries` 在流(stream)建立之前生效。`streamMaxRetries` 仅适用于可安全重放的瞬时流失败。无效的认证、不受支持的模型/提供商、格式错误的请求、上下文溢出、用户中止以及永久性配额失败仍然保持快速失败(fail-fast)。\n\n## TUI identity\n\n默认的 TUI 标识是 SKC 的 **ink-octopus**(章鱼墨汁)主题——温暖的石墨色、纸色文字、唯一的琥珀色强调——用于深色终端,浅色终端使用 **blue-octopus**(蓝章鱼)。还捆绑了一个暖色调的 **red-octopus**(红章鱼)变体,供偏好更深、高对比度配色的用户使用。另有三个迁移主题——`claude-code`、`codex` 和 `opencode`——分别复刻了这些工具的外观,以便于视觉迁移,可从 Settings 或 `/theme` 中选择。显式的用户主题设置仍然优先生效。\n\n### Bundled theme grid\n\n从 Settings(`Appearance -> Dark theme` / `Light theme`)或 `/theme` 中选择。\n\n| Theme | Visual feel | Best fit |\n| --- | --- | --- |\n| `blue-octopus` | 蓝章鱼配色,带触手蓝点缀。 | 浅色终端的默认主题。 |\n| `red-octopus` | 暖色调红章鱼变体,状态对比强烈。 | 高对比度的深色替代方案。 |\n| `ink-octopus` | 章鱼墨汁 — 温暖的石墨色背景、纸色文字、唯一的琥珀色强调。 | 深色终端的默认主题。 |\n| `glow-octopus` | 深海生物荧光 — 青黑底色配荧光绿,辅以紫色。 | 需要鲜明强调色的深色终端。 |\n| `violet-octopus` | 梅紫色深色底配薰衣草强调,辅以杏色。 | 柔和多彩的深色备选。 |\n| `claude-code` | 受 Claude Code 启发的深色配色,带赤陶色和粉色高光。 | 在不离开 SKC 的情况下保留 Claude Code 的肌肉记忆。 |\n| `codex` | 清爽的深蓝灰配色,编码会话对比更锐利。 | 类似 Codex 的深色工作区。 |\n| `opencode` | 受 OpenCode 启发的深色配色,终端点缀更鲜明。 | 在捆绑选择器中保留 OpenCode 的肌肉记忆。 |\n\n## Development\n\n安装依赖、构建原生绑定,并设置本地默认值:\n\n```sh\nbun install\nbun run build:native\nbun run install:defaults\n```\n\n`@sayknow-cli/natives` 的 `.node` 二进制文件已被 gitignore,且在任何 CLI 调用(`install:defaults`、`dev:link`、测试)之前都是必需的。\n\n### Canonical: build and link the dev `skc`\n\n要让全局 `skc` 命令运行**此检出的 TypeScript 源码**(对每一次编辑都即时生效,且技能/原生绑定均可用),请把它链接到你的 `PATH`:\n\n```sh\nbun install\nbun run dev:link\n```\n\n`dev:link` 会把 `skc` → `packages/coding-agent/src/cli.ts` 软链接到 `~/.local/bin`(可用 `SKC_DEV_LINK_DIR` 覆盖),替换该受管目标,如果另一个 `skc` 仍在 `PATH` 上更靠前地遮蔽它则会发出警告并失败,并运行 `--smoke-test` 以确认 `@sayknow-cli/natives` 能够加载。使用 `bun run install:dev` 进行完整的引导(install + link + `setup defaults`)。\n\n随时检查你的 `skc` 是否已经漂移(源码错误,或一个无法加载技能的已编译二进制文件):\n\n```sh\nbun run dev:doctor\n```\n\n> 在日常开发中**不要**使用已编译的二进制文件。`bun --cwd=packages/coding-agent run build` 会产出一个独立的 `dist/skc`,但 `bun build --compile` 生成的二进制无法动态加载 `@sayknow-cli/natives`,因此技能会以 `Cannot find module '@sayknow-cli/natives' from '/$bunfs/root/skc'` 失败。通过 `dev:link` 从源码运行可避免此问题。仅在验证发布版本时才构建该二进制文件。\n\n不进行链接,直接从源码运行 CLI:\n\n```sh\nbun packages/coding-agent/src/cli.ts --help\n```\n\n默认工作流定义存放在源码中,而非已提交的 `.skc` 副本:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\n对于工作流定义或品牌重塑界面(rebrand-surface)的改动,请运行项目门禁(gates):\n\n```sh\nbun scripts/check-visible-definitions.ts\nbun scripts/verify-g002-gates.ts\nbun scripts/rebrand-inventory.ts --strict\nbun test packages/coding-agent/test/default-skc-definitions.test.ts\n```\n\n关于逐包(package-by-package)的对照图,请参阅 [`docs/codebase-overview.md`](docs/codebase-overview.md)。\n\n## Contributors\n\n欢迎通过 GitHub Issues 和 Pull Requests 进行贡献、提交错误报告以及参与发布验证。\n\n## Inspirations and lineage\n\nSayknow-CLI 默认的 TUI 标识是章鱼家族:深色默认 ink-octopus,浅色默认 blue-octopus,以及 red-octopus、glow-octopus、violet-octopus 备选。它还捆绑了 `claude-code`、`codex` 和 `opencode` 迁移主题,其配色受这些工具启发,以便从它们迁移过来的用户能获得熟悉的外观。它在一个小型智能体框架家族的经验之上构建,同时有意保持公共 SKC 界面的专注。历史归属保留在 [`NOTICE.md`](NOTICE.md) 中。\n\n## License\n\nMIT。参见 [`LICENSE`](LICENSE)。\n",
78
78
  "render-mermaid.md": "# RenderMermaid\n\n`RenderMermaid` is an optional built-in tool that renders Mermaid source to terminal-friendly text.\n\n## Enable it\n\nDisabled by default. Turn it on in `/settings` under **Tools → Render Mermaid**, or in `~/.skc/agent/config.yml`:\n\n```yaml\nrenderMermaid:\n enabled: true\n```\n\n## What it does\n\n- Tool name: `render_mermaid`\n- Input: Mermaid source in the required `mermaid` field\n- Output: rendered ASCII/Unicode text, not SVG or PNG\n- Storage: when artifact storage is available, the full render is also saved as an `artifact://...`\n\nThere are no model-specific or environment-variable prerequisites. Once enabled, any model that can call built-in tools can use it.\n\n## Parameters\n\n```json\n{\n \"mermaid\": \"graph TD\\n A[Start] --> B[Stop]\",\n \"config\": {\n \"useAscii\": false,\n \"paddingX\": 2,\n \"paddingY\": 2,\n \"boxBorderPadding\": 0\n }\n}\n```\n\nAvailable `config` fields:\n\n- `useAscii` — `true` for plain ASCII, `false` for Unicode box-drawing characters (default and usually more readable)\n- `paddingX` — horizontal spacing between nodes\n- `paddingY` — vertical spacing between nodes\n- `boxBorderPadding` — inner padding inside node boxes\n\n## Current limitations\n\n`RenderMermaid` uses the `beautiful-mermaid` ASCII renderer. It works best for flowcharts and small diagrams.\n\nComplex sequence diagrams, especially with `alt` / `else` blocks, can become very wide in a terminal. That is current renderer behavior, not a provider or model configuration problem.\n\nIf a sequence diagram is hard to read:\n\n1. Keep Unicode output (`useAscii: false`)\n2. Reduce spacing with a tighter config such as `paddingX: 2`, `paddingY: 2`, `boxBorderPadding: 0`\n3. Prefer smaller sub-diagrams over one large sequence diagram\n4. Open the saved artifact if the inline preview is truncated in the TUI\n\n## Example\n\nInput:\n\n```mermaid\ngraph TD\n A[Start] --> B{Decision}\n B -->|Yes| C[Action]\n B -->|No| D[End]\n```\n\nTypical result:\n\n```text\n┌─────┐\n│Start│\n└─────┘\n │\n ▼\n┌────────┐\n│Decision│\n└────────┘\n```\n",
79
79
  "research-plan-ledger.md": "# Research plan items and evidence ledger\n\nResearch/deep-research workflows need a planning contract that is stronger than an execution-order checklist. A plan item should name the claim under investigation, the uncertainty around it, what evidence is required, what counterexamples would falsify it, and how a verifier should handle source conflicts.\n\nThis document defines the public product-facing spike for issue #932. It intentionally avoids private operator, session, channel, and routing internals.\n\n## Research plan item schema\n\n```ts\ntype ResearchPlanConfidence = \"low\" | \"medium\" | \"high\";\n\ntype ResearchPlanItem = {\n claim: string;\n confidence: ResearchPlanConfidence;\n unknowns: string[];\n evidenceNeeded: string[];\n counterexampleQueries: string[];\n sourceConflictPolicy: string;\n dropCondition: string;\n verifierChecks: string[];\n};\n```\n\nField intent:\n\n- `claim`: The smallest claim that can survive or fail verification.\n- `confidence`: Planner's initial confidence before evidence collection.\n- `unknowns`: Known gaps the final answer must resolve or explicitly carry forward.\n- `evidenceNeeded`: Evidence workers must collect before the claim can be accepted.\n- `counterexampleQueries`: Directed search prompts for evidence that would weaken or falsify the claim.\n- `sourceConflictPolicy`: How the verifier treats conflicting sources, stale sources, or mismatched methodology.\n- `dropCondition`: The explicit condition that removes this claim from the final answer.\n- `verifierChecks`: Checklist the verifier applies before accepting the claim.\n\n## Evidence ledger schema\n\n```ts\ntype ResearchEvidenceVerdict = \"support\" | \"contradict\" | \"uncertain\";\n\ntype ResearchEvidenceEntry = {\n claim: string;\n source: string;\n confidence: ResearchPlanConfidence;\n verdict: ResearchEvidenceVerdict;\n notes?: string;\n};\n\ntype ResearchLedgerVerdict = {\n claim: string;\n finalVerdict: \"accepted\" | \"rejected\" | \"uncertain\";\n survivingSources: ResearchEvidenceEntry[];\n rejectReason?: string;\n unresolvedUnknowns: string[];\n};\n```\n\nThe ledger is claim-centric. Workers add evidence entries against plan-item claims; the verifier reduces those entries into a final verdict. Accepted claims can be cited in the final answer. Rejected claims are named with `rejectReason`. Uncertain claims are either excluded or marked explicitly as unresolved.\n\n## Ralplan/research workflow shape\n\n1. Planner emits `ResearchPlanItem[]` alongside the normal plan narrative when the task is research-heavy.\n2. Workers gather independent evidence for each item, including counterexample-oriented searches.\n3. Verifier checks contradictions, source quality, stale information, and unresolved uncertainty using the item's `verifierChecks`, `sourceConflictPolicy`, and `dropCondition`.\n4. The final answer cites accepted claims, lists rejected claims with reasons, and marks any surviving uncertainty.\n\n## Example\n\n```ts\nconst item: ResearchPlanItem = {\n claim: \"Model X reduces latency by 30% on production-like workloads\",\n confidence: \"medium\",\n unknowns: [\"production workload mix\"],\n evidenceNeeded: [\"benchmark with production-like fixture\", \"baseline comparison\"],\n counterexampleQueries: [\"regression on long-context workload\", \"cold-start latency increase\"],\n sourceConflictPolicy: \"Reject the claim when any credible counterexample contradicts the benchmark.\",\n dropCondition: \"Drop if a counterexample contradicts the claim or key unknowns remain unresolved.\",\n verifierChecks: [\"check source freshness\", \"compare benchmark harness\", \"inspect counterexample evidence\"],\n};\n```\n\nIf the ledger contains a supporting benchmark and a credible long-context counterexample, the verifier rejects the broad claim instead of letting a plausible summary survive by vibes.\n\n## Current spike\n\nThe first implementation spike lives in `packages/coding-agent/src/research-plan/ledger.ts` and provides:\n\n- TypeScript interfaces for research plan items, evidence entries, and final verdicts.\n- Validators for product-facing plan/evidence objects.\n- A deterministic verifier helper that rejects plausible claims when counterexample/source-conflict/drop-condition evidence applies.\n- Regression tests in `packages/coding-agent/test/research-plan-ledger.test.ts`.\n\nFuture runtime integration can make `/skill:ralplan` emit these structures as a fenced JSON block or structured sidecar in the persisted ralplan artifact. The spike keeps the schema independent from private session state so it can be exposed in docs and tests safely.\n",
80
80
  "resolve-tool-runtime.md": "# Resolve tool runtime internals\n\nThis document explains how preview/apply workflows are modeled in coding-agent and how built-in or custom tools can participate via the tool-choice queue and `pushPendingAction`.\n\n## Scope and key files\n\n- [`src/tools/resolve.ts`](../packages/coding-agent/src/tools/resolve.ts)\n- [`src/tools/ast-edit.ts`](../packages/coding-agent/src/tools/ast-edit.ts)\n- [`src/extensibility/custom-tools/types.ts`](../packages/coding-agent/src/extensibility/custom-tools/types.ts)\n- [`src/extensibility/custom-tools/loader.ts`](../packages/coding-agent/src/extensibility/custom-tools/loader.ts)\n- [`src/sdk/session.ts`](../packages/coding-agent/src/sdk/session.ts)\n\n## What `resolve` does\n\n`resolve` is a hidden tool that finalizes a pending preview action.\n\n- `action: \"apply\"` executes the queued action's `apply(reason)` callback and returns that result with resolve metadata.\n- `action: \"discard\"` invokes `reject(reason)` if provided; otherwise returns `Discarded: <label>. Reason: <reason>`.\n\nIf no pending action exists, `resolve` fails with:\n\n- `No pending action to resolve. Nothing to apply or discard.`\n\n## Pending actions use the tool-choice queue\n\nPreview producers call `queueResolveHandler(...)`, which pushes a one-shot forced `resolve` directive onto the session tool-choice queue and adds a `resolve-reminder` steering message.\n\nRuntime behavior:\n\n- the queued handler owns the pending `apply`/`reject` callbacks,\n- `resolve` looks up the current queue invoker with `session.peekQueueInvoker()`,\n- if the model rejects the forced tool choice, the queue directive is requeued,\n- `resolve` does not maintain a separate pending-action stack.\n\nMultiple pending previews therefore follow the active tool-choice queue ordering, not an independent pending-action store.\n\n## Built-in producer example (`ast_edit`)\n\n`ast_edit` previews structural replacements first. When the preview has replacements and is not applied yet, it queues a resolve handler that contains:\n\n- label (human-readable summary)\n- `sourceToolName` (`ast_edit`)\n- `apply(reason: string)` callback that reruns AST edit with `dryRun: false`\n\n`resolve(action=\"apply\", reason=\"...\")` passes `reason` into this callback.\n\n## Custom tools: `pushPendingAction`\n\nCustom tools can register resolve-compatible pending actions through `CustomToolAPI.pushPendingAction(...)`. The custom tool loader forwards these actions to `queueResolveHandler(...)` when that hook is available.\n\n`CustomToolPendingAction`:\n\n- `label: string` (required)\n- `apply(reason: string): Promise<AgentToolResult<unknown>>` (required) — invoked on apply; `reason` is the string passed to `resolve`\n- `reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>` (optional) — invoked on discard; return value replaces the default \"Discarded\" message if provided\n- `details?: unknown` exists on the public custom-tool type but is not currently forwarded by the loader into resolve metadata\n- `sourceToolName?: string` (optional, defaults to `\"custom_tool\"`)\n\n### Minimal usage example\n\n```ts\nimport type { CustomToolFactory } from \"@sayknow-cli/coding-agent\";\n\nconst factory: CustomToolFactory = (pi) => ({\n name: \"batch_rename_preview\",\n label: \"Batch Rename Preview\",\n description: \"Previews renames and defers commit to resolve\",\n parameters: pi.zod.object({\n files: pi.zod.array(pi.zod.string()),\n }),\n\n async execute(_toolCallId, params) {\n const previewSummary = `Prepared rename plan for ${params.files.length} files`;\n\n pi.pushPendingAction({\n label: `Batch rename: ${params.files.length} files`,\n sourceToolName: \"batch_rename_preview\",\n apply: async (reason) => {\n // apply writes here\n return {\n content: [\n { type: \"text\", text: `Applied batch rename. Reason: ${reason}` },\n ],\n };\n },\n reject: async (reason) => {\n // optional: cleanup or notify on discard\n return {\n content: [\n { type: \"text\", text: `Discarded batch rename. Reason: ${reason}` },\n ],\n };\n },\n });\n\n return {\n content: [\n {\n type: \"text\",\n text: `${previewSummary}. Call resolve to apply or discard.`,\n },\n ],\n };\n },\n});\n\nexport default factory;\n```\n\n## Runtime availability and failures\n\n`pushPendingAction` is wired by the custom tool loader through the active session's resolve queue hook.\n\nIf the runtime did not provide the resolve queue hook, `pushPendingAction` throws:\n\n- `Pending action store unavailable for custom tools in this runtime.`\n\n## Tool-choice behavior\n\nWhen `queueResolveHandler(...)` registers a preview, the agent runtime forces a one-shot `resolve` tool choice so pending previews are explicitly finalized before normal tool flow continues.\n\n## Developer guidance\n\n- Use pending actions only for destructive or high-impact operations that should support explicit apply/discard.\n- Keep `label` concise and specific; it is shown in resolve renderer output.\n- Ensure `apply(reason)` is deterministic and idempotent enough for one-shot execution; `reason` is informational and should not change behavior.\n- Implement `reject(reason)` when the discard needs cleanup (temp state, locks, notifications); omit it for stateless previews where the default message suffices.\n- If your tool can stage multiple previews, remember they are mediated by the tool-choice queue rather than a separate pending-action stack.\n",
@@ -88,9 +88,9 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
88
88
  "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 `#SKC1_…#`.\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 `#SKC1_…#` 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 | `~/.skc/agent/secrets.yml` | Plain and regex secrets across all projects |\n| Project | `<cwd>/.skc/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>/.skc/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 `~/.skc/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",
89
89
  "session-import.md": "# Import external sessions\n\nSayknow CLI can reconstruct or idempotently reuse a local session from an explicit Codex or Claude transcript file:\n\n```text\n/import-session <transcript-file> [--provider codex|claude]\n```\n\nQuote paths containing spaces. Format detection is automatic; `--provider` narrows detection and fails when the file is a different provider format.\n\n## Supported formats\n\n- Codex rollout JSONL containing `session_meta` and `response_item` records.\n- Claude Code transcript JSONL containing user and assistant records.\n- A claude.ai conversation export JSON object or array.\n\nNative SKC transcripts and unknown data are rejected rather than guessed. The command never enumerates provider history directories or reads live provider process state.\n\n## Safety contract\n\nThe source must be one explicitly selected regular file. Symbolic links, directories, invalid UTF-8, empty files, files larger than 64 MiB, and files whose identity changes while being read are rejected. Hard links are accepted because selection is explicit; the imported source is opened read-only and is never modified.\n\nImport is available in the local interactive CLI only. Path-bearing `/import-session`, `/export`, and `/move` commands are neither advertised nor executable over ACP; blocked invocations are consumed without echoing their arguments.\n\nActivation acquires an idle-only session transition after import finishes. If a response or turn starts first, the imported session remains durable and resumable from the session picker while the active work continues unchanged.\n\nEvery imported string passes through deterministic credential redaction before persistence. Redaction covers API keys and tokens, private keys, JWTs, authorization values (including JSON and non-Bearer schemes), secret assignments, and credentials embedded in URLs. A summary reports only counts and redaction kinds, never secret values.\n\nOnly the redacted source basename and exact source-byte digest enter durable provenance. The selected path and the provider's original workspace path are omitted from reconstructed model context and operator errors.\n\nRecords that cannot be mapped are not silently discarded. They are quarantined in a model-invisible custom entry as bounded metadata containing the record position, byte count, reason, and SHA-256 digest. Up to 512 digest records are retained while the total count and truncation flag remain authoritative; raw quarantined content is never persisted.\n\n## Reconstructed context\n\nSayknow maps user and assistant text plus bounded tool evidence into provider-neutral context. Claude thinking blocks and command metadata do not enter model context. The reconstructed context is deterministic and bounded:\n\n- At most 5,000 normalized messages are admitted.\n- Individual messages and tool evidence are bounded.\n- Oversized conversations retain a head and continuation tail separated by an explicit elision marker.\n- Provenance is persisted as a custom entry outside model context.\n- Reconstructed context is persisted as a display-styled custom message.\n\nA newly materialized transcript records the provider, source format, source basename, exact source-byte SHA-256, source byte count, source session id/title when available, converter and sanitizer versions, mapped/quarantined/redacted/omitted counts, import timestamp, and SKC session id.\n\n## Materialization and recovery\n\nImport first performs a strict, bounded inventory of native SKC sessions for the requested workspace only. The same source digest, provider, format, converter, and sanitizer versions reuse that workspace's verified target; importing the same file into another workspace creates a separate target. This workspace filter never enumerates provider directories. A new target leaves the current session untouched until the local UI switches, and the source file is never renamed, deleted, or rewritten.\n\nThe native workspace lookup inspects at most 512 session candidates and 128 MiB of descriptor-bound transcript bytes. Exceeding either limit fails closed before publication.\n\nBefore reporting success, Sayknow flushes and reopens the new transcript and verifies that its reconstructed context is continuable. If materialization or verification fails, the unpublished partial session and its artifacts are removed; the current session and source file remain unchanged. Cleanup is verified, and an unverifiable cleanup is reported as a terminal cleanup error rather than hidden.\n\nIf local switching is cancelled after successful materialization, the imported session remains available from the session picker.\n",
90
90
  "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/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| `/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## 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 `~/.skc/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",
91
- "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 `~/.skc/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 `@sayknow-cli/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 bounded trailing v4 header patches from each file.\n - Parses header metadata, applicable tail 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 plus at most 16KB of trailing v4 header patches for file-backed sessions.\n - Builds `SessionInfo` objects from bounded metadata and preview extraction; buried patches outside the tail budget deliberately fall back to line-1 header metadata.\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 (`~/.skc/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",
91
+ "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 `~/.skc/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 `@sayknow-cli/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 bounded trailing v4 header patches from each file.\n - Parses header metadata, applicable tail 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 plus at most 16KB of trailing v4 header patches for file-backed sessions.\n - Builds `SessionInfo` objects from bounded metadata and preview extraction; buried patches outside the tail budget deliberately fall back to line-1 header metadata.\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 (`~/.skc/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 - star toggle -> `SessionManager.setSessionStarredForPicker(session, starred)`\n\nThe picker opens from `/resume`, the command palette, or `alt+r` (`app.session.resume`). The launch screen lists only the three most recent sessions and names that key.\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- Ctrl+S to star or unstar the selected session (only when the host passes a star callback; the CLI and in-session pickers both do). The list re-sorts and the cursor follows the session.\n- fuzzy search across session id/title/cwd/first message/all messages/path\n- starred sessions (`★`) listed first, each group in recency order, before and after filtering\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",
92
92
  "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",
93
- "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~/.skc/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 `.skc-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`, SKC 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~/.skc/agent/blobs/<sha256>\n```\n\nTerminal breadcrumb files are written under:\n\n```text\n~/.skc/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 `~/.skc/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: `~/.skc/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",
93
+ "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~/.skc/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 `.skc-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`, SKC 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~/.skc/agent/blobs/<sha256>\n```\n\nTerminal breadcrumb files are written under:\n\n```text\n~/.skc/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 `~/.skc/agent/sessions`\n\nMetadata extraction reads only a prefix (`readTextPrefix(..., 4096)`) where possible.\n\n### Starred sessions\n\nStars are discovery-only metadata kept outside transcripts, in `~/.skc/agent/session-stars.json` (`src/session/session-stars.ts`), keyed by session id. `/star`, `/unstar`, and Ctrl+S in the resume picker write it under a cross-process lock with an atomic rename. Listing sets `SessionInfo.starred` from it, and the resume picker and `/sessions` dashboard sort starred sessions first (`prioritizeStarredSessions`). Stars never change `--continue`, ID-prefix resolution, deletion, or retention; a fork gets a new id and starts unstarred; a deleted session leaves a harmless stale id. An unreadable index lists as \"nothing starred\" and is never overwritten.\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: `~/.skc/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",
94
94
  "skc-dogfood-skill-template.md": "# SKC dogfood local skill template\n\nIssue #93 requested a gaebal-sayknow/operator dogfood skill. The live issue has no comment approving a fifth bundled default workflow skill, so this stays a local template instead of changing the default workflow surface. Operators can copy it into a user or project override when they want SKC-first session guidance.\n\nThe installable skill body is everything from the first frontmatter marker down; the frontmatter must be the **first line** of the installed file or the skill scan silently skips it (the scan requires a parsed `description`). Install into the user-level scan location (`~/.skc/agent/skills/`, not `~/.skc/skills/`):\n\n```sh\nmkdir -p ~/.skc/agent/skills/skc-dogfood\nsed -n '/^---$/,$p' docs/skc-dogfood-skill-template.md > ~/.skc/agent/skills/skc-dogfood/SKILL.md\n```\n\nFor a single project, install to `<project>/.skc/skills/skc-dogfood/SKILL.md` with the same extraction. Do not commit that project `.skc` copy unless the project explicitly wants a local override.\n\nFilesystem skill discovery is off by default, so enable it once. Set `skills.enabled`, then enable **only the scan that matches where you installed** — `enablePiUser` and `enablePiProject` default to `false` in `DEFAULT_SKILL_DISCOVERY_SETTINGS`, and enabling the project scan opts every future session into repo-local `.skc/skills` discovery, so do not enable it for a user-only install:\n\n```sh\nskc config set skills.enabled true\n\n# for the user-level install (~/.skc/agent/skills/):\nskc config set skills.enablePiUser true\n\n# OR, for the project-level install (<project>/.skc/skills/):\nskc config set skills.enablePiProject true\n```\n\nThen verify in a new session: `/skill:skc-dogfood` should autocomplete.\n\n---\nname: skc-dogfood\ndescription: Use when running or reviewing work through SKC sessions, dogfooding Sayknow-CLI, or migrating an operator workflow from OMX to SKC.\n---\n\n# SKC Dogfood Operator Workflow\n\nUse SKC first for coding, review, planning, and follow-up sessions. Treat OMX as a fallback only when SKC is unavailable, broken, or missing a required capability.\n\n## Locate and launch SKC\n\n- Installed CLI: run `command -v skc` and then launch with `skc --tmux`.\n- Repository checkout: from the sayknow-cli repo, prefer `bun packages/coding-agent/src/cli.ts --tmux` when testing source changes before install.\n- Worktree isolation: for branch-specific work, either let SKC create a managed sibling worktree with `skc --tmux --worktree <branch-like-name>` or `cd <existing-worktree-path>` and run `skc --tmux` there. Do not pass filesystem paths to `--worktree`.\n- Name sessions explicitly with the project and issue, for example `sayknow-cli-93-dogfood-skill`, so tmux panes, logs, and exports remain traceable.\n\n## Start the session\n\n- Put git operations inside the SKC session: fetch, branch/worktree setup, focused commits, pushes, and PR creation should be visible in-session.\n- Submit the initial prompt with the issue URL, target branch, acceptance criteria, verification limits, and any existing plan/spec link.\n- Verify the prompt was accepted: the TUI should show the user prompt, an active assistant turn, or a tool/action request. If the session silently idles, resend once with a shorter prompt and capture the failure.\n- Verify working state before leaving the session unattended: confirm the target cwd/worktree, branch, and issue scope are visible in the transcript or command output.\n\n## During work\n\n- Keep session names and branch names issue-scoped.\n- Prefer SKC workflow skills only when they fit: `deep-interview` for unclear requirements, `ralplan` for planning, `ultragoal` for durable ledgers, and `team` for coordinated tmux execution.\n- Keep evidence in the session: issue reads, focused tests/checks, screenshots only when visual behavior matters, and PR URLs.\n- When SKC is weaker than OMX, finish the urgent work with the smallest safe fallback and file a sayknow-cli follow-up issue with the missing capability, exact command/session context, expected behavior, and evidence.\n\n## Fallback policy\n\nUse OMX or another operator path only when:\n\n- `skc` cannot be located or launched after checking installed and repo-local commands;\n- authentication, model routing, tmux, or prompt submission is broken;\n- SKC lacks a required capability that OMX already has;\n- an urgent production/review deadline would be missed by debugging SKC first.\n\nRecord the fallback reason and create or link the sayknow-cli issue that would make SKC sufficient next time.\n\n## Evidence checklist\n\nReport:\n\n- project, issue, branch/worktree, and session name;\n- whether SKC was installed or repo-local;\n- prompt acceptance and working-state evidence;\n- git operations performed in-session;\n- focused verification commands and results;\n- PR/issue URLs;\n- follow-up sayknow-cli issues for any SKC gap or fallback.\n",
95
95
  "skc-plugins.md": "# SKC Plugin Bundles\n\nSKC supports two distinct plugin families. Do not confuse them:\n\n1. **Legacy marketplace / npm plugins** (`packages/coding-agent/src/extensibility/plugins`) — installed through the existing `skc plugin install <marketplace-ref|npm-spec>` marketplace/npm flows. Unchanged by this system.\n2. **SKC plugin bundles** — directories whose root contains a **`sayknow-plugin.json`** manifest (`kind: \"sayknow-cli-plugin\"`). These *extend* existing SKC capabilities and are the subject of this document.\n\nA SKC plugin bundle may only **extend** existing skills/agents — it can never register a new top-level skill, slash-command, command, or agent. SKC exposes exactly four default workflow skills (`deep-interview`, `ralplan`, `team`, `ultragoal`) and four role agents (`executor`, `architect`, `planner`, `critic`); bundles add sub-skills/appendices/tools/hooks/MCPs to those existing parents only.\n\n## Manifest (`sayknow-plugin.json`)\n\n```json\n{\n \"kind\": \"sayknow-cli-plugin\",\n \"name\": \"example-domain-bundle\",\n \"version\": \"1.0.0\",\n \"subskills\": [\"subskills/ralplan-design/SKILL.md\"],\n \"tools\": [\n { \"name\": \"domain_note\", \"path\": \"tools/domain-note.ts\", \"description\": \"...\" }\n ],\n \"hooks\": [\n { \"name\": \"audit-read\", \"event\": \"tool_call\", \"target\": \"read\", \"phase\": \"before\", \"path\": \"hooks/audit-read.ts\" }\n ],\n \"mcps\": [\n { \"name\": \"domain_docs\", \"transport\": \"stdio\", \"command\": \"bun\", \"args\": [\"mcp/domain-docs.ts\"], \"cwd\": \".\" }\n ],\n \"system_appendix\": [{ \"name\": \"domain-policy\", \"path\": \"prompts/system-appendix.md\" }],\n \"agent-appendix\": [{ \"agent\": \"executor\", \"name\": \"domain-executor\", \"path\": \"prompts/executor-appendix.md\" }]\n}\n```\n\n### Surfaces (the only allowed extension points)\n\n| Surface | Purpose | Additive rule |\n|---------|---------|---------------|\n| `subskills` | Inline sub-skills bound to an existing skill/agent (`binds_to`/`phase`/`activation_arg`) | Two-tier (see below) |\n| `tools` | Always-on custom tools (object entries) or legacy subskill-scoped string paths | Additive; manifest-declared name is authoritative, never overwrites an existing tool |\n| `hooks` | Constrained event hooks bound to a declared `event`/`target`/`phase` | Additive; run alongside built-ins, never replace |\n| `mcps` | MCP servers (`stdio`/`http`/`sse`) | Additive; server-name collisions are hard errors |\n| `system_appendix` | Lower-authority text appended to the default agent system prompt | Append-only, never overrides base |\n| `agent-appendix` | Lower-authority text appended to an existing role agent's prompt | Append-only per named agent |\n\n### Forbidden / unsupported keys\n\n- **Forbidden** (`forbidden_surface`): `skills`, `slash-commands`, `commands`, `agents` — bundles may not register new top-level definitions.\n- **Unsupported** (`unsupported_surface`): `mcp`, `mcpServers` (use the canonical `mcps`), and any unknown top-level key.\n\n## Installation\n\n```sh\nskc plugin install <path|git-url|tarball> --user # install into the user root\nskc plugin install <path|git-url|tarball> --project # install into the project root\n```\n\nExactly one of `--user` / `--project` is required for SKC plugin bundles (there is no default root). A source containing `sayknow-plugin.json` is classified as a SKC bundle and routed to the bundle installer **before** the marketplace/npm path; non-bundle sources fall through to the legacy flow.\n\nInstall is **compile-validate-then-copy**:\n\n1. The bundle is compiled and validated **without importing any plugin code** (manifest, frontmatter, and declared files are read as bytes only).\n2. Collision and MCP security policy are enforced (the durable registry is the collision authority — never capability \"first-wins\").\n3. Only the validated, hashed files are copied into a temp sibling, then atomically renamed into place; the registry entry is written last under a per-scope lock. Nothing is mutated on failure.\n\nIdempotency: re-installing identical content is a no-op; different content requires `--force`.\n\n## Security model\n\n- **Install validation never executes plugin code.** Tool/hook names are manifest-declared; at runtime the loaded factory must return/register exactly the declared name/event or the surface is quarantined (`runtime_mismatch`).\n- **MCP policy** (install + runtime connect): HTTPS-only for `http`/`sse`; private/loopback/link-local/unique-local/multicast and the `169.254.169.254` metadata endpoint are denied across IPv4, IPv6, IPv4-mapped/compatible, zone-id and trailing-dot forms; URL credentials and CRLF headers are rejected; DNS is re-resolved before connect (rebinding defence). `stdio` servers are confined to the plugin root (allowed launchers `node`/`bun` or a root-confined executable; required bundled script argument; no eval/loader flags; no env expansion).\n- **Hooks** run through a *constrained* API: only a handler for the declared event may be registered. `registerCommand`, `sendMessage`, `appendEntry`, renderer registration, and shell `exec` are denied (`security_policy`). The broad first-party hook API is never exposed to bundle hooks.\n- **Appendices** render as lower-authority, delimited `<skc-plugin-system-appendix>` / `<skc-plugin-agent-appendix>` blocks appended after the base/project prompt; size-capped (8 KiB/appendix, 32 KiB total) fail-closed; content is escaped and control-char sanitized. They can never override base/developer instructions.\n- **Hash drift**: installed files are re-verified against the registry at session start; any drift quarantines the plugin (`runtime_mismatch`).\n\n## Sub-skills: Tier-1 vs Tier-2\n\n- **Tier-1 advertisement** (metadata-only): when a parent skill/agent prompt is built, installed sub-skills bound to it are advertised as a bounded list (`plugin` / `name` / `description` / `activation_arg` / `phase`; max 12 items, 200-char descriptions, 4 KiB block, with an overflow note). No body content; rendered only in the target parent prompt, never the global public-workflow surface.\n- **Tier-2 activation** (full body): on explicit activation (e.g. `deep-interview --autoresearch`) or an agent's contextual choice, the full sub-skill body is injected as a `<skc-subskill>` block at the matching phase.\n\n## Registry, enablement, and quarantine\n\nEach scope keeps a durable `registry.json` recording per-plugin: name/version, source (`path`/`git`/`tarball` + ref/sha), manifest hash, copied files (relative path + sha256 — the uninstall ownership boundary), per-surface extension IDs, `enabled` flag, `disabledSurfaceIds`, and any `quarantine` entries.\n\nExtension IDs are stable: `tool:<name>`, `hook:<event>:<phase>:<target>:<name>`, `mcp:<name>`, `system-appendix:<plugin>:<name>`, `agent-appendix:<agent>:<plugin>:<name>`, `subskill:<parent>:<phase>:<activation_arg>`. Disabled is user-controlled (not an error); quarantine is fail-closed and visible.\n\n## Status / scope notes\n\n- Always-on **tools**, **system appendices**, **agent appendices**, and **Tier-1 advertisement** activate at session start (additive; no-op when no bundle is installed).\n- **MCP runtime connection** and the **live hook runner** integration are gated behind the same validated registry + policy; consult the ledger/run notes for their wiring status.\n- Full enable/disable/uninstall/upgrade UX is a planned follow-up; the registry already records everything required for it (per-surface IDs + copied-file ownership).\n",
96
96
  "skc-session-clawhip-routing.md": "# Human-owned SKC tmux sessions\n\nA tmux-hosted SKC TUI is a **human-only terminal surface**. It is not an external control or viewing API.\n\n## Human operator use\n\nA human operator may start an interactive TUI in a dedicated worktree for local terminal visibility:\n\n```sh\n./scripts/skc-session/create.sh <session-name> <worktree-path>\n```\n\nThe person at that terminal interacts with the TUI directly. The helper retains durable, public owner-lifecycle receipts for local troubleshooting; it never accepts routed prompts, exposes pane output, or registers a machine observer.\n\n## External bots and machines\n\nAll external bots, machines, and automation must use a canonical external surface:\n\n- Coordinator MCP for bounded workflow control, turn status, questions, and reports.\n- ACP for an ACP client over the SDK-backed session surface.\n- The Sayknow-CLI SDK for authenticated lifecycle, control, and query operations.\n\nDo not inject prompts, scrape terminal output, or use tmux state as workflow evidence. Use Coordinator lifecycle events and SDK status for external decisions, notifications, and audit records.\n\n## Boundaries\n\n- Keep visible work in a dedicated worktree, never the shared canonical checkout.\n- Treat tmux existence and terminal output as human-only diagnostics.\n- Keep all bot credentials and routing configuration in the external Coordinator MCP/ACP/SDK deployment, not in the tmux helper.",
@@ -99,7 +99,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
99
99
  "telegram-onboarding.md": "# Telegram notification onboarding\n\nThis guide documents the bundled Telegram notification setup path from Sayknow-CLI\nsource. In an interactive SKC session, use `/settings` → **Notifications** as the\nrecommended path; `skc notify` remains the authoritative headless and automation\nfallback. It is for the managed reference client, not a separate remote-control\nproduct.\n\n## What you are setting up\n\nSayknow-CLI notifications are a loopback WebSocket SDK plus a managed Telegram\nreference daemon:\n\n- each SKC session publishes a local notification endpoint under\n `.skc/state/sdk/<sessionId>.json`;\n- the managed Telegram daemon scans those endpoints, connects to them, and sends\n action-needed events to the configured Telegram chat;\n- replies and inline button taps route back to the exact session/action through\n the same notification protocol. When the configured chat supports Telegram\n forum topics, each session is routed through its own topic.\n\nThe setup command stores global notification settings in your SKC agent config\nand later sessions auto-connect when notifications are enabled.\n\n## 1. Create a Telegram bot with BotFather\n\nUse Telegram's official BotFather flow to create a bot and copy its HTTP API\ntoken:\n\n- Official BotFather documentation: <https://core.telegram.org/bots/features#botfather>\n- General Telegram Bot API documentation: <https://core.telegram.org/bots/api>\n\nIn Telegram, open `@BotFather`, run `/newbot`, choose a display name and a unique\nusername ending in `bot`, then copy the token BotFather returns. Treat the token\nlike a password: do not paste it into logs, screenshots, issues, or shell history\nthat other people can read.\n\n## 2. Configure from `/settings` (recommended)\n\nIn an eligible running SKC session, open `/settings` and select the\n**Notifications** tab. It provides the interactive Telegram setup/reconfigure\nflow and the operational controls in one place:\n\n- Enable globally with stored credentials or disable globally;\n- turn notifications on or off for the current session only;\n- refresh or probe health, send a test notification, recover dead-owner\n artifacts, and reconnect the Telegram runtime;\n- remove Telegram credentials without removing configured Discord or Slack\n adapters.\n\nTelegram token entry is a masked setup field. After entry, the token is never\nprefilled, rendered, or shown by the tab; status and health use a masked value.\nThe tab also guides the BotFather Threaded Mode check and private-chat pairing.\n\n### CLI setup fallback\n\n`skc notify setup` retains the same setup workflow for terminal-driven setup and\nautomation:\n\n```sh\nskc notify setup\n```\n\nCurrent implementation path: `packages/coding-agent/src/cli/notify-cli.ts`.\n\nThe wizard does this:\n\n1. prompts for `Telegram BotFather token:`;\n2. validates the token with Telegram `getMe`;\n3. verifies private-chat Threaded Mode capability via `getMe.has_topics_enabled`\n and, when it is off in an interactive run, prints @BotFather guidance and\n lets you retry or continue unverified;\n4. asks you to message the bot from a private Telegram chat;\n5. polls Telegram `getUpdates` until it sees a private chat message;\n6. writes the paired chat id and enables notifications.\n\nThe setup pairing flow is private-chat only. If setup sees a `group`,\n`supergroup`, or `channel`, it rejects that chat and keeps waiting for a private\nDM. This is intentional for safe local discovery: group chats must not receive\nsession names, action ids, or pending status by accident.\n\nTelegram private-chat topics: the managed daemon's per-session delivery uses\nTelegram forum topics (`createForumTopic` + `message_thread_id`). Telegram now\nsupports forum topics in **private chats** when the bot owner enables **Threaded\nMode** for the bot in @BotFather. SKC cannot enable Threaded Mode through the Bot\nAPI; setup only detects the capability (`getMe.has_topics_enabled`) and guides the\nmanual BotFather toggle. A forum-enabled supergroup is no longer required.\n\nNote: enabling topics in private chats may require an additional Telegram Stars\npurchase fee, per Telegram's Terms of Service for Bot Developers.\n\nIf BotFather's **Bot Settings** menu does not show **Threads Settings** or\n**Threaded Mode**, do not treat that as a setup blocker. Telegram exposes this\ncapability unevenly across clients/accounts/bot states, and SKC cannot force the\nmenu to appear through the Bot API. The safe fallback is to continue setup with a\nprivate DM pairing: choose `skip` in the interactive prompt (or use\n`--token <botToken> --chat-id <chatId>` for non-interactive setup). SKC will save\n`threaded=unverified`/`threaded=unknown`, try topics at runtime when possible,\nand otherwise deliver flat to the paired private chat with outbound notifications\nand inline ask buttons only plus the one-time nudge shown below.\n\nSetup verification is capability verification, not a delivery guarantee: even when\nsetup reports `threaded=verified`, the first runtime `createForumTopic` for the\npaired chat can still fail if Telegram refuses it. When per-session topics are\nunavailable, the daemon does **not** drop notifications — it routes them to the\nnormal (flat) paired 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.` Because pairing is private-only, flat delivery lands in your own\nprivate DM with the bot.\n\nThe final setup line reports a `threaded=` status:\n\n- `threaded=verified`: the bot has Threaded Mode capability (`has_topics_enabled`\n was true during setup);\n- `threaded=unverified`: Threaded Mode was off and you skipped, or setup ran\n non-interactively; setup is saved, topics are attempted when available, and\n runtime delivery falls back to the paired flat private chat with outbound\n notifications and inline ask buttons only when Telegram refuses topic creation;\n- `threaded=unknown`: the Telegram response did not include `has_topics_enabled`,\n so capability could not be verified.\n\nAfter setup succeeds, it prints a masked token and the paired chat id:\n\n```text\nNotifications enabled. botToken=1234…(len N) chatId=123456789 threaded=verified\n```\n\nThe raw token is never printed by SKC status/setup output after it is stored.\n\n## 3. Non-interactive setup and CLI operations\n\nFor headless provisioning, scripts, and automation, the authoritative commands\nremain `skc notify setup`, `skc notify status`, `skc notify health`, `skc notify\ntest`, and `skc notify recovery`. The `/settings` tab does not replace these CLI\nsubcommands.\n\nFor scripts or CI-style local provisioning, pass the bot token and known private\nchat id explicitly. Non-interactive runs cannot prompt for the BotFather toggle,\nso if Threaded Mode is off (or the capability is unknown) setup is still saved\nwith a warning and a `threaded=unverified`/`threaded=unknown` status:\n\n```sh\nskc notify setup --token <botToken> --chat-id <chatId>\n```\n\nOptional redaction can be enabled during setup:\n\n```sh\nskc notify setup --token <botToken> --chat-id <chatId> --redact\n```\n\n`--redact` sets `notifications.redact = true`. Under redaction, idle summaries\nand streamed content are suppressed before remote delivery, but ask questions and\noptions remain readable because they must be answerable remotely.\n\n## 4. Check status without leaking secrets\n\n```sh\nskc notify status\n```\n\nThe status command reads the typed notification settings and prints:\n\n- `enabled`\n- masked `botToken`\n- paired `chatId`\n- `redact`\n\nIt uses the same masking helper as setup (`first 4 chars + … + length`), so it is\nsafe to paste into a support thread if the chat id itself is not sensitive in\nyour environment.\n\n## 5. Global configuration, adapters, and precedence\n\nTelegram credentials and all `notifications.*` values are **global-only**. SKC\nreads them from the user/global agent config with schema defaults; notification\nkeys from project config files are ignored, and runtime notification overrides\nare rejected. A project cannot supply, shadow, or disable an outbound\nnotification identity.\n\n`skc notify setup` writes these global Telegram settings through the SKC Settings\nlayer:\n\n- `notifications.enabled = true`\n- `notifications.telegram.botToken = <token>`\n- `notifications.telegram.chatId = <paired chat id>`\n- `notifications.redact = true` only when `--redact` was passed\n- `notifications.telegram.streaming.enabled = true` by default; set it to `false` to disable durable live Telegram assistant-output updates globally. `SKC_NOTIFICATIONS_STREAM=1` forces process-local streaming, while `0`, `off`, or `false` forces it off.\n\nA complete global configuration is `notifications.enabled` plus at least one\ncomplete adapter. Telegram needs its bot token and private-chat id; Discord and\nSlack each need their own credential and destination. Removing Telegram in\n`/settings` is adapter-local: it preserves a complete Discord or Slack adapter\nand global enablement, and disables global notifications only when Telegram was\nthe last complete adapter.\n\n\nThree lifecycle gates keep SDK hosting, setup, and managed delivery separate:\n\n1. An eligible host receives the dormant notification control surface. `SKC_NOTIFY=off`,\n `0`, or `false` is a hard process opt-out; unsupported hosts and\n helper/subagent sessions are also ineligible.\n2. Every eligible top-level session hosts its local SDK endpoint by default,\n independently of notification configuration. `SKC_SDK_DISABLE=1` opts out of\n SDK hosting for that session.\n3. A managed Telegram daemon is ensured only for a complete global Telegram\n configuration with managed delivery enabled. Discord-only, Slack-only, and\n environment-only sessions do not start a Telegram daemon.\n\nEnvironment/session precedence for managed delivery is implemented in\n`packages/coding-agent/src/sdk/bus/config.ts`:\n\nFor a SKC-spawned child, `notifications.sessionScope=primary` suppresses managed\nnotification delivery to avoid duplicate topics; `all` permits it.\n`SKC_NOTIFICATIONS=1` or `SKC_NOTIFICATIONS_TOKEN` explicitly opts that child in,\nbut never overrides a hard opt-out or a helper/subagent exclusion.\n\nManaged-delivery precedence is highest first; it does not change independently\nhosted SDK endpoints:\n\n1. `SKC_NOTIFY=off`, `0`, or `false` prevents the notification control surface\n for that process.\n2. `SKC_NOTIFICATIONS=0` is a hard managed-delivery opt-out.\n3. Local `/notify off` disables managed delivery only for the current session.\n4. `SKC_NOTIFICATIONS=1` or `SKC_NOTIFICATIONS_TOKEN` enables the legacy\n explicit managed-delivery path.\n5. A complete global configuration enables managed delivery automatically.\n6. Otherwise managed delivery stays off; the SDK endpoint remains hosted unless\n `SKC_SDK_DISABLE=1` is set.\n\n## 6. Start or reuse sessions\n\nAfter setup, start SKC normally:\n\n```sh\nskc --tmux\n```\n\nor use any other supported SKC launch mode. Every eligible top-level session\nwrites its SDK endpoint unless `SKC_SDK_DISABLE=1`; when managed Telegram\ndelivery is configured and enabled, it also ensures the Telegram daemon is running.\n\nThe managed daemon is a singleton per bot token/chat pair. Telegram allows only\none active `getUpdates` long-poll owner for a bot token, so SKC keeps a local\ndaemon lock/state file and makes later sessions attach to the fresh owner instead\nof starting a second poller. This avoids Telegram `409 Conflict` failures.\n\n### Same-token and foreign-owner safety\n\nSetup and reconfigure never compete with a live same-token daemon. When a live\nowner already has the stored paired chat, SKC reuses it after non-polling\nvalidation. If that owner has no stored chat or the chat changes, provide a\nvalidated private chat id; SKC performs zero `getUpdates` discovery polls. For a\nforeign or unknown owner, setup does not poll, kill, reload, or take over the\nowner; the default is to cancel before writing configuration.\n\nFor a Telegram-only setup, an explicit **Save inactive for later** choice may\nstore the credentials with notifications disabled. That choice is unavailable\nwhen a complete Discord or Slack adapter is active, because globally disabling\nnotifications would affect that adapter. A post-save identity race similarly\nstops the current session before reporting that activation is blocked; the\nforeign daemon remains untouched, and the editor offers an explicit restore or\nretain-configuration choice.\n\n## 7. Use the Telegram chat\n\nThe managed daemon prefers Telegram forum-topic delivery for per-session routing\nin the paired private chat. When Threaded Mode is available for the bot (verified\nduring setup via `getMe.has_topics_enabled`), the daemon calls\n`createForumTopic`/`editForumTopic` and sends messages with `message_thread_id`\nagainst the paired `notifications.telegram.chatId`. If BotFather does not show\n**Threads Settings**/**Threaded Mode**, or if Telegram refuses topic creation even\nafter setup reported `threaded=verified`, the daemon routes notifications to the\nnormal (flat) paired private chat and posts a one-time nudge to enable Threaded\nMode rather than dropping them.\n\n### Ask-control capability negotiation\n\nThe production Telegram multiplexer is\n`packages/coding-agent/src/sdk/bus/telegram-daemon.ts`. It already sends a\nprotocol-v3 ClientHello with `ask_controls_v1` and `ask_selected_ack_v1`. The\ngeneric `packages/coding-agent/src/sdk/bus/managed-daemon.ts` is\nliveness-only: it advertises `client_ping_pong` but is intentionally\nnon-capable for controlled asks.\n\nTelegram navigation controls appear only after `ask_controls_v1` is negotiated\non that session connection. A non-capable or older third-party client receives\nthe non-actionable `action_unavailable` diagnostic instead of a controlled ask\nwith stripped option buttons, so it cannot be left with unusable controls.\n\nFlat private chat is notification-only plus inline ask buttons. It is not a\nfree-text chat surface: replies typed as normal messages and session commands such\nas `/verbose`, `/lean`, `/verbosity`, and `/redact` require Threaded Mode/topic\nrouting.\n\nFlat private-chat fallback preserves outbound notifications and inline-button\nanswers, but it cannot provide a separate Telegram topic per SKC session. Free-\ntext replies and in-topic config commands depend on topic routing, so enable\nThreaded Mode in @BotFather > Bot Settings > Threads Settings when you need\nmulti-session reply separation or session commands from Telegram. Do not\npair a group, supergroup, or channel as a substitute: setup intentionally accepts\nonly a private DM, and hand-edited non-private chat ids remain fail-closed to\navoid leaking session data. If you specifically want group topics, create a\nforum-enabled Telegram group and use a separate/custom notification integration;\nthe bundled `skc notify setup` onboarding path is private-chat only.\n\nThe managed daemon can render:\n\n- session identity headers;\n- context updates;\n- live/finalized assistant output;\n- image attachments;\n- ask prompts with inline buttons;\n- activity/typing indicators;\n- inbound delivery acknowledgements.\n\nPer-tool activity is off by default so important notifications remain visible. This\nincludes `bash`, `read`, `task`, and subagent start/completion bubbles, including\nboth `ok` and `error` results. Send `/toolactivity on` in the paired private chat\nto opt in globally, or `/toolactivity off` to suppress these bubbles again. The\ntoggle is durable, works without an active SKC session, and has an equivalent\ncontrol under `/settings` → **Notifications** → **Preferences**. Turning it off\ndoes not affect assistant output, ask prompts, or session notifications.\n\nReply paths:\n\n- tap an inline button on an ask notification;\n- reply in the session topic with free text when forum-topic routing is\n available;\n- send in-topic config commands:\n - `/verbose` — per-tool-turn assistant text (and opt-in live streaming)\n - `/lean` — settled assistant answer when the agent reaches idle, plus immediate ask lead-ins (default; no intermediate tool-turn flood)\n - `/verbosity <lean|verbose>`\n - `/redact <on|off>`\n - `/btw <question>` is available only in an authorized, known private-session\n topic. It uses the current session context in an isolated side turn and never\n injects or persists either a user or assistant message in the main session\n history, so it can run while the main session is busy. It accepts no\n attachments; `/btw` with an attachment returns `Usage: /btw <question>`.\n Foreign bot-command suffixes are silently ignored.\n\n Each logical session permits at most two concurrent side questions. The host\n deadline is 120 seconds and cancels the actual provider work. Operational\n responses are: `Usage: /btw <question>` for an empty question; `Telegram\n /btw is disabled in local settings.` when disabled; `Restart this SKC session\n to enable /btw.` when the connected session does not support side turns; `Two\n /btw questions are already running. Wait for one to finish.` when busy; `This\n /btw question timed out after 120 seconds. Send it again to retry.` on\n timeout; `This /btw question stopped because the SKC session closed or\n changed. Reopen it and try again.` when stopped; and `This /btw question\n failed. Send it again to retry.` on failure.\n\n A transient reconnect to the exact session may deliver a result once.\n Graceful SKC or daemon shutdown cancels side questions. Crashes or identity\n changes do not promise delivery, and stale results are fenced.\n `/btw` rich replies use Telegram Bot API 10.1 Markdown only. An eligible,\n complete structured Markdown reply is sent once as\n `{rich_message:{markdown,skip_entity_detection:true}}`, correlated to the\n source message in the same topic; SKC does not send native `blocks` or\n `media`. Eligibility is conservative: valid Unicode; at most 32,768 scalars,\n 131,072 UTF-8 bytes, 500 blocks, 16 nesting levels, and 20 table columns.\n Tables and math use Telegram's 10.1 Markdown support. Ineligible content and\n a definite rich rejection use the existing correlated HTML delivery.\n Ambiguous rich outcomes never retry or fall back; `/rich off` keeps HTML-only\n behavior.\n- send paired-chat lifecycle commands from the Telegram command menu or by typing:\n - `/session_create path <dir>`\n - `/session_create worktree <repo> <branch>`\n - `/session_create dir <newdir>`\n - `/session_recent [create|resume]`\n - `/session_close <sessionId>`\n - `/session_resume <sessionId|prefix>`\n\nThe removed legacy `/answer <session-tag> <answer>` flow is not the primary UX;\nTelegram topic routing identifies the target session when the configured chat\nsupports it.\n### `/btw` operational rollback\n\n`notifications.telegram.btw.enabled` defaults to `true` and is the local kill\nswitch. Disabling it consumes `/btw` without forwarding it to the session. To\nroll back, restart the Telegram daemon, and probe health:\n\n```sh\nskc config set notifications.telegram.btw.enabled false\nskc daemon restart telegram --json\nskc notify health --probe\n```\n\n## 8. Local `/notify` inside a session\n\nInside a running SKC session, `/notify` controls the current session only; it\ndoes not edit global config or credentials:\n\n- `/notify status` reports current session notification status without secrets;\n- `/notify off` disables the current session endpoint and removes its discovery\n record without changing global setup;\n- `/notify on` re-enables the current session when a complete global\n configuration or explicit environment path is available, unless\n `SKC_NOTIFICATIONS=0` is forcing opt-out.\n\nNeither command changes `SKC_NOTIFY` or `SKC_NOTIFICATIONS` precedence. A\nprocess with `SKC_NOTIFY=off`, `0`, or `false` has no notification control\nsurface to override.\n\n## 9. Debug-only manual bridge\n\nThe manual Telegram CLI remains a reference/debug tool:\n\n```sh\nbun run packages/coding-agent/src/sdk/bus/telegram-cli.ts --bot-token \"$BOT_TOKEN\"\n```\n\nIf a fresh managed daemon already owns the same bot token and paired chat, the\nmanual CLI refuses to start by default because a second poller would cause\nTelegram `409 Conflict`. Use `--force` only for deliberate debugging after you\nunderstand which daemon owns polling.\n\n## Troubleshooting\n\n### `Telegram getMe failed`\n\nThe BotFather token is invalid or was revoked. Re-copy the token from BotFather\nor regenerate it in the official BotFather UI.\n\n### Setup times out waiting for a private chat\n\nSend any message directly to the bot from your Telegram user account. Do not add\nit to a group for pairing; groups/supergroups/channels are intentionally rejected\nby the current setup flow.\n\n### Setup succeeds but no Telegram session messages arrive\n\nCheck the `threaded=` status from the last `skc notify setup` run. If it is\n`threaded=unverified` or `threaded=unknown`, first try the current Telegram\nclient's @BotFather flow for this bot. If BotFather's **Bot Settings** menu lacks\n**Threads Settings**/**Threaded Mode**, continue with the saved private-chat\npairing; this is supported. SKC cannot enable Threaded Mode through the Bot API,\nand no paid/Stars option is required just to receive flat private-chat\nnotifications. When `createForumTopic` is refused for the paired chat, the daemon\nfalls back to flat delivery in the paired private chat and posts a one-time nudge\nthat points to @BotFather > Bot Settings > Threads Settings. Flat fallback is\nlimited to outbound notifications and inline ask buttons; free-text replies and\nsession commands require Threaded Mode/topic routing.\n\n### Third-party or older client lacks ask controls\n\nA custom client that omits ClientHello, or sends one without `ask_controls_v1`,\nwill still receive ordinary empty-controls asks but receives\n`action_unavailable` for controlled asks after the short Hello grace or explicit\nnon-capable negotiation. Upgrade it to send\n`{ \"type\": \"hello\", \"protocolVersion\": 3, \"capabilities\": [\"ask_controls_v1\"] }`\non each WebSocket open; reconnecting starts a new negotiation.\n\n### Telegram 409 conflict\n\nOnly one `getUpdates` poller can own a bot token. SKC never takes over a fresh\nforeign or unknown owner. If you own the other process, stop or reconfigure it,\nthen use `skc notify health`, `skc notify recovery`, or `skc notify reconnect`;\nrecovery removes only dead-owner artifacts and never touches a live owner.\n\n### A session does not send notifications\n\nCheck, in order:\n\n1. `skc notify status`\n2. `SKC_NOTIFICATIONS` is not set to `0`\n3. the session has not run `/notify off`\n4. the repo has `.skc/state/sdk/<sessionId>.json`\n5. the managed daemon state is fresh under the SKC agent notifications directory\n\nDo not paste endpoint discovery files into public issues; they contain the\nper-session WebSocket token needed by clients.\n",
100
100
  "telegram-remote.md": "# Telegram Remote — control skc sessions from your phone\n\nTelegram Remote is a **tiny, safe operator remote** for Sayknow-CLI (`skc`)\nsessions. It lets you list, observe, start, and stop sessions from a Telegram\nchat — a control button, not a remote shell or cockpit. The real session owner\nstays on your machine (skc/tmux); Telegram only issues bounded, allowlisted\ncommands over the Coordinator MCP.\n\nThe gateway implementation lives in\n[`packages/telegram-remote`](../packages/telegram-remote/README.md); this guide\ncovers how to turn it on and use it.\n\n## What you get\n\nTwo backends, selected by `telegram.backend`:\n\n- **`coordinator`** (default) — multi-session lifecycle + observation. Bot\n commands: `/sessions`, `/observe <id>`, `/start-session <preset> [task]`,\n `/stop <id>`, `/help`.\n- **`rpc`** — attach/detach keyboard for one persistent `skc launch --output rpc`\n session. Bot commands: `/attach`, `/detach`, `/status`, `/abort`, `/help`.\n\nAnything outside this vocabulary is rejected as unknown.\n\n## Quick start (skc settings)\n\n1. **Create a bot.** Message [@BotFather](https://t.me/BotFather) → `/newbot`,\n copy the token (`123456:AA...`).\n2. **Find your Telegram id.** Message [@userinfobot](https://t.me/userinfobot)\n (or read it from your bot's `getUpdates`). You need your numeric user id\n and/or chat id.\n3. **Configure skc.** Open `skc`, go to **Settings → Integrations**, and set:\n - **Telegram Remote** (`telegram.enabled`) → on\n - **Bot Token** (`telegram.botToken`) → the @BotFather token\n - **Allowed User IDs** (`telegram.allowedUserIds`) → your id\n (comma-separated; or **Allowed Chat IDs**). At least one allowlist is\n required — unlisted senders are refused with no hints.\n - **Session Presets** (`telegram.presets`) → JSON array of approved presets\n (see below) if you want `/start-session`.\n\n Settings persist to your skc config; you can also edit them directly in\n `config.yml` under the `telegram.*` keys.\n\n4. **Start the gateway.**\n\n ```sh\n skc telegram start # start with current settings\n skc telegram status # show whether it is configured / running\n skc telegram env # print the SKC_TELEGRAM_REMOTE_* env it would use\n ```\n\n When `telegram.enabled` is on, skc also **auto-starts** the gateway in the\n background (PID-tracked, detached) the next time you launch an interactive\n session, so `skc telegram start` is only needed for a manual/one-off start.\n\n5. **Use it from Telegram.** Send `/help` to your bot, then `/sessions`,\n `/observe <id>`, `/start-session <preset>`, `/stop <id>` (coordinator mode).\n\n### Presets (`/start-session`)\n\nSession creation is **preset-only** — no workdir/command/branch ever comes from\nchat. A preset binds a fixed workdir + session command + an optional task\ntemplate with a single length-capped `{{task}}` slot:\n\n```json\n[\n {\n \"id\": \"proj\",\n \"workdir\": \"/home/you/src/project\",\n \"sessionCommand\": \"skc --worktree\",\n \"taskTemplate\": \"Use /skill:ralplan to plan: {{task}}\",\n \"taskMaxLen\": 2000\n }\n]\n```\n\n`/start-session proj fix the flaky auth test` starts the `proj` preset with the\ntask substituted into the template.\n\n## RPC mode (one persistent session)\n\nSet **Backend** (`telegram.backend`) → `rpc` to attach to a single existing\nowner-only socket exposed by `skc launch --output rpc --listen <socket>`. The\ngateway never spawns, kills, or tears down that session — it is only a Telegram\nattach/detach remote keyboard. RPC mode requires **RPC Socket**\n(`telegram.rpcSocket`) and **State Directory** (`telegram.stateDir`) for\nreconnect/resync. Agent questions and gates render as inline buttons;\nturn-complete delivery sends the final assistant text (HTML-escaped, chunked to\nTelegram's 4096-byte limit).\n\n## Safety properties\n\n- **Default deny.** Only allowlisted Telegram user/chat ids may issue any\n command; unlisted senders get an identical boring refusal.\n- **Preset-only creation.** No raw workdir/command/branch/shell/RPC from chat.\n- **Forced-minimal mutations.** The coordinator runs with the smallest mutation\n set — `sessions` (read + start), plus `reports` only when `/stop` is enabled\n (**Enable /stop**, `telegram.enableStop`). `questions` is never enabled.\n- **Redaction by construction.** Only a typed projection (session id, derived\n name, bounded status/turn enums, branch, timestamps, short sanitized blocker)\n ever leaves the machine. Raw tmux tail, transcripts, tool IO, diffs, file\n contents, env, prompts, and tokens are never transmitted.\n- **`/stop` confirmation.** `/stop <id>` arms; a second `/stop <id> confirm` (or\n the inline **Confirm stop** button) records a graceful coordinator\n `cancelled`. It does not kill a tmux process.\n\n## Rich messaging & push (optional)\n\n- **Rich Messages** (`telegram.enableRich`, default on) — HTML formatting +\n inline **Observe/Stop/Refresh** buttons. Set off for plain text.\n- **Register Bot Menu** (`telegram.registerCommands`, default on) — registers\n the Bot command menu at startup.\n- **Push Notifications** (`telegram.enablePush`) — Follow/Mute subscriptions via\n the coordinator event-watch surface (needs a state dir). Push never widens the\n transmitted-data allowlist.\n\n## Settings ↔ environment\n\nThe `skc telegram` command and autostart translate `telegram.*` settings into\n`SKC_TELEGRAM_REMOTE_*` environment variables consumed by the gateway (see\n`skc telegram env`). You can also run the gateway standalone with those env vars\ndirectly — see [`packages/telegram-remote/README.md`](../packages/telegram-remote/README.md)\nfor the full variable list, `.env.example`, and turnkey **systemd**/**launchd**\nservice examples for always-on deployment.\n\n## Non-goals\n\nTelegram Remote is not a remote RPC cockpit, remote shell, config editor, or\ntranscript viewer. It is a bounded lifecycle + observation button. For richer\ncontrol, use skc directly on the host.\n",
101
101
  "telegram-session-close-timeout-bug.md": "# Telegram `/session_close` uncertain outcome and delayed topic cleanup\n\n## Baseline\n\n- Branch: `fix/telegram-session-close-timeout`\n- Base: `upstream/dev` at `12aa7ebd18752c338b55a6ddc0ca8945f6e555cb`\n- Reported: 2026-07-22\n\n## Reproduction\n\n1. Create a SKC session from Telegram and wait until its topic/session is active.\n2. Send:\n\n```text\n/session_close <sessionID>\n```\n\n3. Observe the close response, process/session liveness, and Telegram topic lifecycle.\n\n## Expected behavior\n\n- A valid managed session ID is resolved deterministically.\n- The close request terminates the target session promptly.\n- The daemon returns one clear terminal close result.\n- The Telegram topic/thread is deleted promptly after the session reaches the terminal state.\n- A timeout is reserved for a genuinely unresponsive close operation, not the normal successful path.\n\n## Observed behavior\n\n- Telegram displays `Close outcome uncertain. The session may already be closed — check /session_recent before retrying.`\n- The target process appears to terminate, but the close request does not receive authoritative terminal confirmation.\n- The Telegram topic remains visible for approximately 60 seconds.\n- The topic is then deleted by the orphan-topic cleanup path after `ORPHAN_TOPIC_GRACE_MS`, rather than promptly by the authenticated `session_closed` handler.\n\nThe warning does not mean the session is confirmed closed. It means the close effect may have occurred, but the daemon could not prove the terminal result. The delayed deletion indicates that normal terminal cleanup was missed and the 60-second orphan fallback recovered it later.\n\n## Investigation focus\n\nTrace one lifecycle request ID across:\n\n- Telegram command parsing and acknowledgement\n- `session_close` lifecycle frame dispatch\n- managed tmux/session identity resolution\n- force-close SIGTERM, owner-verdict, and compatibility cleanup ordering\n- owner/supervisor terminal-state observation\n- close outcome generation\n- Telegram topic deletion\n\nPay particular attention to ordering. The managed owner must publish its immutable terminal verdict before runtime-state serialization, coordinator/state-file locks, and terminal-payload preservation can delay or return from postmortem handling. Topic cleanup remains an independent path: it must follow an authenticated `session_closed` frame for the current endpoint generation and lease, never a lifecycle acknowledgement alone. Also verify that the supplied session ID maps to the actual managed tmux name and generation.\n\n## Regression coverage\n\nAdd focused tests for:\n\n1. A live managed session closes before the timeout and emits one terminal outcome.\n2. Topic deletion occurs after terminal close evidence, without waiting for the timeout.\n3. A session that exits during the close race is treated idempotently as closed.\n4. Repeating the same close request returns the prior terminal result without another timeout.\n5. Unknown and unmanaged session IDs fail closed without deleting unrelated topics.\n6. A genuinely stuck process reaches the bounded force-close path and reports that distinct outcome.\n\n## Acceptance criteria\n\n- `/session_close <sessionID>` makes the managed session non-live promptly under normal conditions.\n- The normal path does not display an intermediate outcome that remains pending until timeout.\n- Topic deletion is prompt, deterministic, and tied to the correct session generation.\n- Timeout/force-close remains bounded and observable for genuinely unresponsive sessions.\n- Close remains replay-safe and cannot kill a reused tmux session belonging to another generation.\n",
102
- "theme.md": "# Theming Reference\n\nThis document describes how theming works in the coding-agent today: schema, loading, runtime behavior, and failure modes.\n\n## What the theme system controls\n\nThe theme system drives:\n\n- foreground/background color tokens used across the TUI\n- markdown styling adapters (`getMarkdownTheme()`)\n- selector/editor/settings list adapters (`getSelectListTheme()`, `getEditorTheme()`, `getSettingsListTheme()`)\n- symbol preset + symbol overrides (`unicode`, `nerd`, `ascii`)\n- syntax highlighting colors used by native highlighter (`@sayknow-cli/natives`)\n- status line segment colors\n\nPrimary implementation: `src/modes/theme/theme.ts`.\n\n## Theme JSON shape\n\nTheme files are JSON objects validated against the runtime schema in `theme.ts` (`ThemeJsonSchema`) and mirrored by `src/modes/theme/theme-schema.json`.\n\nTop-level fields:\n\n- `name` (required)\n- `colors` (required; all color tokens required)\n- `vars` (optional; reusable color variables)\n- `export` (optional; HTML export colors)\n- `symbols` (optional)\n - `preset` (optional: `unicode | nerd | ascii`)\n - `overrides` (optional: key/value overrides for `SymbolKey`)\n\nColor values accept:\n\n- hex string (`\"#RRGGBB\"`)\n- 256-color index (`0..255`)\n- variable reference string (resolved through `vars`)\n- empty string (`\"\"`) meaning terminal default (`\\x1b[39m` fg, `\\x1b[49m` bg)\n\n## Required color tokens (current)\n\nAll tokens below are required in `colors`.\n\n### Core text and borders (11)\n\n`accent`, `border`, `borderAccent`, `borderMuted`, `success`, `error`, `warning`, `muted`, `dim`, `text`, `thinkingText`\n\n### Background blocks (7)\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`, `statusLineBg`\n\n### Message/tool text (5)\n\n`userMessageText`, `customMessageText`, `customMessageLabel`, `toolTitle`, `toolOutput`\n\n### Markdown (10)\n\n`mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet`\n\n### Tool diff + syntax highlighting (12)\n\n`toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext`,\n`syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation`\n\n### Mode/thinking borders (8)\n\n`thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `bashMode`, `pythonMode`\n\n### Status line segment colors (14)\n\n`statusLineSep`, `statusLineModel`, `statusLinePath`, `statusLineGitClean`, `statusLineGitDirty`, `statusLineContext`, `statusLineSpend`, `statusLineStaged`, `statusLineDirty`, `statusLineUntracked`, `statusLineOutput`, `statusLineCost`, `statusLineSubagents`\n\n## Optional tokens\n\n### `export` section (optional)\n\nUsed for HTML export theming helpers:\n\n- `export.pageBg`\n- `export.cardBg`\n- `export.infoBg`\n\nIf omitted, export code derives defaults from resolved theme colors.\n\n### `symbols` section (optional)\n\n- `symbols.preset` sets a theme-level default symbol set.\n- `symbols.overrides` can override individual `SymbolKey` values.\n\nRuntime precedence:\n\n1. settings `symbolPreset` override (if set)\n2. theme JSON `symbols.preset`\n3. fallback `\"unicode\"`\n\nInvalid override keys are ignored and logged (`logger.debug`).\n\n## Built-in vs custom theme sources\n\nTheme lookup order (`loadThemeJson`):\n\n1. built-in embedded themes (`red-octopus.json`, `blue-octopus.json`, `claude-code.json`, `codex.json`, and `opencode.json` compiled into `defaultThemes`)\n2. custom theme file: `<customThemesDir>/<name>.json`\n\nCustom themes directory comes from `getCustomThemesDir()`:\n\n- default: `~/.skc/agent/themes`\n- overridden by `SKC_CODING_AGENT_DIR` (`$SKC_CODING_AGENT_DIR/themes`)\n\n`getAvailableThemes()` returns merged built-in + custom names, sorted, with built-ins taking precedence on name collision.\n\n## Loading, validation, and resolution\n\nFor custom theme files:\n\n1. read JSON\n2. parse JSON\n3. validate against `ThemeJsonSchema`\n4. resolve `vars` references recursively\n5. convert resolved values to ANSI by terminal capability mode\n\nValidation behavior:\n\n- missing required color tokens: explicit grouped error message\n- bad token types/values: validation errors with JSON path\n- unknown theme file: `Theme not found: <name>`\n\nVar reference behavior:\n\n- supports nested references\n- throws on missing variable reference\n- throws on circular references\n\n## Terminal color mode behavior\n\nColor mode detection (`detectColorMode`):\n\n- `COLORTERM=truecolor|24bit` => truecolor\n- `WT_SESSION` => truecolor\n- `TERM` in `dumb`, `linux`, or empty => 256color\n- otherwise => truecolor\n\nConversion behavior:\n\n- hex -> `Bun.color(..., \"ansi-16m\" | \"ansi-256\")`\n- numeric -> `38;5` / `48;5` ANSI\n- `\"\"` -> default fg/bg reset\n\n## Runtime switching behavior\n\n### Initial theme (`initTheme`)\n\n`main.ts` initializes theme with settings:\n\n- `symbolPreset`\n- `colorBlindMode`\n- `theme.dark`\n- `theme.light`\n\nAuto theme slot selection uses terminal appearance in this order:\n\n1. terminal-reported OSC 11 background luminance, unless the macOS/Zellij fallback path is active\n2. `COLORFGBG` background index (`< 8` => dark, `>= 8` => light)\n3. macOS appearance fallback only for the known-broken macOS/Zellij OSC 11 path\n4. dark slot fallback\n\nBuilt-in theme note: `blue-octopus` is the default SKC theme for both the dark and light slots, and `red-octopus` is a bundled warm, high-contrast alternate. Both are cephalopod brand themes with separate semantic error/warning/diff-removal tokens and octopus-oriented symbol overrides. Three additional bundled migration themes — `claude-code`, `codex`, and `opencode` — mirror the look of those tools for easy eye-migration. All three are dark-classified and recommended for `theme.dark`, but are selectable in either slot; they keep SKC's default symbol identity (no crab-symbol overrides).\n\nCurrent defaults from settings schema:\n\n- `theme.dark = \"blue-octopus\"`\n- `theme.light = \"blue-octopus\"`\n- `symbolPreset = \"unicode\"`\n- `colorBlindMode = false`\n\n### Explicit switching (`setTheme`)\n\n- loads selected theme\n- updates global `theme` singleton\n- optionally starts watcher\n- triggers `onThemeChange` callback\n\nOn failure:\n\n- falls back to built-in `dark`\n- returns `{ success: false, error }`\n\n### Preview switching (`previewTheme`)\n\n- applies temporary preview theme to global `theme`\n- does **not** change persisted settings by itself\n- returns success/error without fallback replacement\n\nThe settings theme picker is confirm-only; arrow-key browsing does not call `previewTheme`, so the rendered theme and displayed/persisted theme name stay aligned until Enter confirms a new selection.\n\n## Watchers and live reload\n\nWhen watcher is enabled (`setTheme(..., true)` / interactive init):\n\n- watches `<customThemesDir>/<currentTheme>.json` only when that file exists\n- built-ins are effectively not watched; built-in theme lookup also takes precedence over same-name custom files\n- matching file changes schedule a debounced reload; reload errors or temporary file absence keep the last successfully loaded theme\n- the watcher does not perform a delete/rename fallback; it waits for a future successful reload or explicit theme switch\n\nAuto mode also reevaluates dark/light slot mapping from terminal appearance changes, `SIGWINCH`, and the macOS fallback observer when active.\n\n## Color-blind mode behavior\n\n`colorBlindMode` changes only one token at runtime:\n\n- `toolDiffAdded` is HSV-adjusted (green shifted toward blue)\n- adjustment is applied only when resolved value is a hex string\n\nOther tokens are unchanged.\n\n## Where theme settings are persisted\n\nTheme-related settings are persisted by `Settings` to global config YAML:\n\n- path: `<agentDir>/config.yml`\n- default agent dir: `~/.skc/agent`\n- effective default file: `~/.skc/agent/config.yml`\n\nPersisted keys:\n\n- `theme.dark`\n- `theme.light`\n- `symbolPreset`\n- `colorBlindMode`\n\nLegacy migration exists: old flat `theme: \"name\"` is migrated to nested `theme.dark` or `theme.light` based on luminance detection; legacy built-in names `dark`/`light` map to `red-octopus`/`blue-octopus` unless matching custom theme files exist.\n\n## Creating a custom theme (practical)\n\n1. Create file in custom themes dir, e.g. `~/.skc/agent/themes/my-theme.json`.\n2. Include `name`, optional `vars`, and **all required** `colors` tokens.\n3. Optionally include `symbols` and `export`.\n4. Select the theme in Settings (`Display -> Dark theme` or `Display -> Light theme`) depending on which auto slot you want. All bundled themes are selectable: the crustacean defaults `red-octopus` and `blue-octopus`, plus the migration themes `claude-code`, `codex`, and `opencode` (dark-classified, recommended for the dark slot but selectable in either).\n\nMinimal skeleton:\n\n```json\n{\n \"name\": \"my-theme\",\n \"vars\": {\n \"accent\": \"#7aa2f7\",\n \"muted\": 244\n },\n \"colors\": {\n \"accent\": \"accent\",\n \"border\": \"#4c566a\",\n \"borderAccent\": \"accent\",\n \"borderMuted\": \"muted\",\n \"success\": \"#9ece6a\",\n \"error\": \"#f7768e\",\n \"warning\": \"#e0af68\",\n \"muted\": \"muted\",\n \"dim\": 240,\n \"text\": \"\",\n \"thinkingText\": \"muted\",\n\n \"selectedBg\": \"#2a2f45\",\n \"userMessageBg\": \"#1f2335\",\n \"userMessageText\": \"\",\n \"customMessageBg\": \"#24283b\",\n \"customMessageText\": \"\",\n \"customMessageLabel\": \"accent\",\n \"toolPendingBg\": \"#1f2335\",\n \"toolSuccessBg\": \"#1f2d2a\",\n \"toolErrorBg\": \"#2d1f2a\",\n \"toolTitle\": \"\",\n \"toolOutput\": \"muted\",\n\n \"mdHeading\": \"accent\",\n \"mdLink\": \"accent\",\n \"mdLinkUrl\": \"muted\",\n \"mdCode\": \"#c0caf5\",\n \"mdCodeBlock\": \"#c0caf5\",\n \"mdCodeBlockBorder\": \"muted\",\n \"mdQuote\": \"muted\",\n \"mdQuoteBorder\": \"muted\",\n \"mdHr\": \"muted\",\n \"mdListBullet\": \"accent\",\n\n \"toolDiffAdded\": \"#9ece6a\",\n \"toolDiffRemoved\": \"#f7768e\",\n \"toolDiffContext\": \"muted\",\n\n \"syntaxComment\": \"#565f89\",\n \"syntaxKeyword\": \"#bb9af7\",\n \"syntaxFunction\": \"#7aa2f7\",\n \"syntaxVariable\": \"#c0caf5\",\n \"syntaxString\": \"#9ece6a\",\n \"syntaxNumber\": \"#ff9e64\",\n \"syntaxType\": \"#2ac3de\",\n \"syntaxOperator\": \"#89ddff\",\n \"syntaxPunctuation\": \"#9aa5ce\",\n\n \"thinkingOff\": 240,\n \"thinkingMinimal\": 244,\n \"thinkingLow\": \"#7aa2f7\",\n \"thinkingMedium\": \"#2ac3de\",\n \"thinkingHigh\": \"#bb9af7\",\n \"thinkingXhigh\": \"#f7768e\",\n\n \"bashMode\": \"#2ac3de\",\n \"pythonMode\": \"#bb9af7\",\n\n \"statusLineBg\": \"#16161e\",\n \"statusLineSep\": 240,\n \"statusLineModel\": \"#bb9af7\",\n \"statusLinePath\": \"#7aa2f7\",\n \"statusLineGitClean\": \"#9ece6a\",\n \"statusLineGitDirty\": \"#e0af68\",\n \"statusLineContext\": \"#2ac3de\",\n \"statusLineSpend\": \"#7dcfff\",\n \"statusLineStaged\": \"#9ece6a\",\n \"statusLineDirty\": \"#e0af68\",\n \"statusLineUntracked\": \"#f7768e\",\n \"statusLineOutput\": \"#c0caf5\",\n \"statusLineCost\": \"#ff9e64\",\n \"statusLineSubagents\": \"#bb9af7\"\n }\n}\n```\n\n## Testing custom themes\n\nUse this workflow:\n\n1. Start interactive mode (watcher enabled from startup).\n2. Open settings and confirm the custom theme in the dark/light theme picker; arrow-key browsing is intentionally non-mutating.\n3. For custom theme files, edit the JSON while running and confirm auto-reload on save.\n4. Exercise critical surfaces:\n - markdown rendering\n - tool blocks (pending/success/error)\n - diff rendering (added/removed/context)\n - status line readability\n - thinking level border changes\n - bash/python mode border colors\n5. Validate both symbol presets if your theme depends on glyph width/appearance.\n\n## Real constraints and caveats\n\n- All `colors` tokens are required for custom themes.\n- `export` and `symbols` are optional.\n- `$schema` in theme JSON is informational; runtime validation is enforced by a Zod schema in code.\n- `setTheme` failure falls back to `dark`; `previewTheme` failure does not replace current theme.\n- File watcher reload errors or temporary missing files keep the current loaded theme until a successful reload or explicit theme switch.\n",
102
+ "theme.md": "# Theming Reference\n\nThis document describes how theming works in the coding-agent today: schema, loading, runtime behavior, and failure modes.\n\n## What the theme system controls\n\nThe theme system drives:\n\n- foreground/background color tokens used across the TUI\n- markdown styling adapters (`getMarkdownTheme()`)\n- selector/editor/settings list adapters (`getSelectListTheme()`, `getEditorTheme()`, `getSettingsListTheme()`)\n- symbol preset + symbol overrides (`unicode`, `nerd`, `ascii`)\n- syntax highlighting colors used by native highlighter (`@sayknow-cli/natives`)\n- status line segment colors\n\nPrimary implementation: `src/modes/theme/theme.ts`.\n\n## Theme JSON shape\n\nTheme files are JSON objects validated against the runtime schema in `theme.ts` (`ThemeJsonSchema`) and mirrored by `src/modes/theme/theme-schema.json`.\n\nTop-level fields:\n\n- `name` (required)\n- `colors` (required; all color tokens required)\n- `vars` (optional; reusable color variables)\n- `export` (optional; HTML export colors)\n- `symbols` (optional)\n - `preset` (optional: `unicode | nerd | ascii`)\n - `overrides` (optional: key/value overrides for `SymbolKey`)\n\nColor values accept:\n\n- hex string (`\"#RRGGBB\"`)\n- 256-color index (`0..255`)\n- variable reference string (resolved through `vars`)\n- empty string (`\"\"`) meaning terminal default (`\\x1b[39m` fg, `\\x1b[49m` bg)\n\n## Required color tokens (current)\n\nAll tokens below are required in `colors`.\n\n### Core text and borders (11)\n\n`accent`, `border`, `borderAccent`, `borderMuted`, `success`, `error`, `warning`, `muted`, `dim`, `text`, `thinkingText`\n\n### Background blocks (7)\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`, `statusLineBg`\n\nIn the terminal, user prompts, tool blocks, and the composer are drawn on the bare terminal background with a colored rail (`▌` for prompts and the composer, `│` for tool output); they do not paint `userMessageBg` or the `tool*Bg` tokens. Those tokens remain required: session exports derive their palette from `userMessageBg`, and the status line paints `statusLineBg` behind powerline separators.\n\n### Message/tool text (5)\n\n`userMessageText`, `customMessageText`, `customMessageLabel`, `toolTitle`, `toolOutput`\n\n### Markdown (10)\n\n`mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet`\n\n### Tool diff + syntax highlighting (12)\n\n`toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext`,\n`syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation`\n\n### Mode/thinking borders (8)\n\n`thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `bashMode`, `pythonMode`\n\n### Status line segment colors (14)\n\n`statusLineSep`, `statusLineModel`, `statusLinePath`, `statusLineGitClean`, `statusLineGitDirty`, `statusLineContext`, `statusLineSpend`, `statusLineStaged`, `statusLineDirty`, `statusLineUntracked`, `statusLineOutput`, `statusLineCost`, `statusLineSubagents`\n\n## Optional tokens\n\n### `export` section (optional)\n\nUsed for HTML export theming helpers:\n\n- `export.pageBg`\n- `export.cardBg`\n- `export.infoBg`\n\nIf omitted, export code derives defaults from resolved theme colors.\n\n### `symbols` section (optional)\n\n- `symbols.preset` sets a theme-level default symbol set.\n- `symbols.overrides` can override individual `SymbolKey` values.\n\nRuntime precedence:\n\n1. settings `symbolPreset` override (if set)\n2. theme JSON `symbols.preset`\n3. fallback `\"unicode\"`\n\nInvalid override keys are ignored and logged (`logger.debug`).\n\n## Built-in vs custom theme sources\n\nTheme lookup order (`loadThemeJson`):\n\n1. built-in embedded themes (`blue-octopus.json`, `red-octopus.json`, `ink-octopus.json`, `glow-octopus.json`, `violet-octopus.json`, `claude-code.json`, `codex.json`, `gruvbox-dark.json`, and `opencode.json` compiled into `defaultThemes`)\n2. custom theme file: `<customThemesDir>/<name>.json`\n\nCustom themes directory comes from `getCustomThemesDir()`:\n\n- default: `~/.skc/agent/themes`\n- overridden by `SKC_CODING_AGENT_DIR` (`$SKC_CODING_AGENT_DIR/themes`)\n\n`getAvailableThemes()` returns merged built-in + custom names, sorted, with built-ins taking precedence on name collision.\n\n## Loading, validation, and resolution\n\nFor custom theme files:\n\n1. read JSON\n2. parse JSON\n3. validate against `ThemeJsonSchema`\n4. resolve `vars` references recursively\n5. convert resolved values to ANSI by terminal capability mode\n\nValidation behavior:\n\n- missing required color tokens: explicit grouped error message\n- bad token types/values: validation errors with JSON path\n- unknown theme file: `Theme not found: <name>`\n\nVar reference behavior:\n\n- supports nested references\n- throws on missing variable reference\n- throws on circular references\n\n## Terminal color mode behavior\n\nColor mode detection (`detectColorMode`):\n\n- `COLORTERM=truecolor|24bit` => truecolor\n- `WT_SESSION` => truecolor\n- `TERM` in `dumb`, `linux`, or empty => 256color\n- otherwise => truecolor\n\nConversion behavior:\n\n- hex -> `Bun.color(..., \"ansi-16m\" | \"ansi-256\")`\n- numeric -> `38;5` / `48;5` ANSI\n- `\"\"` -> default fg/bg reset\n\n## Runtime switching behavior\n\n### Initial theme (`initTheme`)\n\n`main.ts` initializes theme with settings:\n\n- `symbolPreset`\n- `colorBlindMode`\n- `theme.dark`\n- `theme.light`\n\nAuto theme slot selection uses terminal appearance in this order:\n\n1. terminal-reported OSC 11 background luminance, unless the macOS/Zellij fallback path is active\n2. `COLORFGBG` background index (`< 8` => dark, `>= 8` => light)\n3. macOS appearance fallback only for the known-broken macOS/Zellij OSC 11 path\n4. dark slot fallback\n\nBuilt-in theme note: `ink-octopus` (warm graphite with a single amber accent) is the default SKC theme for the dark slot and `blue-octopus` for the light slot. `red-octopus` is a bundled warm, high-contrast alternate, and two more dark octopus palettes are bundled: `glow-octopus` (teal-black with bioluminescent green) and `violet-octopus` (plum-dark with lavender). All five are cephalopod brand themes with separate semantic error/warning/diff-removal tokens and octopus-oriented symbol overrides. Three additional bundled migration themes — `claude-code`, `codex`, and `opencode` — mirror the look of those tools for easy eye-migration. All three are dark-classified and recommended for `theme.dark`, but are selectable in either slot; they keep SKC's default symbol identity (no crab-symbol overrides).\n\nCurrent defaults from settings schema:\n\n- `theme.dark = \"ink-octopus\"`\n- `theme.light = \"blue-octopus\"`\n- `symbolPreset = \"unicode\"`\n- `colorBlindMode = false`\n\n### Explicit switching (`setTheme`)\n\n- loads selected theme\n- updates global `theme` singleton\n- optionally starts watcher\n- triggers `onThemeChange` callback\n\nOn failure:\n\n- falls back to built-in `dark`\n- returns `{ success: false, error }`\n\n### Preview switching (`previewTheme`)\n\n- applies temporary preview theme to global `theme`\n- does **not** change persisted settings by itself\n- returns success/error without fallback replacement\n\nThe settings theme picker is confirm-only; arrow-key browsing does not call `previewTheme`, so the rendered theme and displayed/persisted theme name stay aligned until Enter confirms a new selection.\n\n## Watchers and live reload\n\nWhen watcher is enabled (`setTheme(..., true)` / interactive init):\n\n- watches `<customThemesDir>/<currentTheme>.json` only when that file exists\n- built-ins are effectively not watched; built-in theme lookup also takes precedence over same-name custom files\n- matching file changes schedule a debounced reload; reload errors or temporary file absence keep the last successfully loaded theme\n- the watcher does not perform a delete/rename fallback; it waits for a future successful reload or explicit theme switch\n\nAuto mode also reevaluates dark/light slot mapping from terminal appearance changes, `SIGWINCH`, and the macOS fallback observer when active.\n\n## Color-blind mode behavior\n\n`colorBlindMode` changes only one token at runtime:\n\n- `toolDiffAdded` is HSV-adjusted (green shifted toward blue)\n- adjustment is applied only when resolved value is a hex string\n\nOther tokens are unchanged.\n\n## Where theme settings are persisted\n\nTheme-related settings are persisted by `Settings` to global config YAML:\n\n- path: `<agentDir>/config.yml`\n- default agent dir: `~/.skc/agent`\n- effective default file: `~/.skc/agent/config.yml`\n\nPersisted keys:\n\n- `theme.dark`\n- `theme.light`\n- `symbolPreset`\n- `colorBlindMode`\n\nLegacy migration exists: old flat `theme: \"name\"` is migrated to nested `theme.dark` or `theme.light` based on luminance detection; legacy built-in names `dark`/`light` both map to `blue-octopus` (pinned explicitly, so they do not follow the `ink-octopus` dark default) unless matching custom theme files exist.\n\n## Creating a custom theme (practical)\n\n1. Create file in custom themes dir, e.g. `~/.skc/agent/themes/my-theme.json`.\n2. Include `name`, optional `vars`, and **all required** `colors` tokens.\n3. Optionally include `symbols` and `export`.\n4. Select the theme in Settings (`Display -> Dark theme` or `Display -> Light theme`) depending on which auto slot you want. All bundled themes are selectable: the octopus themes `blue-octopus`, `red-octopus`, `ink-octopus`, `glow-octopus`, and `violet-octopus`, plus the migration themes `claude-code`, `codex`, and `opencode` (dark-classified, recommended for the dark slot but selectable in either).\n\nMinimal skeleton:\n\n```json\n{\n \"name\": \"my-theme\",\n \"vars\": {\n \"accent\": \"#7aa2f7\",\n \"muted\": 244\n },\n \"colors\": {\n \"accent\": \"accent\",\n \"border\": \"#4c566a\",\n \"borderAccent\": \"accent\",\n \"borderMuted\": \"muted\",\n \"success\": \"#9ece6a\",\n \"error\": \"#f7768e\",\n \"warning\": \"#e0af68\",\n \"muted\": \"muted\",\n \"dim\": 240,\n \"text\": \"\",\n \"thinkingText\": \"muted\",\n\n \"selectedBg\": \"#2a2f45\",\n \"userMessageBg\": \"#1f2335\",\n \"userMessageText\": \"\",\n \"customMessageBg\": \"#24283b\",\n \"customMessageText\": \"\",\n \"customMessageLabel\": \"accent\",\n \"toolPendingBg\": \"#1f2335\",\n \"toolSuccessBg\": \"#1f2d2a\",\n \"toolErrorBg\": \"#2d1f2a\",\n \"toolTitle\": \"\",\n \"toolOutput\": \"muted\",\n\n \"mdHeading\": \"accent\",\n \"mdLink\": \"accent\",\n \"mdLinkUrl\": \"muted\",\n \"mdCode\": \"#c0caf5\",\n \"mdCodeBlock\": \"#c0caf5\",\n \"mdCodeBlockBorder\": \"muted\",\n \"mdQuote\": \"muted\",\n \"mdQuoteBorder\": \"muted\",\n \"mdHr\": \"muted\",\n \"mdListBullet\": \"accent\",\n\n \"toolDiffAdded\": \"#9ece6a\",\n \"toolDiffRemoved\": \"#f7768e\",\n \"toolDiffContext\": \"muted\",\n\n \"syntaxComment\": \"#565f89\",\n \"syntaxKeyword\": \"#bb9af7\",\n \"syntaxFunction\": \"#7aa2f7\",\n \"syntaxVariable\": \"#c0caf5\",\n \"syntaxString\": \"#9ece6a\",\n \"syntaxNumber\": \"#ff9e64\",\n \"syntaxType\": \"#2ac3de\",\n \"syntaxOperator\": \"#89ddff\",\n \"syntaxPunctuation\": \"#9aa5ce\",\n\n \"thinkingOff\": 240,\n \"thinkingMinimal\": 244,\n \"thinkingLow\": \"#7aa2f7\",\n \"thinkingMedium\": \"#2ac3de\",\n \"thinkingHigh\": \"#bb9af7\",\n \"thinkingXhigh\": \"#f7768e\",\n\n \"bashMode\": \"#2ac3de\",\n \"pythonMode\": \"#bb9af7\",\n\n \"statusLineBg\": \"#16161e\",\n \"statusLineSep\": 240,\n \"statusLineModel\": \"#bb9af7\",\n \"statusLinePath\": \"#7aa2f7\",\n \"statusLineGitClean\": \"#9ece6a\",\n \"statusLineGitDirty\": \"#e0af68\",\n \"statusLineContext\": \"#2ac3de\",\n \"statusLineSpend\": \"#7dcfff\",\n \"statusLineStaged\": \"#9ece6a\",\n \"statusLineDirty\": \"#e0af68\",\n \"statusLineUntracked\": \"#f7768e\",\n \"statusLineOutput\": \"#c0caf5\",\n \"statusLineCost\": \"#ff9e64\",\n \"statusLineSubagents\": \"#bb9af7\"\n }\n}\n```\n\n## Testing custom themes\n\nUse this workflow:\n\n1. Start interactive mode (watcher enabled from startup).\n2. Open settings and confirm the custom theme in the dark/light theme picker; arrow-key browsing is intentionally non-mutating.\n3. For custom theme files, edit the JSON while running and confirm auto-reload on save.\n4. Exercise critical surfaces:\n - markdown rendering\n - tool blocks (pending/success/error)\n - diff rendering (added/removed/context)\n - status line readability\n - thinking level border changes\n - bash/python mode border colors\n5. Validate both symbol presets if your theme depends on glyph width/appearance.\n\n## Real constraints and caveats\n\n- All `colors` tokens are required for custom themes.\n- `export` and `symbols` are optional.\n- `$schema` in theme JSON is informational; runtime validation is enforced by a Zod schema in code.\n- `setTheme` failure falls back to `dark`; `previewTheme` failure does not replace current theme.\n- File watcher reload errors or temporary missing files keep the current loaded theme until a successful reload or explicit theme switch.\n",
103
103
  "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",
104
104
  "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 `SKC_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: `SKC_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.",
105
105
  "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).",