@sayknow-cli/coding-agent 0.3.12 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commands/web-search.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +9 -9
- package/dist/types/config/keybindings.d.ts +134 -129
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +35 -35
- package/dist/types/config/models-config-schema.d.ts +111 -111
- package/dist/types/config/settings-schema.d.ts +56 -16
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +79 -6
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/modes/patch.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/goals/tools/goal-tool.d.ts +6 -6
- package/dist/types/i18n/messages/en.d.ts +8 -8
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/lsp/index.d.ts +9 -9
- package/dist/types/lsp/types.d.ts +9 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +40 -3
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/skc-runtime/state-schema.d.ts +19 -19
- package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/task/agents.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -27
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/browser.d.ts +28 -28
- package/dist/types/tools/computer.d.ts +20 -20
- package/dist/types/tools/cron.d.ts +2 -2
- package/dist/types/tools/debug.d.ts +40 -40
- package/dist/types/tools/gh.d.ts +10 -10
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/monitor.d.ts +6 -6
- package/dist/types/tools/skill-discovery.d.ts +4 -4
- package/dist/types/tools/subagent.d.ts +12 -12
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/tools/todo-write.d.ts +8 -8
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/dist/types/web/insane/bridge.d.ts +9 -9
- package/dist/types/web/search/index.d.ts +4 -4
- package/package.json +8 -8
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +51 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +2958 -430
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +301 -80
- package/src/modes/acp/acp-agent.ts +730 -100
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/segments.ts +14 -7
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +37 -18
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +199 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +670 -217
- package/src/session/cache-economics.ts +275 -0
- package/src/session/context-estimation.ts +91 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/slash-commands/builtin-registry.ts +153 -9
- package/src/slash-commands/helpers/context-report.ts +26 -4
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT
|
|
2
2
|
Reflect.set(globalThis, Symbol.for("skc.docs-index.generated.loaded"), true);
|
|
3
3
|
|
|
4
|
-
export const EMBEDDED_DOC_FILENAMES: readonly string[] = ["ERRATA-GPT5-HARMONY.md","FORK_MAINTENANCE.md","REBRANDING_PLAN_260525.md","ai-schema-normalize.md","analyze-me-with-skc.md","aside-integration.md","auth-broker-gateway.md","bash-tool-runtime.md","blob-artifact-architecture.md","bot-integration.md","brand-assets.md","bridge.md","codebase-overview.md","codegraph-custom-tool.md","compaction.md","composer-codex-parity.md","computer-use/README.md","environment-variables.md","external-control-readiness.md","extragoal-skill-template.md","fs-scan-cache-architecture.md","geobench.md","git-daemon.md","grok-build-provider-design.md","handoff-generation-pipeline.md","hermes-mcp-bridge.md","hotspot-map-successor.md","keybindings.md","lsp-config.md","memory.md","models.md","multi-vendor-profiles.md","native-ffi-optimization-policy.md","natives-addon-loader-runtime.md","natives-architecture.md","natives-binding-contract.md","natives-build-release-debugging.md","natives-media-system-utils.md","natives-package-split-plan.md","natives-rust-task-cancellation.md","natives-shell-pty-process.md","natives-text-search-pipeline.md","non-compaction-retry-policy.md","notebook-tool-runtime.md","notifications-sdk.md","onboarding-packet.md","onboarding-receipt.md","ooo-bridge-extension-contract.md","openclaw-hermes-rpc-integration.md","perf-profiling-corpus.md","porting-from-pi-mono.md","porting-to-natives.md","prompt-architect-reports/README.md","prompt-architect-reports/recovered-context/0-ToolPrompts.recovered.md","prompt-architect-reports/recovered-context/1-SystemPrompts.recovered.md","prompt-architect-reports/recovered-context/3-SkillMiscPrompts.recovered.md","prompt-architect-reports/recovery-summary.md","prompt-architect-reports/system-prompts.raw.md","prompt-architect-reports/tool-prompts.raw.md","provider-streaming-internals.md","python-repl.md","readme/README.de.md","readme/README.es.md","readme/README.fr.md","readme/README.ja.md","readme/README.ko.md","readme/README.zh.md","render-mermaid.md","research-plan-ledger.md","resolve-tool-runtime.md","rpc.md","rulebook-matching-pipeline.md","sayknow-remote.md","sdk.md","secrets.md","session-operations-export-share-fork-resume.md","session-switching-and-recent-listing.md","session-tree-plan.md","session.md","skc-dogfood-skill-template.md","skc-plugins.md","skc-session-clawhip-routing.md","standalone-mcp.md","telegram-onboarding.md","telegram-remote.md","theme.md","tools/ask.md","tools/ast-edit.md","tools/ast-grep.md","tools/bash.md","tools/bisect.md","tools/browser.md","tools/calc.md","tools/checkpoint.md","tools/computer.md","tools/cron.md","tools/debug.md","tools/edit.md","tools/eval.md","tools/find.md","tools/github.md","tools/irc.md","tools/job.md","tools/lsp.md","tools/monitor.md","tools/read.md","tools/recipe.md","tools/render_mermaid.md","tools/resolve.md","tools/rewind.md","tools/search.md","tools/search_tool_bm25.md","tools/ssh.md","tools/task.md","tools/todo_write.md","tools/web_search.md","tools/write.md","tree.md","ttsr-injection-lifecycle.md","tui-runtime-internals.md","ui-design-visual-qa.md"];
|
|
4
|
+
export const EMBEDDED_DOC_FILENAMES: readonly string[] = ["ERRATA-GPT5-HARMONY.md","FORK_MAINTENANCE.md","REBRANDING_PLAN_260525.md","ai-schema-normalize.md","analyze-me-with-skc.md","aside-integration.md","auth-broker-gateway.md","bash-tool-runtime.md","blob-artifact-architecture.md","bot-integration.md","brand-assets.md","bridge.md","codebase-overview.md","codegraph-custom-tool.md","compaction.md","composer-codex-parity.md","computer-use/README.md","environment-variables.md","external-control-readiness.md","extragoal-skill-template.md","fs-scan-cache-architecture.md","geobench.md","git-daemon.md","gpt-5.6-codex-preset-benchmark.md","grok-build-provider-design.md","handoff-generation-pipeline.md","hermes-mcp-bridge.md","hotspot-map-successor.md","keybindings.md","lsp-config.md","memory.md","models.md","multi-vendor-profiles.md","native-ffi-optimization-policy.md","natives-addon-loader-runtime.md","natives-architecture.md","natives-binding-contract.md","natives-build-release-debugging.md","natives-media-system-utils.md","natives-package-split-plan.md","natives-rust-task-cancellation.md","natives-shell-pty-process.md","natives-text-search-pipeline.md","non-compaction-retry-policy.md","notebook-tool-runtime.md","notifications-sdk.md","onboarding-packet.md","onboarding-receipt.md","ooo-bridge-extension-contract.md","openclaw-hermes-rpc-integration.md","perf-profiling-corpus.md","porting-from-pi-mono.md","porting-to-natives.md","prompt-architect-reports/README.md","prompt-architect-reports/recovered-context/0-ToolPrompts.recovered.md","prompt-architect-reports/recovered-context/1-SystemPrompts.recovered.md","prompt-architect-reports/recovered-context/3-SkillMiscPrompts.recovered.md","prompt-architect-reports/recovery-summary.md","prompt-architect-reports/system-prompts.raw.md","prompt-architect-reports/tool-prompts.raw.md","provider-streaming-internals.md","python-repl.md","readme/README.de.md","readme/README.es.md","readme/README.fr.md","readme/README.ja.md","readme/README.ko.md","readme/README.zh.md","render-mermaid.md","research-plan-ledger.md","resolve-tool-runtime.md","rpc.md","rulebook-matching-pipeline.md","sayknow-remote.md","sdk.md","secrets.md","session-operations-export-share-fork-resume.md","session-switching-and-recent-listing.md","session-tree-plan.md","session.md","skc-dogfood-skill-template.md","skc-plugins.md","skc-session-clawhip-routing.md","standalone-mcp.md","telegram-onboarding.md","telegram-remote.md","theme.md","tools/ask.md","tools/ast-edit.md","tools/ast-grep.md","tools/bash.md","tools/bisect.md","tools/browser.md","tools/calc.md","tools/checkpoint.md","tools/computer.md","tools/cron.md","tools/debug.md","tools/edit.md","tools/eval.md","tools/find.md","tools/github.md","tools/irc.md","tools/job.md","tools/lsp.md","tools/monitor.md","tools/read.md","tools/recipe.md","tools/render_mermaid.md","tools/resolve.md","tools/rewind.md","tools/search.md","tools/search_tool_bm25.md","tools/ssh.md","tools/task.md","tools/todo_write.md","tools/web_search.md","tools/write.md","tree.md","ttsr-injection-lifecycle.md","tui-runtime-internals.md","ui-design-visual-qa.md"];
|
|
5
5
|
|
|
6
6
|
export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
7
7
|
"ERRATA-GPT5-HARMONY.md": "# ERRATA — GPT-5 Harmony-Header Leakage\n\n## 1. The problem\n\nOpenAI frames tool calls in the Harmony chat protocol:\n\n```\n<|start|>assistant<|channel|>commentary to=functions.<NAME><|message|>{ARGS}<|call|>\n```\n\n`<|channel|>commentary to=functions.NAME` is the **routing header** —\ncontrol tokens consumed by the runtime to dispatch the call. These\ntokens never appear as content under normal operation; the runtime\nstrips them.\n\nThe defect: gpt-5 models occasionally emit, **as ordinary content\ninside `{ARGS}`**, the **plain-text shadow** of these routing tokens —\nthe same characters without the `<|…|>` brackets — and continue\nproducing more pseudo-routing structure (channel name, body marker,\nmultilingual spam, fake tool-result framing). The contamination lives\ninside the visible tool argument and is dispatched to the tool as if it\nwere intended content.\n\n**Critical detail.** The actual `<|start|>` / `<|channel|>` /\n`<|message|>` / `<|call|>` special tokens almost never appear in tool\nargs. What leaks is the bracket-less spelling — `analysis to=functions.X\ncode …` — because OpenAI applies a logit mask suppressing the\ncontrol-token IDs inside the args region. The mass that would have gone\nto those special tokens redistributes onto the un-bracketed plain-text\nrepresentation the model also learned. This makes the leak structurally\ninvisible to the routing parser and lands it in the tool input verbatim.\n\nManifestation in tool args (real corpus example):\n\n```\n~ add_function(iso, ctx, ns, \"installSystemChangeObserver\",\n os_install_system_change_observer);】【\"】【analysis to=functions.edit\n code above เงินไทยฟรีuser to=functions.edit code …\n```\n\nThe leading code is real and intended. Everything after the first\nnon-Latin token through the next clean structural boundary is corruption.\n\n---\n\n## 2. Observed statistics & failure modes\n\nSource: `~/.skc/stats.db` (`ss_tool_calls`, `ss_assistant_msgs`), through\n2026-05-10. 1.05M tool calls scanned.\n\n### 2.1 Rate\n\n| Model | Leaks in tool args | Calls | per million |\n|------------------|-------------------:|--------:|------------:|\n| gpt-5.4 | 37 | 226,957 | 163 |\n| gpt-5.3-openai-code | 17 | 112,243 | 151 |\n| gpt-5.5 | 2 | 80,750 | 25 |\n| gpt-5.2-openai-code | 0 | — | — |\n\nPlus 15 hits in assistant visible text / thinking blobs.\n\n### 2.2 Tool distribution\n\n| Tool | Hits |\n|---------------------|-----:|\n| `edit` | 38 |\n| `eval` | 11 |\n| `report_tool_issue` | 3 |\n| `grep`/`read`/`search`/`yield` | 1 each |\n\nConcentrated in tools with free-form (non-JSON-schema) argument formats.\n\n### 2.3 Leak shape (deterministic)\n\n```\nLEAK ::= JUNK_PREFIX MARKER CHANNEL_BODY (LEAK)?\nMARKER ::= \"to=functions.\" TOOL_NAME\nCHANNEL_BODY ::= \" code \" (SPAM | reasoning_prose | fake_tool_output)*\nJUNK_PREFIX ::= (GLITCH_TOKEN | CHANNEL_WORD | NON_LATIN_RUN | \"}\" | \"】【\")+\n```\n\n**Cascading is common.** Of 96 marker occurrences across 71 contaminated\nrecords, 39 contain ≥2 markers and 7 contain ≥3 — the model emits\nmultiple fake `to=functions.X code …` blocks back-to-back, often with\nfake `code_output\\nCell N:\\n…` framing between them. Once the\nplain-text scaffolding is in the residual stream, the prefix now *looks\nlike* a fresh tool envelope start, so the macro prior over continuations\nkeeps voting for more scaffolding. Self-amplifying.\n\n### 2.4 Glitch tokens\n\nSingle-token identifiers in `o200k_base` whose embeddings appear to be\nnear-init from underrepresentation in post-training. ASCII residue\nimmediately before the marker in the natural corpus:\n\n| Surface string | Single-token | Token ID | Hits in corpus |\n|-------------------|:-:|---------:|---:|\n| `Japgolly` | ✅ | 199,745 | 1 |\n| `Jsii` | ✅ | 114,318 | (subtoken of `Jsii_commentary`) |\n| `Jsii_commentary` | — (3 toks) | — | 2 |\n| `changedFiles` | — (2 toks) | — | 8 |\n| `RTLU` | — (2 toks) | — | 3 |\n\n`Japgolly` is in the last 0.13% of the vocabulary — the same family of\nGitHub-corpus residue that produced `SolidGoldMagikarp` in the 2023\nGPT-2 vocabulary (Rumbelow & Watkins). `SolidGoldMagikarp` itself\ntokenizes to 5 tokens in `o200k_base` — that specific token was retired,\nbut the class wasn't.\n\nFor the multi-token entries, the corpus-level signature is the surface\nstring; the underlying glitch trigger is a sub-token (e.g. `Jsii` inside\n`Jsii_commentary`). The detector list (`G` signal) keys on the surface\nstrings.\n\nStable across unrelated sessions. Treated as a high-precision detector\nsignal.\n\n### 2.5 Channel-word leakage\n\n`analysis` (5), `assistant` (5), `commentary` (3), `user` (1) appear\ndirectly preceding `to=`. Always bare words; never `<|channel|>analysis`\nor any other bracketed form. Consistent with §1 — the brackets are\nmasked, the words are not.\n\n### 2.6 Non-Latin spam residue\n\n96 marker hits, by script: CJK 40, Cyrillic 12, Telugu/Kannada/Malayalam\n18, Thai 8, Georgian 7, Armenian 7, Arabic 1. Recurring fragments are\nChinese gambling SEO (`大发时时彩`, `天天中彩票`), Georgian/Abkhaz junk,\nand Thai casino spam — well-known low-quality crawl residue.\n\nThis is the same script distribution observed in the controlled\nreproduction (§7.3), independent of the prompt's natural language.\n\n### 2.7 Failure-mode breakdown for the `edit` tool\n\nThe `edit` tool exists in two variants in the corpus:\n\n| Variant | Calls | Recovery |\n|--------------------------|------:|----------|\n| Patch-DSL (`§PATH`/anchor/`«»≔` ops) | 27 | **Recoverable** by op-truncation (§3.3) |\n| JSON-schema (`{path,edits:[…]}`) | 11 | **Not recoverable** — contamination is escaped *inside* JSON strings, parser accepts it cleanly, content would be written verbatim into source files |\n\nFor Patch-DSL leaks specifically:\n\n- 20/27 cases: contamination on the last input line; nothing follows.\n- 7/27 cases: contamination mid-input; what follows is one of: a\n duplicate replay of an earlier file/anchor, intended content for a\n *different* tool call (the model started its next call inline), or\n pure hallucination. Post-contamination content is never trustworthy.\n\n### 2.8 Mechanism (confirmed)\n\n**Prior collapse from null-embedding glitch tokens, into a\ncontrol-token-masked basin whose mass redistributes onto the\nplain-text shadow of the Harmony protocol.**\n\nStep by step:\n\n1. The model is mid-`{ARGS}` of a Harmony tool call. The runtime applies\n a logit mask suppressing structural control tokens (`<|channel|>`,\n `<|message|>`, `<|call|>`, `<|start|>`, `<|end|>`) inside the args\n region. Without this mask, normal generation would constantly\n hallucinate envelope-closes; with it, those token IDs have logit\n `-∞` in args.\n2. A glitch token `g` is sampled. By construction `g` was in the BPE\n merge corpus but barely in LM/RL training, so its **input embedding\n `e_g` ≈ near-init noise of small norm**.\n3. At position t+1, the residual update `h_{t+1} ≈ LN(h_t + e_g + Attn +\n MLP)` is dominated by the prefix-derived terms; the just-emitted-token\n signal is effectively absent. Generation diversity normally comes\n from `e_x` steering the residual into different sub-regions —\n stripped here.\n4. The next-token distribution therefore collapses onto the **conditional\n prior over continuations of the prefix, with local conditioning\n removed**. In a tool-calling rollout context, that prior is sharply\n peaked on Harmony scaffolding (control tokens + routing tokens) —\n that's what RL trained.\n5. The mask zeros the control-token IDs. Mass redistributes onto the\n **next-best continuation**: the un-bracketed surface-form spelling of\n the same protocol (`analysis`, `commentary`, ` to=functions.X`,\n ` code `). This spelling is unmasked because those characters are\n ordinary tokens.\n6. Once a few tokens of plain-text scaffolding land in the residual\n stream, the prefix now resembles a fresh envelope start. The macro\n prior keeps voting for more scaffolding. Cascading (§2.3) follows.\n7. Multilingual spam after the marker is the same prior-collapse\n continuation, drawn from the training neighborhood of the glitch\n token (often ESL/auto-generated multilingual web junk — exactly the\n crawl residue in §2.6).\n\n**Two corollaries the corpus data demanded but only the experiment\nexplained:**\n\n- **The brackets never appear** (§1, §2.5). The mask is what makes the\n leak land in plain text instead of as a real envelope-close.\n- **Counterintuitive grammar dependency** (§7.4). The leak is *worse* in\n formats closest to OpenAI's training distribution. Off-distribution\n custom grammars dampen the macro-prior basin; the official\n `*** Begin Patch` format is the strongest collapse target.\n\nThe 2023 SolidGoldMagikarp paper documented mechanism (1)+(2)+(4). The\nnew piece is (5): when constrained decoding masks the natural collapse\ntarget, the mass laundered through the un-masked plain-text shadow\nbecomes a structurally-invisible exfiltration channel.",
|
|
@@ -13,9 +13,9 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
13
13
|
"auth-broker-gateway.md": "# Auth Broker and Auth Gateway\n\nThe auth broker and auth gateway are two cooperating HTTP services that move OAuth refresh tokens and provider access tokens off developer laptops and into a single broker host.\n\n- **`skc auth-broker serve`** holds the canonical SQLite credential vault, performs OAuth refreshes, and exposes a small REST API (`/v1/snapshot`, `/v1/credential/:id/refresh`, `/v1/credential/:id/disable`, `/v1/credential`, `/v1/usage`, `/v1/healthz`).\n- **`skc auth-gateway serve`** is a forward-proxy. It accepts OpenAI Chat Completions, Anthropic Messages, and OpenAI Responses requests, injects the broker-resolved access token, and forwards the bytes to the real provider. Clients (containerised skc, llm-git, the macOS usage widget, …) never see the access token.\n\nTransport security between operator, broker, and gateway is delegated to the operator (Tailscale / Wireguard / reverse proxy + TLS). Every endpoint except `/v1/healthz` (broker) and `/healthz` (gateway) requires a bearer token.\n\nSource: `packages/ai/src/auth-broker/`, `packages/ai/src/auth-gateway/`, `packages/coding-agent/src/cli/auth-broker-cli.ts`, `packages/coding-agent/src/cli/auth-gateway-cli.ts`, `packages/coding-agent/src/session/auth-broker-config.ts`.\n\n## Data flow\n\n```\n ┌────────────────────────────────────────────────────────────┐\n │ broker host │\n │ │\n developer ──▶ │ ┌──────────────────────────┐ ┌────────────────────┐ │\n laptop / │ │ skc auth-broker serve │◀──▶│ SQLite agent.db │ │\n CI / roboskc │ │ - holds refresh tokens │ │ (canonical writer)│ │\n │ │ - background refresher │ └────────────────────┘ │\n │ │ /v1/{snapshot,refresh,…}│ │\n │ └─────────┬────────────────┘ │\n │ │ bearer ($CONFIG_DIR/auth-broker.token) │\n │ ▼ │\n │ ┌──────────────────────────┐ │\n │ │ skc auth-gateway serve │ RemoteAuthCredentialStore │\n │ │ /v1/{chat,messages,…} │ pulls /v1/snapshot at boot, │\n │ │ /v1/usage, /v1/models │ refreshes credentials by id │\n │ └─────────┬────────────────┘ via the broker on expiry │\n └────────────┼───────────────────────────────────────────────┘\n │ bearer ($CONFIG_DIR/auth-gateway.token)\n ▼\n unauthenticated clients\n (llm-git, macOS widget, roboskc containers, IDE plugins, …)\n │\n ▼ same path is forwarded with Authorization\n api.anthropic.com / api.openai.com / …\n```\n\nThe broker is the only writer of OAuth refresh tokens. Clients (including the gateway itself) load a redacted snapshot in which every `refresh` field has been replaced with `REMOTE_REFRESH_SENTINEL`; when an access token expires the client calls `POST /v1/credential/:id/refresh` and the broker performs the refresh server-side. `RemoteAuthCredentialStore` rejects any local code path that tries to write through it, with an error pointing at `skc auth-broker login` / `skc auth-broker logout`.\n\n## auth-broker\n\n### CLI\n\n```\nskc auth-broker serve [--bind=host:port] # boot the broker\nskc auth-broker token [--regenerate] [--json] # print or rotate the bearer token\nskc auth-broker login <provider> [--via=user@host] [--dry-run]\nskc auth-broker logout <provider>\nskc auth-broker import <file|dir> [--provider=<id>] [--include-disabled] [--dry-run] [--json]\nskc auth-broker migrate --from-local [--dry-run] [--json]\nskc auth-broker status [--json]\n```\n\n- `serve` opens the local SQLite store at `getAgentDbPath()` and binds an HTTP listener (default `127.0.0.1:8765`). On startup a token is ensured at `<config-dir>/auth-broker.token` (mode `0600`, `0700` parent dir). The background refresher refreshes any OAuth credential whose `expires - Date.now() < refreshSkewMs` (default 5 min) every `refreshIntervalMs` (default 60 s).\n- `token` prints the cached bearer or generates a new one. `--regenerate` rotates it.\n- `login <provider>` runs the per-provider OAuth flow locally, or — with `--via=user@host` — `ssh -L <callback-port>:127.0.0.1:<callback-port> user@host skc auth-broker login <provider>` so the OAuth callback hits the local browser but the credential is written on the broker host. Built-in callback ports: `anthropic:54545`, `openai-code:1455`, `google-gemini-cli:8085`, `google-antigravity:51121`, `gitlab-duo:8080`.\n- `logout <provider>` deletes every credential row for `<provider>`.\n- `import <file|dir>` imports CLIProxyAPI-style JSON credentials into the local SQLite store. Maps `type` field → skc provider (`anthropic-model → anthropic`, `openai-code → openai-code`, `gemini → google-gemini-cli`, `antigravity → google-antigravity`, `gemini-cli → google-gemini-cli`).\n- `migrate --from-local` walks the local SQLite store + env-derived credentials and idempotently uploads them to the configured broker (`POST /v1/credential`).\n- `status` health-pings the configured remote broker.\n\n### Endpoints\n\n| Method | Path | Auth | Purpose |\n| ------ | ---- | ---- | ------- |\n| `GET` | `/v1/healthz` | none | Liveness + version |\n| `GET` | `/v1/snapshot` | bearer | Redacted snapshot (refresh tokens replaced by sentinel) |\n| `POST` | `/v1/credential` | bearer | Upsert one OAuth or API-key credential |\n| `POST` | `/v1/credential/:id/refresh` | bearer | Force-refresh one OAuth credential |\n| `POST` | `/v1/credential/:id/disable` | bearer | Disable one credential with a recorded cause |\n| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` across credentials |\n\nRequests use `Authorization: Bearer <token>`. The server compares against an in-memory token allow-list; the gateway’s implementation uses a timing-safe comparison.\n\n### Background refresher\n\n`AuthBrokerRefresher` iterates active OAuth credentials at `refreshIntervalMs` cadence and refreshes any within `refreshSkewMs` of expiry. Refreshes are single-flighted per credential id so a slow refresh cannot be retriggered. The refresher distinguishes:\n\n- **definitive failures** (`invalid_grant`, `invalid_token`, `revoked`, unauthorized refresh-token, 401/403 not from a network blip) — credentials are passed to `AuthStorage.disableCredentialById(id, cause)` so the next snapshot pull surfaces a clean delete on the client;\n- **transient failures** (timeout / ECONNREFUSED / fetch failed) — left in place for the next sweep.\n\n## auth-gateway\n\n### CLI\n\n```\nskc auth-gateway serve [--bind=host:port] [--no-auth]\nskc auth-gateway token [--regenerate] [--json]\nskc auth-gateway status [--json]\n```\n\n- `serve` requires `SKC_AUTH_BROKER_URL` (or `auth.broker.url` in `config.yml`) — the gateway is itself a broker client. It calls `AuthBrokerClient.fetchSnapshot()`, wraps it in `RemoteAuthCredentialStore`, and constructs an `AuthStorage` that resolves access tokens through the broker. Default bind is `127.0.0.1:4000`. The gateway token is stored at `<config-dir>/auth-gateway.token` (`0600`); `--no-auth` disables the bearer check entirely (loopback-only use).\n- `token` / `status` mirror the broker’s equivalents.\n\n### Endpoints\n\n| Method | Path | Auth | Purpose |\n| ------ | ---- | ---- | ------- |\n| `GET` | `/healthz` | none | Liveness + version |\n| `GET` | `/v1/usage` | bearer | Aggregate `UsageReport[]` (proxied through `AuthStorage`) |\n| `GET` | `/v1/models` | bearer | Bundled-model catalog filtered to providers with credentials |\n| `POST` | `/v1/chat/completions` | bearer | OpenAI Chat Completions wire format |\n| `POST` | `/v1/messages` | bearer | Anthropic Messages wire format |\n| `POST` | `/v1/responses` | bearer | OpenAI Responses wire format |\n\nThe model id is read from the top-level `model` field. The gateway picks the first bundled `Model<Api>` matching that id and:\n\n- **Passthrough fast-path** — when the inbound wire format matches the model’s native API (`openai-chat → openai-completions`, `anthropic-messages → anthropic-messages`, `openai-responses → openai-responses`), the request body is forwarded byte-for-byte with the client `Authorization`/`x-api-key` stripped and replaced by `Authorization: Bearer <resolved-access-token>`. Provider-specific fields (`cache_control`, `service_tier`, tool-choice extensions, …) flow through unmodified. Hop-by-hop headers (RFC 7230) plus `Content-Encoding`/`Content-Length` are stripped from the upstream response.\n- **Translate path** — when the inbound format and the resolved model’s API differ (e.g. `/v1/chat/completions` targeting an Anthropic model, or `/v1/responses` targeting `openai-code-responses` which runs over a websocket transport), the request is parsed against the wire schema, rebuilt into an skc `Context`, dispatched through `streamSimple()`, and re-encoded back to the inbound format (SSE for streamed responses).\n\n`idleTimeout` on the underlying `Bun.serve` is set to `255 s` so long thinking-budget calls do not get killed by Bun’s default idle timeout.\n\n## Usage cache: server-side 5-min jitter + client-side 15 s single-flight\n\nTwo layers cache the aggregate provider-usage report. Both are intentional and stacked.\n\n### Server-side cache (broker `AuthStorage`)\n\n`AuthStorage` caches each credential’s `UsageReport` in the broker’s SQLite store at a **5-minute per-credential TTL with ±25 % jitter**. Anthropic and OpenAI rate-limit `/usage` aggressively per source IP, and a synchronized 5-credential fan-out trips 429s every cycle; the jitter decorrelates refresh times within a few cycles. On fetch failure the store keeps the **last-good** report for up to 24 h with a short jittered re-poll window — so a transient upstream blip never blanks out the widget.\n\nConstants: `USAGE_REPORT_TTL_MS = 5 * 60_000`, `USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000` (`packages/ai/src/auth-storage.ts`).\n\n### Client-side single-flight (`RemoteAuthCredentialStore`)\n\nWhen the gateway (or any other broker client) calls `fetchUsageReports()` / `getUsageReport(provider, credential)`, `RemoteAuthCredentialStore` coalesces concurrent calls into a single `GET /v1/usage` round-trip and caches the result for **15 s** in memory.\n\n- `USAGE_CACHE_TTL_MS = 15_000` (`packages/ai/src/auth-broker/remote-store.ts`).\n- A single `#usageInflight` promise is shared across all callers; a per-caller `AbortSignal` is **raced** against the shared promise, not threaded into it, so one caller’s abort never cascades into a peer’s in-flight request.\n- On fetch failure the rejected promise is logged and the awaited value is `null` — callers (`AuthStorage.fetchUsageReports`, `#getUsageReport`) treat a `null` report as \"no usage signal for this cycle\" and proceed without it. **This is the 15 s TTL fallback**: the client absorbs transient broker outages by suppressing the error, returning `null` to ranking, and re-attempting after the 15 s window.\n\nThe 15 s client window deliberately sits below the broker’s 5 min server cache, so almost every client poll is served from the broker’s already-cached value; the client cache exists to absorb the parallel fan-out generated by `AuthStorage.#rankOAuthSelections` into a single broker round-trip.\n\n## Operator opt-in\n\nThe broker is **off** unless `SKC_AUTH_BROKER_URL` (or `auth.broker.url` in `config.yml`) is set. When set, `discoverAuthStorage` in `packages/coding-agent/src/sdk.ts` swaps the local SQLite credential store for `RemoteAuthCredentialStore` and every API call resolves credentials through the broker.\n\n### Environment variables\n\n| Variable | Purpose | Required when |\n| -------- | ------- | ------------- |\n| `SKC_AUTH_BROKER_URL` | Base URL of the remote auth-broker (e.g. `https://broker.tailnet:8765`). Selecting this puts the client in broker mode — local SQLite is bypassed. | Any time the skc client should resolve credentials through a broker (and required by `skc auth-gateway serve`). |\n| `SKC_AUTH_BROKER_TOKEN` | Bearer token used for every broker endpoint except `/v1/healthz`. | When `SKC_AUTH_BROKER_URL` is set and no token is available from `auth.broker.token` or `<config-dir>/auth-broker.token`. |\n\nResolution order in `resolveAuthBrokerConfig()`:\n\n1. `SKC_AUTH_BROKER_URL` env (else `auth.broker.url` from `config.yml`, with `$ENV_NAME` resolution);\n2. `SKC_AUTH_BROKER_TOKEN` env (else `auth.broker.token` from `config.yml`, else `<config-dir>/auth-broker.token`);\n3. URL set but no token resolvable → hard error pointing at the token file path.\n\nThe gateway has no dedicated env vars — it inherits `SKC_AUTH_BROKER_*` because it is itself a broker client.\n\n### `config.yml` keys\n\n| Key | Default | Purpose |\n| --- | ------- | ------- |\n| `auth.broker.url` | unset | Same as `SKC_AUTH_BROKER_URL`; env wins. Hidden from the settings UI. |\n| `auth.broker.token` | unset | Same as `SKC_AUTH_BROKER_TOKEN`; env wins. Values may be the literal token or `$ENV_NAME` to indirect through env. |\n\n### Token files\n\n| Path | Owner | Mode |\n| ---- | ----- | ---- |\n| `<config-dir>/auth-broker.token` | `skc auth-broker serve` (created at first start) | `0600` in a `0700` parent dir |\n| `<config-dir>/auth-gateway.token` | `skc auth-gateway serve` (skipped under `--no-auth`) | `0600` in a `0700` parent dir |\n\n`<config-dir>` resolves to `~/.skc/` (respecting `SKC_CONFIG_DIR`).\n\n## Interaction with the local API-key resolution order\n\nThe broker only owns OAuth credentials and provider-API-key credentials that were uploaded to it. The standard credential ladder in `models.md` (`Auth and API key resolution order`) is preserved, with one addition committed alongside the gateway:\n\n- `AuthStorage.setConfigApiKey / removeConfigApiKey / clearConfigApiKeys` let a `models.yml` `apiKey` beat a stored OAuth token **without** overriding an explicit `--api-key`. This is what allows a broker-resolved OAuth credential to be reliably shadowed by a per-environment `models.yml` config key when both are present.\n\n## See also\n\n- [`secrets.md`](./secrets.md) — secret obfuscation around tokens that *do* leak through (e.g. `SKC_AUTH_BROKER_TOKEN` in shell output).\n- [`models.md`](./models.md) — provider auth resolution order; the broker plugs in at layers 2–3 (stored credentials).\n- [`environment-variables.md`](./environment-variables.md) — full env reference including `SKC_AUTH_BROKER_URL` / `SKC_AUTH_BROKER_TOKEN`.\n",
|
|
14
14
|
"bash-tool-runtime.md": "# Bash tool runtime\n\nThis document describes the **`bash` tool** runtime path used by agent tool calls, from command normalization to execution, truncation/artifacts, and rendering.\n\nIt also calls out where behavior diverges in interactive TUI, print mode, RPC mode, and user-initiated bang (`!`) shell execution.\n\n## Scope and runtime surfaces\n\nThere are two different bash execution surfaces in coding-agent:\n\n1. **Tool-call surface** (`toolName: \"bash\"`): used when the model calls the bash tool.\n - Entry point: `BashTool.execute()`.\n - Parameters include `command`, optional `env`, `timeout`, `cwd`, `head`, `tail`, `pty`, and, when `async.enabled` is true, `async`.\n2. **User bang-command surface** (`!cmd` from interactive input or RPC `bash` command): session-level helper path.\n - Entry point: `AgentSession.executeBash()`.\n\nBoth eventually use `executeBash()` in `src/exec/bash-executor.ts` for non-PTY execution, but only the tool-call path runs normalization/interception, optional managed background-job handling, and tool renderer logic.\n\n## End-to-end tool-call pipeline\n\n## 1) Input handling and parameter merge\n\n`BashTool.execute()` currently handles input before execution as follows:\n\n- validates optional `env` names against shell-variable syntax,\n- extracts a leading `cd <path> && ...` into `cwd` when `cwd` was not supplied,\n- rejects `async: true` when `async.enabled` is false,\n- uses only explicit `head`/`tail` tool args for post-run filtering.\n\n`normalizeBashCommand()` still exists in `src/tools/bash-normalize.ts`, but `BashTool.execute()` does not call it in the current source. Trailing shell pipes such as `| head -n 50` remain part of the shell command unless the caller uses the structured `head`/`tail` args.\n\n## 2) Optional interception (blocked-command path)\n\nIf `bashInterceptor.enabled` is true, `BashTool` loads rules from settings and runs `checkBashInterception()` against the normalized command.\n\nInterception behavior:\n\n- command is blocked **only** when:\n - regex rule matches, and\n - the suggested tool is present in `ctx.toolNames`.\n- invalid regex rules are silently skipped.\n- on block, `BashTool` throws `ToolError` with message:\n - `Blocked: ...`\n - original command included.\n\nDefault rule patterns (defined in code) target common misuses:\n\n- file readers (`cat`, `head`, `tail`, ...)\n- search tools (`grep`, `rg`, ...)\n- file finders (`find`, `fd`, ...)\n- in-place editors (`sed -i`, `perl -i`, `awk -i inplace`)\n- shell redirection writes (`echo ... > file`, heredoc redirection)\n\n### Caveat\n\n`InterceptionResult` includes `suggestedTool`, but `BashTool` currently surfaces only the message text (no structured suggested-tool field in `details`).\n\n## 3) CWD validation and timeout clamping\n\n`cwd` is resolved relative to session cwd (`resolveToCwd`), then validated via `stat`:\n\n- missing path -> `ToolError(\"Working directory does not exist: ...\")`\n- non-directory -> `ToolError(\"Working directory is not a directory: ...\")`\n\nTimeout is clamped to `[1, 3600]` seconds and converted to milliseconds.\n\n## 4) Artifact allocation\n\nBefore execution, the tool allocates an artifact path/id (best-effort) for truncated output storage.\n\n- artifact allocation failure is non-fatal (execution continues without artifact spill file),\n- artifact id/path are passed into execution path for full-output persistence on truncation.\n\n## 5) PTY vs non-PTY execution selection\n\n`BashTool` chooses PTY execution only when all are true:\n\n- tool input `pty === true`\n- `SKC_NO_PTY !== \"1\"`\n- tool context has UI (`ctx.hasUI === true` and `ctx.ui` set)\n\nOtherwise it uses non-interactive `executeBash()`.\n\nThat means print mode and non-UI RPC/tool contexts always use non-PTY.\n\n## Non-interactive execution engine (`executeBash`)\n\n## Shell session reuse model\n\n`executeBash()` caches native `Shell` instances in a process-global map keyed by:\n\n- shell path,\n- configured command prefix,\n- snapshot path,\n- serialized shell env,\n- optional agent session key.\n\nSession-level bang-command executions pass `sessionKey: this.sessionId`.\n\nTool-call executions pass `sessionKey: this.session.getSessionId?.()`, when available. In both surfaces, a session key isolates shell reuse per session; without one, reuse falls back to shell config/snapshot/env.\n\n## Shell config and snapshot behavior\n\nAt each call, executor loads settings shell config (`shell`, `env`, optional `prefix`).\n\nIf selected shell includes `bash`, it attempts `getOrCreateSnapshot()`:\n\n- snapshot captures aliases/functions/options from user rc,\n- snapshot creation is best-effort,\n- failure falls back to no snapshot.\n\nIf `prefix` is configured, command becomes:\n\n```text\n<prefix> <command>\n```\n\n## Streaming and cancellation\n\n`Shell.run()` streams chunks to `OutputSink` and optional `onChunk` callback.\n\nCancellation:\n\n- aborted signal triggers `shellSession.abort(...)`,\n- timeout from native result is mapped to `cancelled: true` + annotation text,\n- explicit cancellation similarly returns `cancelled: true` + annotation.\n\nNo exception is thrown inside executor for timeout/cancel; it returns structured `BashResult` and lets caller map error semantics.\n\n## Interactive PTY path (`runInteractiveBashPty`)\n\nWhen PTY is enabled, tool runs `runInteractiveBashPty()` which opens an overlay console component and drives a native `PtySession`.\n\nBehavior highlights:\n\n- xterm-headless virtual terminal renders viewport in overlay,\n- keyboard input is normalized (including Kitty sequences and application cursor mode handling),\n- `esc` while running kills the PTY session,\n- terminal resize propagates to PTY (`session.resize(cols, rows)`).\n\nEnvironment hardening defaults are injected for unattended runs:\n\n- pagers disabled (`PAGER=cat`, `GIT_PAGER=cat`, etc.),\n- editor prompts disabled (`GIT_EDITOR=true`, `EDITOR=true`, ...),\n- terminal/auth prompts reduced (`GIT_TERMINAL_PROMPT=0`, `SSH_ASKPASS=/usr/bin/false`, `CI=1`),\n- package-manager/tool automation flags for non-interactive behavior.\n\nPTY output is normalized (`CRLF`/`CR` to `LF`, `sanitizeText`) and written into `OutputSink`, including artifact spill support.\n\nOn PTY startup/runtime error, sink receives `PTY error: ...` line and command finalizes with undefined exit code.\n\n## Output handling: streaming, truncation, artifact spill\n\nBoth PTY and non-PTY paths use `OutputSink`.\n\n## OutputSink semantics\n\n- keeps an in-memory UTF-8-safe tail buffer (`DEFAULT_MAX_BYTES`, currently 50KB),\n- tracks total bytes/lines seen,\n- if artifact path exists and output overflows (or file already active), writes full stream to artifact file,\n- when memory threshold overflows, trims in-memory buffer to tail (UTF-8 boundary safe),\n- marks `truncated` when overflow/file spill occurs.\n\n`dump()` returns:\n\n- `output` (possibly annotated prefix),\n- `truncated`,\n- `totalLines/totalBytes`,\n- `outputLines/outputBytes`,\n- `artifactId` if artifact file was active.\n\n### Long-output caveat\n\nRuntime truncation is byte-threshold based in `OutputSink` (50KB default). It does not enforce a hard 2000-line cap in this code path.\n\n## Live tool updates and async jobs\n\nFor non-PTY foreground execution, `BashTool` uses a separate `TailBuffer` for partial updates and emits `onUpdate` snapshots while command is running.\n\nFor PTY execution, live rendering is handled by custom UI overlay, not by `onUpdate` text chunks.\n\nWhen `async.enabled` is true and the call passes `async: true`, `BashTool` starts a managed bash job, returns a running job result with a job id, and stores completion through the session managed-job path. Auto-backgrounding can also start this path after `bash.autoBackground.thresholdMs`.\n\n## Result shaping, metadata, and error mapping\n\nAfter execution:\n\n1. `cancelled` handling:\n - if abort signal is aborted -> throw `ToolAbortError` (abort semantics),\n - else -> throw `ToolError` (treated as tool failure).\n2. PTY `timedOut` -> throw `ToolError`.\n3. apply head/tail filters to final output text (`applyHeadTail`, head then tail).\n4. empty output becomes `(no output)`.\n5. attach truncation metadata via `toolResult(...).truncationFromSummary(result, { direction: \"tail\" })`.\n6. exit-code mapping:\n - missing exit code -> `ToolError(\"... missing exit status\")`\n - non-zero exit -> `ToolError(\"... Command exited with code N\")`\n - zero exit -> success result.\n\nSuccess payload structure:\n\n- `content`: text output,\n- `details.meta.truncation` when truncated, including:\n - `direction`, `truncatedBy`, total/output line+byte counts,\n - `shownRange`,\n - `artifactId` when available.\n\nBecause built-in tools are wrapped with `wrapToolWithMetaNotice()`, truncation notice text is appended to final text content automatically (for example: `Full: artifact://<id>`).\n\n## Rendering paths\n\n## Tool-call renderer (`bashToolRenderer`)\n\n`bashToolRenderer` is used for tool-call messages (`toolCall` / `toolResult`):\n\n- collapsed mode shows visual-line-truncated preview,\n- expanded mode shows all currently available output text,\n- warning line includes truncation reason and `artifact://<id>` when truncated,\n- timeout value (from args) is shown in footer metadata line.\n\n### Caveat: full artifact expansion\n\n`BashRenderContext` has `isFullOutput`, but current renderer context builder does not set it for bash tool results. Expanded view still uses the text already in result content (tail/truncated output) unless another caller provides full artifact content.\n\n## User bang-command component (`BashExecutionComponent`)\n\n`BashExecutionComponent` is for user `!` commands in interactive mode (not model tool calls):\n\n- streams chunks live,\n- collapsed preview keeps last 20 logical lines,\n- line clamp at 4000 chars per line,\n- shows truncation + artifact warnings when metadata is present,\n- marks cancelled/error/exit state separately.\n\nThis component is wired by `CommandController.handleBashCommand()` and fed from `AgentSession.executeBash()`.\n\n## Mode-specific behavior differences\n\n| Surface | Entry path | PTY eligible | Live output UX | Error surfacing |\n| ------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ |\n| Interactive tool call | `BashTool.execute` | Yes, when `pty=true` and UI exists and `SKC_NO_PTY!=1` | PTY overlay (interactive) or streamed tail updates | Tool errors become `toolResult.isError` |\n| Print mode tool call | `BashTool.execute` | No (no UI context) | No TUI overlay; output appears in event stream/final assistant text flow | Same tool error mapping |\n| RPC tool call (agent tooling) | `BashTool.execute` | Usually no UI -> non-PTY | Structured tool events/results | Same tool error mapping |\n| Interactive bang command (`!`) | `AgentSession.executeBash` + `BashExecutionComponent` | No (uses executor directly) | Dedicated bash execution component | Controller catches exceptions and shows UI error |\n| RPC `bash` command | `rpc-mode` -> `session.executeBash` | No | Returns `BashResult` directly | Consumer handles returned fields |\n\n## Operational caveats\n\n- Interceptor only blocks commands when suggested tool is currently available in context.\n- If artifact allocation fails, truncation still occurs but no `artifact://` back-reference is available.\n- Shell session cache has no explicit eviction in this module; lifetime is process-scoped.\n- PTY and non-PTY timeout surfaces differ:\n - PTY exposes explicit `timedOut` result field,\n - non-PTY maps timeout into `cancelled + annotation` summary.\n\n## Implementation files\n\n- [`src/tools/bash.ts`](../packages/coding-agent/src/tools/bash.ts) — tool entrypoint, input handling/interception, async and PTY/non-PTY selection, result/error mapping, bash tool renderer.\n- [`src/tools/bash-normalize.ts`](../packages/coding-agent/src/tools/bash-normalize.ts) — post-run head/tail filtering; also contains an unused command-normalization helper.\n- [`src/tools/bash-interceptor.ts`](../packages/coding-agent/src/tools/bash-interceptor.ts) — interceptor rule matching and blocked-command messages.\n- [`src/exec/bash-executor.ts`](../packages/coding-agent/src/exec/bash-executor.ts) — non-PTY executor, shell session reuse, cancellation wiring, output sink integration.\n- [`src/tools/bash-interactive.ts`](../packages/coding-agent/src/tools/bash-interactive.ts) — PTY runtime, overlay UI, input normalization, non-interactive env defaults.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink`, `TailBuffer`, truncation/artifact spill, and summary metadata.\n- [`src/tools/output-meta.ts`](../packages/coding-agent/src/tools/output-meta.ts) — truncation metadata shape + notice injection wrapper.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — session-level `executeBash`, message recording, abort lifecycle.\n- [`src/modes/components/bash-execution.ts`](../packages/coding-agent/src/modes/components/bash-execution.ts) — interactive `!` command execution component.\n- [`src/modes/controllers/command-controller.ts`](../packages/coding-agent/src/modes/controllers/command-controller.ts) — wiring for interactive `!` command UI stream/update completion.\n- [`src/modes/rpc/rpc-mode.ts`](../packages/coding-agent/src/modes/rpc/rpc-mode.ts) — RPC `bash` and `abort_bash` command surface.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://<id>` resolution.\n",
|
|
15
15
|
"blob-artifact-architecture.md": "# Blob and artifact storage architecture\n\nThis document describes how coding-agent stores large/binary payloads outside session JSONL, how truncated tool output is persisted, and how internal URLs (`artifact://`, `agent://`) resolve back to stored data.\n\n## Why two storage systems exist\n\nThe runtime uses two different persistence mechanisms for different data shapes:\n\n- **Content-addressed blobs** (`blob:sha256:<hash>`): global storage used to externalize large image base64 payloads and provider image data URLs from persisted session entries.\n- **Session-scoped artifacts** (files under `<sessionFile-without-.jsonl>/`): per-session text files used for full tool outputs and subagent outputs.\n\nThey are intentionally separate:\n\n- blob storage optimizes deduplication and stable references by content hash,\n- artifact storage optimizes append-only session tooling and human/tool retrieval by local IDs.\n\n## Storage boundaries and on-disk layout\n\n## Blob store boundary (global)\n\n`SessionManager` constructs `BlobStore(getBlobsDir())`, so blob files live in a shared global blob directory (not in a session folder).\n\nBlob file naming:\n\n- file path: `<blobsDir>/<sha256-hex>`\n- no extension\n- reference string stored in entries: `blob:sha256:<sha256-hex>`\n\nImplications:\n\n- same binary content across sessions resolves to the same hash/path,\n- writes are idempotent at the content level,\n- blobs can outlive any individual session file.\n\n## Artifact boundary (session-local)\n\n`ArtifactManager` derives artifact directory from session file path:\n\n- session file: `.../<timestamp>_<sessionId>.jsonl`\n- artifacts directory: `.../<timestamp>_<sessionId>/` (strip `.jsonl`)\n\nArtifact types share this directory:\n\n- truncated tool output files: `<numericId>.<toolType>.log` (for `artifact://`)\n- subagent output files: `<outputId>.md` (for `agent://`)\n\n## ID and name allocation schemes\n\n## Blob IDs: content hash\n\n`BlobStore.put()` computes SHA-256 over the bytes it is given and returns:\n\n- `hash`: hex digest,\n- `path`: `<blobsDir>/<hash>`,\n- `ref`: `blob:sha256:<hash>`.\n\nNo session-local counter is used.\n\n## Artifact IDs: session-local monotonic integer\n\n`ArtifactManager` scans existing `*.log` artifact files on first use to find max existing numeric ID and sets `nextId = max + 1`.\n\nAllocation behavior:\n\n- file format: `{id}.{toolType}.log`\n- IDs are sequential strings (`\"0\"`, `\"1\"`, ...)\n- resume does not overwrite existing artifacts because scan happens before allocation.\n\nIf artifact directory is missing, scanning yields empty list and allocation starts from `0`.\n\n## Agent output IDs (`agent://`)\n\n`AgentOutputManager` allocates IDs for subagent outputs as `<index>-<requestedId>` (optionally nested under parent prefix, e.g. `0-Parent.1-Child`). It scans existing `.md` files on initialization to continue from the next index on resume.\n\n## Persistence dataflow\n\n## 1) Session entry persistence rewrite path\n\nBefore session entries are written (`#rewriteFile` / incremental persist), `SessionManager` calls `prepareEntryForPersistence()` (via `truncateForPersistence`).\n\nKey behaviors:\n\n1. **Large string truncation**: oversized strings are cut and suffixed with `\"[Session persistence truncated large content]\"`; signature fields (`thinkingSignature`, `thoughtSignature`, `textSignature`) are cleared instead of truncated.\n2. **Transient field stripping**: `partialJson` and `jsonlEvents` are removed from persisted entries.\n3. **Image externalization to blobs**:\n - image blocks in `content` arrays are externalized when `data` is not already a blob ref and base64 length is at least threshold (`BLOB_EXTERNALIZE_THRESHOLD = 1024`),\n - provider-style `image_url` data URLs are externalized when they start with `data:image/` and contain `;base64,`,\n - image block `data` is stored as decoded binary bytes,\n - provider data URLs are stored as the original UTF-8 data URL string,\n - persisted values are replaced with `blob:sha256:<hash>`.\n\nThis keeps session JSONL compact while preserving recoverability.\n\n## 2) Session load rehydration path\n\nWhen opening a session (`setSessionFile`), after migrations, `SessionManager` runs `resolveBlobRefsInEntries()`.\n\nFor message/custom-message image blocks with `blob:sha256:<hash>` and for persisted provider `image_url` fields with blob refs:\n\n- reads blob bytes from blob store,\n- converts image-block bytes back to base64,\n- converts provider `image_url` blobs back to the original string,\n- mutates in-memory entry fields for runtime consumers.\n\nIf blob is missing:\n\n- `resolveImageData()` logs warning,\n- returns original ref string unchanged,\n- load continues (no hard crash).\n\n## 3) Tool output spill/truncation path\n\n`OutputSink` powers streaming output in bash/python/ssh and related executors.\n\nBehavior:\n\n1. Every chunk is sanitized and appended to in-memory tail buffer.\n2. When in-memory bytes exceed spill threshold (`DEFAULT_MAX_BYTES`, 50KB), sink marks output truncated.\n3. If an artifact path is available, sink opens a file writer and writes:\n - existing buffered content once,\n - all subsequent chunks.\n4. In-memory buffer is always trimmed to tail window for display.\n5. `dump()` returns summary including `artifactId` only when file sink was successfully created.\n\nPractical effect:\n\n- UI/tool return shows truncated tail,\n- full output is preserved in artifact file and referenced as `artifact://<id>`.\n\nIf file sink creation fails (I/O error, missing path, etc.), sink silently falls back to in-memory truncation only; full output is not persisted.\n\n## URL access model\n\n## `blob:` references\n\n`blob:sha256:<hash>` is a persistence reference inside session entry payloads, not an internal URL scheme handled by the router. Resolution is done by `SessionManager` during session load.\n\n## `artifact://<id>`\n\nHandled by `ArtifactProtocolHandler`:\n\n- requires active session artifact directory,\n- ID must be numeric,\n- resolves by matching filename prefix `<id>.`,\n- returns raw text (`text/plain`) from the matched `.log` file,\n- when missing, error includes list of available artifact IDs.\n\nMissing directory behavior:\n\n- if artifacts directory does not exist, throws `No artifacts directory found`.\n\n## `agent://<id>`\n\nHandled by `AgentProtocolHandler` over `<artifactsDir>/<id>.md`:\n\n- plain form returns markdown text,\n- `/path` or `?q=` forms perform JSON extraction,\n- path and query extraction cannot be combined,\n- if extraction requested, file content must parse as JSON.\n\nMissing directory behavior:\n\n- throws `No artifacts directory found`.\n\nMissing output behavior:\n\n- throws `Not found: <id>` with available IDs from existing `.md` files.\n\nRead tool integration:\n\n- `read` supports offset/limit pagination for non-extraction internal URL reads,\n- rejects `offset/limit` when `agent://` extraction is used.\n\n## Resume, fork, and move semantics\n\n## Resume\n\n- `ArtifactManager` scans existing `{id}.*.log` files on first allocation and continues numbering.\n- `AgentOutputManager` scans existing `.md` output IDs and continues numbering.\n- `SessionManager` rehydrates blob refs to base64 on load.\n\n## Fork\n\n`SessionManager.fork()` creates a new session file with new session ID and `parentSession` link, then returns old/new file paths. Artifact copying is handled by `AgentSession.fork()`:\n\n- attempts recursive copy of old artifact directory to new artifact directory,\n- missing old directory is tolerated,\n- non-ENOENT copy errors are logged as warnings and fork still completes.\n\nID implications after fork:\n\n- if copy succeeded, artifact counters in new session continue after max copied ID,\n- if copy failed/skipped, new session artifact IDs start from `0`.\n\nBlob implications after fork:\n\n- blobs are global and content-addressed, so no blob directory copy is required.\n\n## Move to new cwd\n\n`SessionManager.moveTo()` renames both session file and artifact directory to the new default session directory, with rollback logic if a later step fails. This preserves artifact identity while relocating session scope.\n\n## Failure handling and fallback paths\n\n| Case | Behavior |\n| -------------------------------------------------------- | --------------------------------------------------------------------- |\n| Blob file missing during rehydration | Warn and keep `blob:sha256:` ref string in-memory |\n| Blob read ENOENT via `BlobStore.get` | Returns `null` |\n| Artifact directory missing (`ArtifactManager.listFiles`) | Returns empty list (allocation can start fresh) |\n| Artifact directory missing (`artifact://` / `agent://`) | Throws explicit `No artifacts directory found` |\n| Artifact ID not found | Throws with available IDs listing |\n| OutputSink artifact writer init fails | Continues with tail-only truncation (no full-output artifact) |\n| No session file (some task paths) | Task tool falls back to temp artifacts directory for subagent outputs |\n\n## Binary blob externalization vs text-output artifacts\n\n- **Blob externalization** is for image payloads inside persisted session entry content and provider image data URLs; it replaces inline payload strings in JSONL with stable content refs.\n- **Artifacts** are plain text files for execution output and subagent output; they are addressable by session-local IDs through internal URLs.\n\nThe two systems intersect only indirectly (both reduce session JSONL bloat) but have different identity, lifetime, and retrieval paths.\n\n## Implementation files\n\n- [`src/session/blob-store.ts`](../packages/coding-agent/src/session/blob-store.ts) — blob reference format, hashing, put/get, externalize/resolve helpers.\n- [`src/session/artifacts.ts`](../packages/coding-agent/src/session/artifacts.ts) — session artifact directory model and numeric artifact ID/path allocation.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink` truncation/spill-to-file behavior and summary metadata.\n- [`src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts) — persistence transforms, blob rehydration on load, session fork/move interactions.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — artifact directory copy during interactive fork.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://` resolver.\n- [`src/internal-urls/agent-protocol.ts`](../packages/coding-agent/src/internal-urls/agent-protocol.ts) — `agent://` resolver + JSON extraction.\n- [`src/sdk.ts`](../packages/coding-agent/src/sdk.ts) — internal URL router wiring and artifacts-dir resolver.\n- [`src/task/output-manager.ts`](../packages/coding-agent/src/task/output-manager.ts) — session-scoped agent output ID allocation for `agent://`.\n- [`src/task/executor.ts`](../packages/coding-agent/src/task/executor.ts) — subagent output artifact writes (`<id>.md`) and temp artifact directory fallback.\n",
|
|
16
|
-
"bot-integration.md": "# External controller integration guide\n\nThis guide is for authors of bots and orchestrators that want to drive Sayknow-CLI (`skc`) without scraping terminal scrollback. Hermes, OpenClaw, GitHub bots, chatops bots, and custom schedulers are examples of external controllers; none of them need bespoke SKC behavior if they can speak the Coordinator MCP or RPC lifecycle below.\n\nSKC is an external runner. Your controller owns queueing, identity, policy, and credentials; SKC owns the coding-agent session, workflows, tools, artifacts, and evidence inside the selected repository or worktree.\n\n## Integration surfaces\n\nUse the smallest surface that fits your bot:\n\n| Surface | Best for | Command | Stability notes |\n| --- | --- | --- | --- |\n| Coordinator MCP | Any external controller that can call MCP tools to start/register tmux sessions, send turns, answer questions, and read artifacts. | `skc mcp-serve coordinator` | Preferred orchestration surface. `skc mcp-serve hermes` is a compatibility alias, not a separate contract. |\n| Setup adapter | Rendering a portable MCP config and operator instructions for a controller profile. | `skc setup hermes --root /path/to/repo` | Compatibility-oriented config renderer; does not call an LLM or validate provider credentials. |\n| RPC stdio | A controller that embeds a single `skc --mode rpc` subprocess and handles JSONL frames directly or through `python/skc-rpc`. | `skc --mode rpc` | Best for process-backed, single-session bot workers. |\n| Bridge HTTPS | Experimental remote control for an already-running session. | `skc --mode bridge` | Session-control endpoints are fail-closed by default; do not use as the default bot lifecycle surface yet. |\n| Visible tmux fallback | Human-supervised lanes where an existing visible `skc --tmux` pane should become coordinator-authoritative. | `skc --tmux`, then `skc_coordinator_register_session` | Use when an operator already opened a pane or wants direct terminal visibility. |\n\n## Recommended architecture\n\n```text\nexternal controller / bot\n ├─ chooses repo/worktree and task policy\n ├─ starts MCP server: skc mcp-serve coordinator\n ├─ starts or registers one SKC tmux session\n ├─ sends one bounded turn at a time\n ├─ answers structured questions explicitly\n ├─ marks turn completion/failure with report_status\n └─ reads artifacts/reports from allowlisted roots\n```\n\nDo not infer completion from terminal output. Treat durable turn state as authoritative and tmux tail output as advisory debug context only.\n\n## Coordinator MCP setup\n\nRender a non-mutating config preview:\n\n```sh\nskc setup hermes --root /path/to/repo --profile my-bot --repo my-repo\n```\n\nInstall into a Hermes-compatible profile only when the target path is intentional:\n\n```sh\nskc setup hermes \\\n --root /path/to/repo \\\n --profile my-bot \\\n --repo my-repo \\\n --mutation sessions,questions,reports \\\n --profile-dir /path/to/hermes/profile \\\n --install\n```\n\nRun provider-independent contract smokes before trying a live model:\n\n```sh\nskc setup hermes --root /path/to/repo --smoke --json\nskc mcp-serve coordinator --check --json\n```\n\nThe generated config uses these environment variables:\n\n| Variable | Purpose |\n| --- | --- |\n| `SKC_COORDINATOR_MCP_WORKDIR_ROOTS` | Required allowlist for workdirs and artifact paths. |\n| `SKC_COORDINATOR_MCP_MUTATIONS` | Startup opt-in for mutation classes: `sessions`, `questions`, `reports`, or `all`. |\n| `SKC_COORDINATOR_MCP_SESSION_COMMAND` | Command used to start real SKC sessions, defaulting to `skc --worktree` in generated setup. |\n| `SKC_COORDINATOR_MCP_PROFILE` | Optional profile namespace so one bot cannot enumerate another profile's state. |\n| `SKC_COORDINATOR_MCP_REPO` | Optional repo namespace so one repo cannot enumerate another repo's state. |\n| `SKC_COORDINATOR_MCP_STATE_ROOT` | Optional coordination state root; defaults under `.skc/state/coordinator-mcp`. |\n| `SKC_COORDINATOR_MCP_ARTIFACT_BYTE_CAP` | Maximum bytes returned by artifact reads. |\n\nMutating calls require both startup opt-in and per-call `allow_mutation: true`. Missing either one fails closed.\n\n## Generic smoke strategy\n\nUse three different smoke levels so CI does not depend on one operator's model, API key, tmux layout, or desktop:\n\n| Smoke | Required for CI | What it proves | Example |\n| --- | --- | --- | --- |\n| Contract smoke | Yes | MCP server metadata, tool discovery, exported tool names, input schemas, read-only default, and mutation-gate failures. No provider credentials or tmux pane required. | `skc mcp-serve coordinator --check --json` and focused tests around `tools/list` plus mutation denial. |\n| Dry-run lifecycle smoke | Yes when changed behavior affects lifecycle state | A generic controller can start/register a mocked session, send a turn, observe active-turn protection, report terminal status, and read the completed turn without a real LLM. | `bun test packages/coding-agent/test/coordinator-mcp.test.ts` uses mocked coordinator services and temporary state roots. |\n| Optional live smoke | No | One operator's local provider/model/profile/tmux setup can run end-to-end in their chosen repo. Failure diagnoses that setup; it must not fail CI or PR validation. | Start `skc mcp-serve coordinator` with local env, dispatch a tiny task, then report/read evidence. |\n\nA public bot integration change should at least preserve the contract smoke and local-leak docs test. Live smokes are diagnostics, not mandatory gates.\n\n## MCP tool contract\n\nRead-only tools:\n\n- `skc_coordinator_list_sessions`\n- `skc_coordinator_read_status`\n- `skc_coordinator_read_tail`\n- `skc_coordinator_read_turn`\n- `skc_coordinator_await_turn`\n- `skc_coordinator_list_questions`\n- `skc_coordinator_list_artifacts`\n- `skc_coordinator_read_artifact`\n- `skc_coordinator_read_coordination_status`\n- `skc_coordinator_watch_events`\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\nHigh-level delegation tools:\n\n- `skc_delegate_plan`\n- `skc_delegate_execute`\n- `skc_delegate_team`\n\nThe `skc_delegate_*` tools package common SKC workflows for hosts that want to delegate an entire planning, execution, or team turn without manually composing `start_session` and `send_prompt`. They use the same coordinator mutation gates and workdir allowlists as the lower-level session tools.\n\n### Start a managed SKC session\n\nCall `skc_coordinator_start_session` with a canonical workdir inside `SKC_COORDINATOR_MCP_WORKDIR_ROOTS`:\n\n```json\n{\n \"cwd\": \"/path/to/repo\",\n \"prompt\": \"Optional first bounded task prompt\",\n \"allow_mutation\": true\n}\n```\n\nThe returned payload includes `session.session_id`, `session_state`, and, when a prompt is provided, `turn_id`, `status`, `delivery`, `queued`, and `delivered`.\n\n### Register a visible tmux fallback session\n\nIf an operator already started a visible session, register it instead of starting a hidden coordinator session:\n\n```sh\nskc --tmux\n```\n\n```json\n{\n \"session_id\": \"visible-skc-1\",\n \"cwd\": \"/path/to/repo\",\n \"tmux_session\": \"visible-skc-1\",\n \"tmux_target\": \"visible-skc-1:0.0\",\n \"visible\": true,\n \"source\": \"operator-visible-tmux\",\n \"allow_mutation\": true\n}\n```\n\n`skc_coordinator_register_session` validates safe ids, workdir allowlists, tmux target syntax, and liveness before writing coordinator state.\n\n### Send work as turns\n\nSend one bounded task prompt and persist the returned `turn_id`:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"prompt\": \"Use /skill:ralplan to build a plan for ...\",\n \"allow_mutation\": true\n}\n```\n\nA session may have one active turn by default. A second prompt returns `active_turn_exists` unless the bot passes:\n\n- `queue: true` to enqueue a durable follow-up turn, or\n- `force: true` to supersede the previous active turn and audit the supersession.\n\n### Wait or watch for completion\n\nUse `skc_coordinator_read_turn` for polling or `skc_coordinator_await_turn` for bounded waiting:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"timeout_ms\": 30000,\n \"poll_interval_ms\": 1000,\n \"lines\": 80\n}\n```\n\nTerminal turn statuses are `completed`, `failed`, `cancelled`, and `superseded`. Non-terminal statuses include `queued`, `delivering`, `active`, `waiting_for_answer`, and `completing`.\n\nWhen the work is done, your bot must call `skc_coordinator_report_status` with the turn id. This writes the final response/error, evidence paths, and coordinator report that later reads consume:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"status\": \"completed\",\n \"summary\": \"Implemented the requested fix and ran focused tests.\",\n \"evidence_paths\": [\"/path/to/repo/test-output.txt\"],\n \"allow_mutation\": true\n}\n```\n\nUse `status: \"failed\"` plus `blocker` for provider failures, unrecoverable tool failures, missing credentials, policy denial, or task blockers.\nUse `status: \"cancelled\"` when the coordinator policy intentionally stops tracking an active turn, for example after an operator abort or a bot-side shutdown decision. This records the turn as terminal in coordinator state; it does not kill the underlying tmux process. To supersede one active turn with replacement work, send the replacement prompt with `force: true` and preserve the superseded turn id in your audit trail.\n\n### Forward finish/stop lifecycle notifications\n\nDiscord, Hermes, Clawhip, and similar external notifiers should be opt-in and should forward only the public lifecycle surface. Use one of these supported paths:\n\n- Coordinator controllers: watch or poll turn state with `skc_coordinator_watch_events`, `skc_coordinator_await_turn`, or `skc_coordinator_read_turn`, then notify from the terminal turn status your controller records with `skc_coordinator_report_status`.\n- In-process extensions or hooks: subscribe to the public lifecycle events `turn_end` and `agent_end` from the shared hook/extension event contract.\n\nRecommended notification mapping:\n\n| Notification intent | Public surface | Safe meaning |\n| --- | --- | --- |\n| Turn finished | `turn_end` or terminal coordinator turn status `completed` | One LLM turn produced its final assistant message. |\n| Agent stopped / finished | `agent_end` | The agent loop ended for the submitted prompt. |\n| Waiting for user | Coordinator turn status `waiting_for_answer` | The agent is blocked on a structured question. |\n| Failed or blocked | Coordinator status `failed` with a public `blocker` summary | The controller recorded a terminal failure. |\n| Cancelled / superseded | Coordinator status `cancelled` or `superseded` | The controller intentionally stopped tracking or replaced the turn. |\n\nDo not forward raw prompts, transcripts, tool outputs, hidden instructions, private configs, host paths, channel ids, webhook URLs, or tokens. If your notifier needs a human-readable sentence, create a caller-supplied sanitized summary and keep provider/tool details out of the payload.\n\nExample public-safe extension event payloads:\n\n```json\n{ \"type\": \"turn_end\", \"turnIndex\": 2, \"summary\": \"Turn finished; review the local SKC session for details.\" }\n```\n\n```json\n{ \"type\": \"agent_end\", \"summary\": \"Agent loop ended; no raw transcript is included.\" }\n```\n\nExample opt-in forwarding policy:\n\n```json\n{\n \"enabled\": true,\n \"events\": [\"turn_end\", \"agent_end\"],\n \"destination\": \"external-notifier-profile\",\n \"redaction\": \"metadata-only\"\n}\n```\n\nSKC does not currently expose a structured stop-reason field on `agent_end`; integrators that need `waiting_for_answer`, `failed`, `cancelled`, or `superseded` should prefer the Coordinator MCP turn status because it is explicit, terminal-state oriented, and safe to relay after controller-side redaction.\n\n### Answer structured questions\n\nList pending questions:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"status\": \"pending\"\n}\n```\n\nThen answer by id:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"question_id\": \"question-1\",\n \"answer\": { \"decision\": \"approve\" },\n \"allow_mutation\": true\n}\n```\n\nAlways answer the advertised shape. Do not synthesize approvals for destructive actions unless your bot policy explicitly permits that action.\n\n### Read artifacts and reports\n\nUse `skc_coordinator_list_artifacts` to inspect safe roots and `skc_coordinator_read_artifact` to read a bounded artifact:\n\n```json\n{ \"path\": \"/path/to/repo/.skc/ultragoal/ledger.jsonl\" }\n```\n\nArtifact paths are canonicalized, symlink escapes are rejected, and output is byte-capped. Use `skc_coordinator_read_coordination_status` for status reports written through `skc_coordinator_report_status`.\n\n## RPC stdio integration\n\nUse RPC when your bot owns a single worker subprocess rather than an MCP coordinator. The wire protocol is JSONL over stdio:\n\n```sh\nskc --mode rpc --provider anthropic --model claude-sonnet-4-5\n```\n\nRecommended Python client:\n\n```python\nfrom skc_rpc import RpcClient, WorkflowGate\n\nwith RpcClient(no_session=True, no_rules=True) as client:\n client.install_headless_ui()\n\n def on_gate(gate: WorkflowGate) -> None:\n if gate.kind == \"approval\":\n client.respond_gate(gate.gate_id, {\"decision\": \"approve\"})\n\n client.on_workflow_gate(on_gate)\n turn = client.prompt_and_wait(\"Inspect this repo and report the integration contract.\")\n print(turn.require_assistant_text())\n```\n\nRPC hosts can also expose host-owned tools and URI schemes. Use these to give SKC controlled access to your bot's issue tracker, queue, database rows, or artifact store without leaking long-lived credentials into the SKC process.\n\nKey RPC lifecycle facts:\n\n- `{ \"type\": \"ready\" }` means the subprocess is ready for commands.\n- `prompt` is acknowledged immediately; completion is observed through `agent_end` or `RpcClient.prompt_and_wait()`.\n- `workflow_gate` frames are answered with `workflow_gate_response`.\n- `extension_ui_request` frames are answered with `extension_ui_response` or a headless policy.\n- Host tool calls and host URI requests are explicit callback frames that must be completed or rejected by the host.\n- `RpcClient` enforces single-flight prompt lifecycle collection; use one client per concurrent worker.\n- `abort` and `abort_and_prompt` are the RPC cancellation commands for subprocess workers; coordinator MCP cancellation is recorded through terminal turn status instead.\n\n## Error handling playbook\n\n| Situation | Bot behavior |\n| --- | --- |\n| `coordinator_mutation_class_disabled:*` | Re-render setup with the required mutation class, or keep the bot in read-only mode. |\n| `coordinator_mutation_call_not_allowed:*` | Add `allow_mutation: true` only after policy approval for that specific call. |\n| `unknown_session` | Re-list sessions; start a new managed session or register the visible tmux fallback. |\n| `active_turn_exists` | Poll the active turn, send with `queue: true`, or use `force: true` only when supersession is intentional. |\n| `timeout` from `await_turn` | Treat as non-terminal. Poll again or inspect `read_status`/`read_tail`; do not mark failure solely from a bounded wait timeout. |\n| Coordinator cancellation | Use `skc_coordinator_report_status` with `status: \"cancelled\"` for an intentionally stopped turn, or send replacement work with `force: true` when supersession is policy-approved. This is coordinator state, not a tmux process kill. |\n| Stale tmux/session state | Check `read_status.session_state` and advisory liveness. Register a new visible session or report the turn failed with a recoverable blocker. |\n| Provider/auth failure | Capture the model/provider error in `report_status` with `status: \"failed\"`; do not retry forever without a policy budget. |\n| Artifact denied | Keep the artifact inside allowlisted roots and avoid symlink escapes. |\n| Malformed or invalid question answer | Re-read the question/gate schema and submit a value matching the advertised shape. |\n| Bot shutdown | Persist `session_id` and active `turn_id`; on restart use `read_turn` and `read_status` before sending more work. |\n\n## Controller examples\n\nGeneric MCP controller config:\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\": \"/home/bot/src/project:/home/bot/src/worktrees\",\n \"SKC_COORDINATOR_MCP_MUTATIONS\": \"sessions,questions,reports\",\n \"SKC_COORDINATOR_MCP_PROFILE\": \"controller-prod\",\n \"SKC_COORDINATOR_MCP_REPO\": \"project\",\n \"SKC_COORDINATOR_MCP_SESSION_COMMAND\": \"skc --worktree\"\n },\n \"enabled\": true\n }\n }\n}\n```\n\nExample controller loop:\n\n```text\n1. Start `skc mcp-serve coordinator` with repo/worktree roots allowlisted.\n2. Call `skc_coordinator_start_session` for a SKC-managed worktree session.\n3. Send `/skill:deep-interview`, `/skill:ralplan`, or an approved `skc ultragoal ...` task as one turn.\n4. Await the turn; answer `skc_coordinator_list_questions` entries using bot policy.\n5. Report terminal status with evidence paths.\n6. Read artifacts/reports for the user-facing bot response.\n```\n\nHermes and OpenClaw can use the same MCP tool contract. Their names here are examples of controller products, not privileged integration modes.\n\n## Security and credential boundaries\n\n- Do not put provider API keys, GitHub tokens, or bot secrets in prompts.\n- Prefer host tools, host URI schemes, or bot-side sidecars for credentialed external writes.\n- Keep `SKC_COORDINATOR_MCP_WORKDIR_ROOTS` narrow; do not allow `/`, `/home`, or broad parent directories.\n- Use namespaces for multi-tenant bots.\n- Keep mutation classes minimal: read-only for dashboards, `sessions` for work dispatch, `questions` for answering questions, and `reports` for final state.\n- Treat `.skc/` as local runtime state and evidence. Do not expose it wholesale to untrusted users.\n\n## Related references\n\n- [`docs/hermes-mcp-bridge.md`](./hermes-mcp-bridge.md) — coordinator MCP details and setup adapter behavior.\n- [`docs/rpc.md`](./rpc.md) — JSONL RPC protocol, event frames, workflow gates, host tools, and host URI schemes.\n- [`docs/bridge.md`](./bridge.md) — experimental HTTPS bridge and fail-closed endpoint matrix.\n- [`python/skc-rpc/README.md`](../python/skc-rpc/README.md) — typed Python RPC client examples.\n- [`python/roboskc/README.md`](../python/roboskc/README.md) — example self-hosted GitHub bot using `skc --mode rpc`.\n",
|
|
16
|
+
"bot-integration.md": "# External controller integration guide\n\nThis guide is for authors of bots and orchestrators that want to drive Sayknow-CLI (`skc`) without scraping terminal scrollback. Hermes, OpenClaw, GitHub bots, chatops bots, and custom schedulers are examples of external controllers; none of them need bespoke SKC behavior if they can speak the Coordinator MCP or RPC lifecycle below.\n\nSKC is an external runner. Your controller owns queueing, identity, policy, and credentials; SKC owns the coding-agent session, workflows, tools, artifacts, and evidence inside the selected repository or worktree.\n\n## Integration surfaces\n\nUse the smallest surface that fits your bot:\n\n| Surface | Best for | Command | Stability notes |\n| --- | --- | --- | --- |\n| Coordinator MCP | Any external controller that can call MCP tools to start/register tmux sessions, send turns, answer questions, and read artifacts. | `skc mcp-serve coordinator` | Preferred orchestration surface. `skc mcp-serve hermes` is a compatibility alias, not a separate contract. |\n| Setup adapter | Rendering a portable MCP config and operator instructions for a controller profile. | `skc setup hermes --root /path/to/repo` | Compatibility-oriented config renderer; does not call an LLM or validate provider credentials. |\n| RPC stdio | A controller that embeds a single `skc --mode rpc` subprocess and handles JSONL frames directly or through `python/skc-rpc`. | `skc --mode rpc` | Best for process-backed, single-session bot workers. |\n| Bridge HTTPS | Experimental remote control for an already-running session. | `skc --mode bridge` | Session-control endpoints are fail-closed by default; do not use as the default bot lifecycle surface yet. |\n| Visible tmux fallback | Human-supervised lanes where an existing visible `skc --tmux` pane should become coordinator-authoritative. | `skc --tmux`, then `skc_coordinator_register_session` | Use when an operator already opened a pane or wants direct terminal visibility. |\n\n## Recommended architecture\n\n```text\nexternal controller / bot\n ├─ chooses repo/worktree and task policy\n ├─ starts MCP server: skc mcp-serve coordinator\n ├─ starts or registers one SKC tmux session\n ├─ sends one bounded turn at a time\n ├─ answers structured questions explicitly\n ├─ marks turn completion/failure with report_status\n └─ reads artifacts/reports from allowlisted roots\n```\n\nDo not infer completion from terminal output. Treat durable turn state as authoritative and tmux tail output as advisory debug context only.\n\n## Coordinator MCP setup\n\nRender a non-mutating config preview:\n\n```sh\nskc setup hermes --root /path/to/repo --profile my-bot --repo my-repo\n```\n\nInstall into a Hermes-compatible profile only when the target path is intentional:\n\n```sh\nskc setup hermes \\\n --root /path/to/repo \\\n --profile my-bot \\\n --repo my-repo \\\n --mutation sessions,questions,reports \\\n --profile-dir /path/to/hermes/profile \\\n --install\n```\n\nRun provider-independent contract smokes before trying a live model:\n\n```sh\nskc setup hermes --root /path/to/repo --smoke --json\nskc mcp-serve coordinator --check --json\n```\n\nThe generated config uses these environment variables:\n\n| Variable | Purpose |\n| --- | --- |\n| `SKC_COORDINATOR_MCP_WORKDIR_ROOTS` | Required allowlist for workdirs and artifact paths. |\n| `SKC_COORDINATOR_MCP_MUTATIONS` | Startup opt-in for mutation classes: `sessions`, `questions`, `reports`, or `all`. |\n| `SKC_COORDINATOR_MCP_SESSION_COMMAND` | Command used to start real SKC sessions, defaulting to `skc --worktree` in generated setup. |\n| `SKC_COORDINATOR_MCP_PROFILE` | Optional profile namespace so one bot cannot enumerate another profile's state. |\n| `SKC_COORDINATOR_MCP_REPO` | Optional repo namespace so one repo cannot enumerate another repo's state. |\n| `SKC_COORDINATOR_MCP_STATE_ROOT` | Optional coordination state root; defaults under `.skc/state/coordinator-mcp`. |\n| `SKC_COORDINATOR_MCP_ARTIFACT_BYTE_CAP` | Maximum bytes returned by artifact reads. |\n\nMutating calls require both startup opt-in and per-call `allow_mutation: true`. Missing either one fails closed.\n\n## Generic smoke strategy\n\nUse three different smoke levels so CI does not depend on one operator's model, API key, tmux layout, or desktop:\n\n| Smoke | Required for CI | What it proves | Example |\n| --- | --- | --- | --- |\n| Contract smoke | Yes | MCP server metadata, tool discovery, exported tool names, input schemas, read-only default, and mutation-gate failures. No provider credentials or tmux pane required. | `skc mcp-serve coordinator --check --json` and focused tests around `tools/list` plus mutation denial. |\n| Dry-run lifecycle smoke | Yes when changed behavior affects lifecycle state | A generic controller can start/register a mocked session, send a turn, observe active-turn protection, report terminal status, and read the completed turn without a real LLM. | `bun test packages/coding-agent/test/coordinator-mcp.test.ts` uses mocked coordinator services and temporary state roots. |\n| Optional live smoke | No | One operator's local provider/model/profile/tmux setup can run end-to-end in their chosen repo. Failure diagnoses that setup; it must not fail CI or PR validation. | Start `skc mcp-serve coordinator` with local env, dispatch a tiny task, then report/read evidence. |\n\nA public bot integration change should at least preserve the contract smoke and local-leak docs test. Live smokes are diagnostics, not mandatory gates.\n\n## MCP tool contract\n\nRead-only tools:\n\n- `skc_coordinator_list_sessions`\n- `skc_coordinator_read_status`\n- `skc_coordinator_read_tail`\n- `skc_coordinator_read_turn`\n- `skc_coordinator_await_turn`\n- `skc_coordinator_list_questions`\n- `skc_coordinator_list_artifacts`\n- `skc_coordinator_read_artifact`\n- `skc_coordinator_read_coordination_status`\n- `skc_coordinator_watch_events`\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_coordinator_stop_session`\n\n`skc_coordinator_stop_session` terminates a coordinator-managed session through the owner-proof tmux shutdown path — it verifies the pane pid, native session id, owner generation, server key, and process start time before signaling, so it can never kill a PID-reused or foreign session — and then purges the session's coordinator state. It refuses a session that still has an active turn unless an explicit force is supplied, and it is the same reclamation path the idle reaper uses to reap abandoned ephemeral (delegate-created) sessions.\n\nHigh-level delegation tools:\n\n- `skc_delegate_plan`\n- `skc_delegate_execute`\n- `skc_delegate_team`\n\nThe `skc_delegate_*` tools package common SKC workflows for hosts that want to delegate an entire planning, execution, or team turn without manually composing `start_session` and `send_prompt`. They use the same coordinator mutation gates and workdir allowlists as the lower-level session tools.\n\n### Start a managed SKC session\n\nCall `skc_coordinator_start_session` with a canonical workdir inside `SKC_COORDINATOR_MCP_WORKDIR_ROOTS`:\n\n```json\n{\n \"cwd\": \"/path/to/repo\",\n \"prompt\": \"Optional first bounded task prompt\",\n \"allow_mutation\": true\n}\n```\n\nThe returned payload includes `session.session_id`, `session_state`, and, when a prompt is provided, `turn_id`, `status`, `delivery`, `queued`, and `delivered`.\n\n### Register a visible tmux fallback session\n\nIf an operator already started a visible session, register it instead of starting a hidden coordinator session:\n\n```sh\nskc --tmux\n```\n\n```json\n{\n \"session_id\": \"visible-skc-1\",\n \"cwd\": \"/path/to/repo\",\n \"tmux_session\": \"visible-skc-1\",\n \"tmux_target\": \"visible-skc-1:0.0\",\n \"visible\": true,\n \"source\": \"operator-visible-tmux\",\n \"allow_mutation\": true\n}\n```\n\n`skc_coordinator_register_session` validates safe ids, workdir allowlists, tmux target syntax, and liveness before writing coordinator state.\n\n### Send work as turns\n\nSend one bounded task prompt and persist the returned `turn_id`:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"prompt\": \"Use /skill:ralplan to build a plan for ...\",\n \"allow_mutation\": true\n}\n```\n\nA session may have one active turn by default. A second prompt returns `active_turn_exists` unless the bot passes:\n\n- `queue: true` to enqueue a durable follow-up turn, or\n- `force: true` to supersede the previous active turn and audit the supersession.\n\n### Wait or watch for completion\n\nUse `skc_coordinator_read_turn` for polling or `skc_coordinator_await_turn` for bounded waiting:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"timeout_ms\": 30000,\n \"poll_interval_ms\": 1000,\n \"lines\": 80\n}\n```\n\nTerminal turn statuses are `completed`, `failed`, `cancelled`, and `superseded`. Non-terminal statuses include `queued`, `delivering`, `active`, `waiting_for_answer`, and `completing`.\n\nWhen the work is done, your bot must call `skc_coordinator_report_status` with the turn id. This writes the final response/error, evidence paths, and coordinator report that later reads consume:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"status\": \"completed\",\n \"summary\": \"Implemented the requested fix and ran focused tests.\",\n \"evidence_paths\": [\"/path/to/repo/test-output.txt\"],\n \"allow_mutation\": true\n}\n```\n\nUse `status: \"failed\"` plus `blocker` for provider failures, unrecoverable tool failures, missing credentials, policy denial, or task blockers.\nUse `status: \"cancelled\"` when the coordinator policy intentionally stops tracking an active turn, for example after an operator abort or a bot-side shutdown decision. This records the turn as terminal in coordinator state; it does not kill the underlying tmux process. To supersede one active turn with replacement work, send the replacement prompt with `force: true` and preserve the superseded turn id in your audit trail.\n\n### Forward finish/stop lifecycle notifications\n\nDiscord, Hermes, Clawhip, and similar external notifiers should be opt-in and should forward only the public lifecycle surface. Use one of these supported paths:\n\n- Coordinator controllers: watch or poll turn state with `skc_coordinator_watch_events`, `skc_coordinator_await_turn`, or `skc_coordinator_read_turn`, then notify from the terminal turn status your controller records with `skc_coordinator_report_status`.\n- In-process extensions or hooks: subscribe to the public lifecycle events `turn_end` and `agent_end` from the shared hook/extension event contract.\n\nRecommended notification mapping:\n\n| Notification intent | Public surface | Safe meaning |\n| --- | --- | --- |\n| Turn finished | `turn_end` or terminal coordinator turn status `completed` | One LLM turn produced its final assistant message. |\n| Agent stopped / finished | `agent_end` | The agent loop ended for the submitted prompt. |\n| Waiting for user | Coordinator turn status `waiting_for_answer` | The agent is blocked on a structured question. |\n| Failed or blocked | Coordinator status `failed` with a public `blocker` summary | The controller recorded a terminal failure. |\n| Cancelled / superseded | Coordinator status `cancelled` or `superseded` | The controller intentionally stopped tracking or replaced the turn. |\n\nDo not forward raw prompts, transcripts, tool outputs, hidden instructions, private configs, host paths, channel ids, webhook URLs, or tokens. If your notifier needs a human-readable sentence, create a caller-supplied sanitized summary and keep provider/tool details out of the payload.\n\nExample public-safe extension event payloads:\n\n```json\n{ \"type\": \"turn_end\", \"turnIndex\": 2, \"summary\": \"Turn finished; review the local SKC session for details.\" }\n```\n\n```json\n{ \"type\": \"agent_end\", \"summary\": \"Agent loop ended; no raw transcript is included.\" }\n```\n\nExample opt-in forwarding policy:\n\n```json\n{\n \"enabled\": true,\n \"events\": [\"turn_end\", \"agent_end\"],\n \"destination\": \"external-notifier-profile\",\n \"redaction\": \"metadata-only\"\n}\n```\n\nSKC does not currently expose a structured stop-reason field on `agent_end`; integrators that need `waiting_for_answer`, `failed`, `cancelled`, or `superseded` should prefer the Coordinator MCP turn status because it is explicit, terminal-state oriented, and safe to relay after controller-side redaction.\n\n### Answer structured questions\n\nList pending questions:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"status\": \"pending\"\n}\n```\n\nThen answer by id:\n\n```json\n{\n \"session_id\": \"skc-demo\",\n \"turn_id\": \"turn-00000000-0000-0000-0000-000000000000\",\n \"question_id\": \"question-1\",\n \"answer\": { \"decision\": \"approve\" },\n \"allow_mutation\": true\n}\n```\n\nAlways answer the advertised shape. Do not synthesize approvals for destructive actions unless your bot policy explicitly permits that action.\n\n### Read artifacts and reports\n\nUse `skc_coordinator_list_artifacts` to inspect safe roots and `skc_coordinator_read_artifact` to read a bounded artifact:\n\n```json\n{ \"path\": \"/path/to/repo/.skc/ultragoal/ledger.jsonl\" }\n```\n\nArtifact paths are canonicalized, symlink escapes are rejected, and output is byte-capped. Use `skc_coordinator_read_coordination_status` for status reports written through `skc_coordinator_report_status`.\n\n## RPC stdio integration\n\nUse RPC when your bot owns a single worker subprocess rather than an MCP coordinator. The wire protocol is JSONL over stdio:\n\n```sh\nskc --mode rpc --provider anthropic --model claude-sonnet-4-5\n```\n\nRecommended Python client:\n\n```python\nfrom skc_rpc import RpcClient, WorkflowGate\n\nwith RpcClient(no_session=True, no_rules=True) as client:\n client.install_headless_ui()\n\n def on_gate(gate: WorkflowGate) -> None:\n if gate.kind == \"approval\":\n client.respond_gate(gate.gate_id, {\"decision\": \"approve\"})\n\n client.on_workflow_gate(on_gate)\n turn = client.prompt_and_wait(\"Inspect this repo and report the integration contract.\")\n print(turn.require_assistant_text())\n```\n\nRPC hosts can also expose host-owned tools and URI schemes. Use these to give SKC controlled access to your bot's issue tracker, queue, database rows, or artifact store without leaking long-lived credentials into the SKC process.\n\nKey RPC lifecycle facts:\n\n- `{ \"type\": \"ready\" }` means the subprocess is ready for commands.\n- `prompt` is acknowledged immediately; completion is observed through `agent_end` or `RpcClient.prompt_and_wait()`.\n- `workflow_gate` frames are answered with `workflow_gate_response`.\n- `extension_ui_request` frames are answered with `extension_ui_response` or a headless policy.\n- Host tool calls and host URI requests are explicit callback frames that must be completed or rejected by the host.\n- `RpcClient` enforces single-flight prompt lifecycle collection; use one client per concurrent worker.\n- `abort` and `abort_and_prompt` are the RPC cancellation commands for subprocess workers; coordinator MCP cancellation is recorded through terminal turn status instead.\n\n## Error handling playbook\n\n| Situation | Bot behavior |\n| --- | --- |\n| `coordinator_mutation_class_disabled:*` | Re-render setup with the required mutation class, or keep the bot in read-only mode. |\n| `coordinator_mutation_call_not_allowed:*` | Add `allow_mutation: true` only after policy approval for that specific call. |\n| `unknown_session` | Re-list sessions; start a new managed session or register the visible tmux fallback. |\n| `active_turn_exists` | Poll the active turn, send with `queue: true`, or use `force: true` only when supersession is intentional. |\n| `timeout` from `await_turn` | Treat as non-terminal. Poll again or inspect `read_status`/`read_tail`; do not mark failure solely from a bounded wait timeout. |\n| Coordinator cancellation | Use `skc_coordinator_report_status` with `status: \"cancelled\"` for an intentionally stopped turn, or send replacement work with `force: true` when supersession is policy-approved. This is coordinator state, not a tmux process kill. |\n| Stale tmux/session state | Check `read_status.session_state` and advisory liveness. Register a new visible session or report the turn failed with a recoverable blocker. |\n| Provider/auth failure | Capture the model/provider error in `report_status` with `status: \"failed\"`; do not retry forever without a policy budget. |\n| Artifact denied | Keep the artifact inside allowlisted roots and avoid symlink escapes. |\n| Malformed or invalid question answer | Re-read the question/gate schema and submit a value matching the advertised shape. |\n| Bot shutdown | Persist `session_id` and active `turn_id`; on restart use `read_turn` and `read_status` before sending more work. |\n\n## Controller examples\n\nGeneric MCP controller config:\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\": \"/home/bot/src/project:/home/bot/src/worktrees\",\n \"SKC_COORDINATOR_MCP_MUTATIONS\": \"sessions,questions,reports\",\n \"SKC_COORDINATOR_MCP_PROFILE\": \"controller-prod\",\n \"SKC_COORDINATOR_MCP_REPO\": \"project\",\n \"SKC_COORDINATOR_MCP_SESSION_COMMAND\": \"skc --worktree\"\n },\n \"enabled\": true\n }\n }\n}\n```\n\nExample controller loop:\n\n```text\n1. Start `skc mcp-serve coordinator` with repo/worktree roots allowlisted.\n2. Call `skc_coordinator_start_session` for a SKC-managed worktree session.\n3. Send `/skill:deep-interview`, `/skill:ralplan`, or an approved `skc ultragoal ...` task as one turn.\n4. Await the turn; answer `skc_coordinator_list_questions` entries using bot policy.\n5. Report terminal status with evidence paths.\n6. Read artifacts/reports for the user-facing bot response.\n```\n\nHermes and OpenClaw can use the same MCP tool contract. Their names here are examples of controller products, not privileged integration modes.\n\n## Security and credential boundaries\n\n- Do not put provider API keys, GitHub tokens, or bot secrets in prompts.\n- Prefer host tools, host URI schemes, or bot-side sidecars for credentialed external writes.\n- Keep `SKC_COORDINATOR_MCP_WORKDIR_ROOTS` narrow; do not allow `/`, `/home`, or broad parent directories.\n- Use namespaces for multi-tenant bots.\n- Keep mutation classes minimal: read-only for dashboards, `sessions` for work dispatch, `questions` for answering questions, and `reports` for final state.\n- Treat `.skc/` as local runtime state and evidence. Do not expose it wholesale to untrusted users.\n\n## Related references\n\n- [`docs/hermes-mcp-bridge.md`](./hermes-mcp-bridge.md) — coordinator MCP details and setup adapter behavior.\n- [`docs/rpc.md`](./rpc.md) — JSONL RPC protocol, event frames, workflow gates, host tools, and host URI schemes.\n- [`docs/bridge.md`](./bridge.md) — experimental HTTPS bridge and fail-closed endpoint matrix.\n- [`python/skc-rpc/README.md`](../python/skc-rpc/README.md) — typed Python RPC client examples.\n- [`python/roboskc/README.md`](../python/roboskc/README.md) — example self-hosted GitHub bot using `skc --mode rpc`.\n",
|
|
17
17
|
"brand-assets.md": "# Brand assets\n\nSayknow-CLI's mascot is an **octopus** 🐙. The source artwork is\n[`assets/brand/mascot.png`](../assets/brand/mascot.png) (512×512, transparent);\nall product images are derived from it.\n\n| Asset | Purpose |\n| --- | --- |\n| [`assets/brand/mascot.png`](../assets/brand/mascot.png) | Source mascot (transparent, 512×512). Edit/replace this to rebrand. |\n| [`assets/hero.png`](../assets/hero.png) | Wide README/docs hero — mascot + `sayknow` wordmark on a deep-navy field. |\n| [`assets/character.png`](../assets/character.png) | Standalone mascot (square). |\n| [`python/roboskc/assets/icon.png`](../python/roboskc/assets/icon.png) | App/service icon (square, transparent). |\n| [`python/roboskc/assets/icon.jpg`](../python/roboskc/assets/icon.jpg) | Same, flattened on a navy background for JPEG. |\n| [`assets/tool-image-fixture.webp`](../assets/tool-image-fixture.webp) | Minimal WebP fixture for terminal image rendering tests. Not a product brand asset. |\n\n## Regenerating\n\nDerived images are composited from `assets/brand/mascot.png` with ImageMagick:\n\n```sh\nSRC=assets/brand/mascot.png\nmagick \"$SRC\" -resize 800x800 assets/character.png\ncp \"$SRC\" python/roboskc/assets/icon.png\nmagick \"$SRC\" -background '#06182b' -flatten -resize 512x512 python/roboskc/assets/icon.jpg\nmagick -size 1200x630 gradient:'#0c2a47'-'#020812' \\\n \\( \"$SRC\" -resize 300x300 \\) -gravity North -geometry +0+72 -composite \\\n -gravity North -font \"/System/Library/Fonts/Supplemental/Arial Bold.ttf\" \\\n -fill '#eef7ff' -pointsize 104 -annotate +0+392 'sayknow' \\\n -fill '#ff8a4c' -pointsize 27 -annotate +0+520 'Coding should feel like thinking.' \\\n assets/hero.png\n```\n\n> Note: the mascot is warm/orange while the bundled TUI theme is `blue-octopus`\n> (blue). They coexist intentionally; if you want the terminal palette to match\n> the mascot, switch the default theme to `red-octopus` or add a warm theme.\n",
|
|
18
|
-
"bridge.md": "# Bridge Protocol Reference (Experimental, Fail-Closed)\n\nBridge mode runs the coding agent as an experimental network control surface over\nHTTPS. The session-control surface is intentionally **fail-closed by default**\nwhile the bridge security model is hardened.\n\nDefault availability:\n\n- `GET /healthz` is available without auth and returns `{ \"status\": \"ok\" }`.\n- `GET /v1/help` is available without auth and reports the fail-closed endpoint\n matrix.\n- `POST /v1/handshake` remains authenticated, but the default response advertises\n no enabled session endpoints, no accepted capabilities, no accepted scopes, and\n no frame types.\n- `GET /v1/sessions/{session_id}/events` fails closed with\n `403 endpoint_disabled` after bearer auth succeeds.\n- `POST /v1/sessions/{session_id}/commands` fails closed with\n `403 endpoint_disabled` after bearer auth succeeds and before body parsing,\n command validation, scope checks, or dispatch.\n- `POST /v1/sessions/{session_id}/control:claim` and\n `POST /v1/sessions/{session_id}/control:disconnect` fail closed with\n `403 endpoint_disabled` after bearer auth succeeds.\n- `POST /v1/sessions/{session_id}/ui-responses/{correlation_id}` fails closed\n with `403 endpoint_disabled` after bearer auth succeeds and before body parsing\n or controller checks.\n- `POST /v1/sessions/{session_id}/host-tool-results/{correlation_id}` and\n `POST /v1/sessions/{session_id}/host-uri-results/{correlation_id}` fail closed\n with `403 endpoint_disabled` after bearer auth succeeds and before body parsing\n or host callback handling.\n\nThe implementation still contains the v1 protocol scaffolding and internal tests\nfor the previously enabled surface, but external clients must treat events,\ncommands, controller ownership, UI responses, host tool results, and host URI\nresults as unavailable unless a future release explicitly re-enables them.\n\nPrimary implementation:\n\n- `src/modes/bridge/bridge-mode.ts`\n- `src/modes/bridge/auth.ts`\n- `src/modes/bridge/event-stream.ts`\n- `src/modes/bridge/bridge-client-bridge.ts`\n- `src/modes/bridge/bridge-ui-context.ts`\n- `src/modes/shared/agent-wire/*` (protocol, scopes, handshake, command dispatch/validation, host bridges)\n- `packages/bridge-client/src/*`\n\n## Startup\n\n```bash\nskc --mode bridge [regular CLI options]\n```\n\nBehavior notes:\n\n- The bridge is served over **HTTPS only**. Startup refuses to bind without TLS\n configured (see Security and TLS). There is no unencrypted startup path.\n- `@file` CLI arguments are rejected in bridge mode (as in RPC mode).\n- Bridge mode reuses the RPC default-setting overrides and suppresses automatic\n session title generation.\n- One bridge process serves exactly **one live `AgentSession`**.\n- The default endpoint matrix disables session events, commands, controller\n ownership, UI responses, host tool results, and host URI results.\n\n### Configuration (environment variables)\n\nSee `docs/environment-variables.md` for the authoritative table. Summary:\n\n| Variable | Required | Default | Notes |\n| --- | --- | --- | --- |\n| `SKC_BRIDGE_TOKEN` | Yes | — | Bearer token for authenticated endpoints. **Secret — never commit.** |\n| `SKC_BRIDGE_TLS_CERT` | Yes | — | Path to the TLS certificate (PEM). |\n| `SKC_BRIDGE_TLS_KEY` | Yes | — | Path to the TLS private key (PEM). **Secret — never commit.** |\n| `SKC_BRIDGE_HOST` | No | `127.0.0.1` | Bind hostname. |\n| `SKC_BRIDGE_PORT` | No | `4077` | Bind port (1–65535). |\n| `SKC_BRIDGE_SCOPES` | No | `prompt` | Parsed for internal compatibility, but default session endpoints are fail-closed. |\n\n## Security and TLS\n\nThe bridge is a network control surface, so it is **secure-by-default**:\n\n- **TLS is mandatory for every bind, including loopback.** Startup fails closed\n with a clear error if `SKC_BRIDGE_TLS_CERT` and `SKC_BRIDGE_TLS_KEY` are not\n both set. There is no plaintext fallback and no insecure/trust-bypass switch.\n- **Bearer token is mandatory** for every endpoint except `GET /healthz` and\n `GET /v1/help`.\n- The TypeScript SDK refuses bearer-token clients over non-`https` URLs by\n default. It allows plaintext only for `localhost`, `127.0.0.1`, or `[::1]`\n when the caller explicitly passes the localhost/test opt-in.\n- Session endpoints fail closed by default even when bearer auth and scopes are\n otherwise valid.\n\n## Handshake\n\n```\nPOST /v1/handshake (authenticated)\n```\n\nThe client sends its supported protocol version range, requested capabilities,\nand requested scopes. Version mismatch returns `status: \"rejected\"`,\n`reason: \"incompatible_version\"`. Malformed request bodies return\n`400 invalid_request`.\n\nIn the default fail-closed configuration, a successful authenticated\nhandshake returns:\n\n- `protocol_version` — the server protocol version (`BRIDGE_PROTOCOL_VERSION`, `2`).\n- `session_id` — the single session id this bridge serves.\n- `accepted_capabilities` — empty.\n- `accepted_scopes` — empty.\n- `unsupported` — every requested capability.\n- `endpoints` — all session endpoint descriptors present but empty strings.\n- `frame_types` — empty.\n\n## Fail-Closed Endpoint Matrix\n\nThe disabled endpoint matrix is:\n\n| Surface | Endpoint(s) | Default |\n| --- | --- | --- |\n| Events | `GET /v1/sessions/{session_id}/events?last_seq=<n>` | Disabled |\n| Commands | `POST /v1/sessions/{session_id}/commands` | Disabled |\n| Control | `POST /v1/sessions/{session_id}/control:claim`, `POST /v1/sessions/{session_id}/control:disconnect` | Disabled |\n| UI responses | `POST /v1/sessions/{session_id}/ui-responses/{correlation_id}` | Disabled |\n| Host tool results | `POST /v1/sessions/{session_id}/host-tool-results/{correlation_id}` | Disabled |\n| Host URI results | `POST /v1/sessions/{session_id}/host-uri-results/{correlation_id}` | Disabled |\n\nAuthenticated requests to disabled endpoints return:\n\n```json\n{ \"error\": \"endpoint_disabled\", \"endpoint\": \"commands\" }\n```\n\nThe `endpoint` value is one of `events`, `commands`, `control`, `uiResponses`,\n`hostToolResults`, or `hostUriResults`.\n\n## Protocol Catalog Kept for Internal Compatibility\n\nThe bridge protocol module still defines the v1 command and scope catalog so\nexisting internal tests can validate the dormant implementation and future\nre-enable work has a stable baseline.\n\nWhen internally enabled for compatibility tests, event replay still uses `last_seq` and the bounded replay reset marker `replay_window_exceeded`; command and UI response retries still use `Idempotency-Key`. These mechanisms are dormant for default external bridge clients because the endpoint matrix rejects the endpoints before they reach replay, body parsing, idempotency, scope, or dispatch logic.\n\nWorkflow-gate responses are part of the UI-response surface, not the dormant command surface: when internally enabled, an answerer responds to `workflow_gate` frame `wg_...` by posting `{ \"gate_id\": \"wg_...\", \"answer\": ... }` to `POST /v1/sessions/{session_id}/ui-responses/{gate_id}`. Gate answers are authorized by bearer auth, the `control` scope on this (default-disabled) endpoint, and the currently claimed controller owner token. `X-SKC-Bridge-Owner-Token` must match the claimed controller token; mismatches return `403 not_controller` and do not resolve the gate. `Idempotency-Key` is optional and is also forwarded as `idempotency_key` when supplied by SDK helpers.\n\n### Scopes\n\nThe configurable scope set (`BRIDGE_COMMAND_SCOPES`) is:\n\n- `prompt`\n- `control`\n- `bash`\n- `export`\n- `session`\n- `model`\n- `message:read`\n- `host_tools`\n- `host_uri`\n- `admin`\n\nThe mandatory compliance floor (`MANDATORY_FLOOR_COMMAND_SCOPES`) remains\n`prompt` for the dormant command surface. Because commands are disabled by the\nendpoint matrix, the default handshake advertises no accepted scopes.\n\n### Command catalog and scope mapping\n\n| Command | Scope |\n| --- | --- |\n| `prompt` | `prompt` |\n| `steer` | `prompt` |\n| `follow_up` | `prompt` |\n| `abort` | `prompt` |\n| `abort_and_prompt` | `prompt` |\n| `new_session` | `session` |\n| `get_state` | `message:read` |\n| `set_todos` | `control` |\n| `set_host_tools` | `host_tools` |\n| `set_host_uri_schemes` | `host_uri` |\n| `get_pending_workflow_gates` | `message:read` |\n| `set_capabilities` | `control` |\n| `set_model` | `model` |\n| `cycle_model` | `model` |\n| `get_available_models` | `model` |\n| `set_thinking_level` | `model` |\n| `cycle_thinking_level` | `model` |\n| `set_steering_mode` | `control` |\n| `set_follow_up_mode` | `control` |\n| `set_interrupt_mode` | `control` |\n| `compact` | `control` |\n| `set_auto_compaction` | `control` |\n| `set_auto_retry` | `control` |\n| `abort_retry` | `control` |\n| `bash` | `bash` |\n| `abort_bash` | `bash` |\n| `get_session_stats` | `message:read` |\n| `export_html` | `export` |\n| `switch_session` | `session` |\n| `branch` | `session` |\n| `get_branch_messages` | `session` |\n| `get_last_assistant_text` | `message:read` |\n| `set_session_name` | `session` |\n| `handoff` | `admin` |\n| `get_messages` | `message:read` |\n| `get_login_providers` | `admin` |\n| `login` | `admin` |\n| `negotiate_unattended` | `control` |\n| `workflow_gate_response` | `control` |\n\n### Dormant capabilities and frame types\n\nThese names remain in the protocol code for future compatibility and internal\nconformance tests, but they are not advertised by the default fail-closed\nhandshake:\n\nCapabilities: `events`, `prompt`, `permission`, `elicitation`, `ui.declarative`,\n`host_tools`, `host_uri`, `workflow_gate`.\n\nFrame types: `ready`, `event`, `response`, `ui_request`, `permission_request`,\n`host_tool_call`, `host_uri_request`, `reset`, `workflow_gate`, `error`.\n\n## UI Capability Parity\n\nBridge UI parity remains **semantic, not pixel-perfect** when the dormant UI\nsurface is explicitly enabled for internal validation. Local-only UI capabilities\ncontinue to report typed unsupported results instead of silent defaults:\n\n- `ui.terminal_input`\n- `ui.widget.component`\n- `ui.footer.component`\n- `ui.header.component`\n- `ui.custom.component`\n- `ui.editor.get_text`\n- `ui.editor.component`\n- `ui.tools_expanded`\n- Theme switching is unsupported (`setTheme` returns `{ success: false }`).\n\n## SDK Usage\n\n`@sayknow-cli/bridge-client` exposes `BridgeClient` with handshake, command\nhelpers mirroring the full RPC command catalog, an `events()` async generator,\ncontroller/UI/host-callback helpers, and an idempotency-key helper. The bridge\nsession-control surface remains fail-closed by default, so against an\nunconfigured bridge those helpers should be expected to fail because the server\nendpoint matrix disables the corresponding session endpoints until they are\nexplicitly enabled.\n\n`BridgeClient.respondGate(sessionId, gateId, ownerToken, answer, options)` posts to the fail-closed UI-response endpoint and returns the gate resolution envelope emitted by the bridge. It deliberately does not send `workflow_gate_response` through `/commands`. Gate answers are authorized by bearer auth, the `control` scope on the (by-default-disabled) `ui-responses` endpoint, and the current controller owner token; unauthorized owner-token attempts return `403 not_controller` without resolving the gate.\n\n> Response typing: in this experimental version, `command()` and the typed\n> command helpers return `Promise<unknown>`. Callers narrow the response\n> themselves. Importing `@sayknow-cli/coding-agent` internal `rpc-types` into the\n> SDK is intentionally avoided to preserve the package boundary; stable shared\n> protocol response types are tracked as follow-up work.\n\n## Limitations\n\n- **Single session per process.** A bridge process serves exactly one live\n `AgentSession`. The `session_id` is present in every frame and endpoint for\n ordering and future additive multiplexing, but multi-session multiplexing is\n **not** implemented in v1.\n- Session events, commands, controller ownership, UI responses, host tool\n results, and host URI results are disabled by default.\n- Coarse per-token scopes only (no fine-grained per-command policy yet).\n- UI parity is semantic, not pixel-perfect (see UI Capability Parity).\n\n## Hermes/Claw orchestration layering\n\nFor Hermes/Claw-style orchestration, treat `skc` as an external runner. The orchestration agent should choose or create the repository checkout first, preferably a dedicated Git worktree for branch-local work, then launch or attach a leader session with `skc --tmux` from that directory. SKC is not embedded runtime injection into Hermes, Claw Code, or another coding tool.\n\nPublic orchestration boundaries:\n\n1. Choose the repo/worktree and branch that will own changes, logs, and review evidence.\n2. Start or attach the SKC leader with `skc --tmux` from that directory. If you want SKC to create the sibling worktree, use `skc --tmux --worktree <branch-like-name>`; the argument is a worktree/branch name, not a filesystem path.\n3. Submit the workflow appropriate to the task: `/skill:deep-interview` for requirements discovery, `/skill:ralplan` for plan consensus, and `skc ultragoal ...` for durable goal tracking through execution and verification.\n4. Use `skc team ...` only when coordinated parallel tmux workers help with implementation or verification; single-lane work should stay in the leader session.\n5. Collect the handoff state: whether the session stopped cleanly, changed files, commands/checks run, failures, unresolved risks, and evidence summaries.\n\nBridge mode remains the public remote-control protocol for an already-running SKC session, but the session-control endpoints are fail-closed by default. Keep lifecycle, worktree selection, and evidence policy above the bridge frames, and avoid documenting private deployment, routing, or credential internals. Introducing another authenticated remote-control protocol for the same purpose should require ADR-level rationale.\n\nThe same external-runner workflow is summarized in the README section [Using SKC with other coding agents](../README.md#using-skc-with-other-coding-agents).\n",
|
|
18
|
+
"bridge.md": "# Bridge Protocol Reference (Experimental, Fail-Closed)\n\nBridge mode runs the coding agent as an experimental network control surface over\nHTTPS. The session-control surface is intentionally **fail-closed by default**\nwhile the bridge security model is hardened.\n\nDefault availability:\n\n- `GET /healthz` is available without auth and returns `{ \"status\": \"ok\" }`.\n- `GET /v1/help` is available without auth and reports the fail-closed endpoint\n matrix.\n- `POST /v1/handshake` remains authenticated, but the default response advertises\n no enabled session endpoints, no accepted capabilities, no accepted scopes, and\n no frame types.\n- `GET /v1/sessions/{session_id}/events` fails closed with\n `403 endpoint_disabled` after bearer auth succeeds.\n- `POST /v1/sessions/{session_id}/commands` fails closed with\n `403 endpoint_disabled` after bearer auth succeeds and before body parsing,\n command validation, scope checks, or dispatch.\n- `POST /v1/sessions/{session_id}/control:claim` and\n `POST /v1/sessions/{session_id}/control:disconnect` fail closed with\n `403 endpoint_disabled` after bearer auth succeeds.\n- `POST /v1/sessions/{session_id}/ui-responses/{correlation_id}` fails closed\n with `403 endpoint_disabled` after bearer auth succeeds and before body parsing\n or controller checks.\n- `POST /v1/sessions/{session_id}/host-tool-results/{correlation_id}` and\n `POST /v1/sessions/{session_id}/host-uri-results/{correlation_id}` fail closed\n with `403 endpoint_disabled` after bearer auth succeeds and before body parsing\n or host callback handling.\n\nThe implementation still contains the v1 protocol scaffolding and internal tests\nfor the previously enabled surface, but external clients must treat events,\ncommands, controller ownership, UI responses, host tool results, and host URI\nresults as unavailable unless a future release explicitly re-enables them.\n\nPrimary implementation:\n\n- `src/modes/bridge/bridge-mode.ts`\n- `src/modes/bridge/auth.ts`\n- `src/modes/bridge/event-stream.ts`\n- `src/modes/bridge/bridge-client-bridge.ts`\n- `src/modes/bridge/bridge-ui-context.ts`\n- `src/modes/shared/agent-wire/*` (protocol, scopes, handshake, command dispatch/validation, host bridges)\n- `packages/bridge-client/src/*`\n\n## Startup\n\n```bash\nskc --mode bridge [regular CLI options]\n```\n\nBehavior notes:\n\n- The bridge is served over **HTTPS only**. Startup refuses to bind without TLS\n configured (see Security and TLS). There is no unencrypted startup path.\n- `@file` CLI arguments are rejected in bridge mode (as in RPC mode).\n- Bridge mode reuses the RPC default-setting overrides and suppresses automatic\n session title generation.\n- One bridge process serves exactly **one live `AgentSession`**.\n- The default endpoint matrix disables session events, commands, controller\n ownership, UI responses, host tool results, and host URI results.\n\n### Configuration (environment variables)\n\nSee `docs/environment-variables.md` for the authoritative table. Summary:\n\n| Variable | Required | Default | Notes |\n| --- | --- | --- | --- |\n| `SKC_BRIDGE_TOKEN` | Yes | — | Bearer token for authenticated endpoints. **Secret — never commit.** |\n| `SKC_BRIDGE_TLS_CERT` | Yes | — | Path to the TLS certificate (PEM). |\n| `SKC_BRIDGE_TLS_KEY` | Yes | — | Path to the TLS private key (PEM). **Secret — never commit.** |\n| `SKC_BRIDGE_HOST` | No | `127.0.0.1` | Bind hostname. |\n| `SKC_BRIDGE_PORT` | No | `4077` | Bind port (1–65535). |\n| `SKC_BRIDGE_SCOPES` | No | `prompt` | Parsed for internal compatibility, but default session endpoints are fail-closed. |\n\n## Security and TLS\n\nThe bridge is a network control surface, so it is **secure-by-default**:\n\n- **TLS is mandatory for every bind, including loopback.** Startup fails closed\n with a clear error if `SKC_BRIDGE_TLS_CERT` and `SKC_BRIDGE_TLS_KEY` are not\n both set. There is no plaintext fallback and no insecure/trust-bypass switch.\n- **Bearer token is mandatory** for every endpoint except `GET /healthz` and\n `GET /v1/help`.\n- The TypeScript SDK refuses bearer-token clients over non-`https` URLs by\n default. It allows plaintext only for `localhost`, `127.0.0.1`, or `[::1]`\n when the caller explicitly passes the localhost/test opt-in.\n- Session endpoints fail closed by default even when bearer auth and scopes are\n otherwise valid.\n\n## Handshake\n\n```\nPOST /v1/handshake (authenticated)\n```\n\nThe client sends its supported protocol version range, requested capabilities,\nand requested scopes. Version mismatch returns `status: \"rejected\"`,\n`reason: \"incompatible_version\"`. Malformed request bodies return\n`400 invalid_request`.\n\nIn the default fail-closed configuration, a successful authenticated\nhandshake returns:\n\n- `protocol_version` — the server protocol version (`BRIDGE_PROTOCOL_VERSION`, `2`).\n- `session_id` — the single session id this bridge serves.\n- `accepted_capabilities` — empty.\n- `accepted_scopes` — empty.\n- `unsupported` — every requested capability.\n- `endpoints` — all session endpoint descriptors present but empty strings.\n- `frame_types` — empty.\n\n## Fail-Closed Endpoint Matrix\n\nThe disabled endpoint matrix is:\n\n| Surface | Endpoint(s) | Default |\n| --- | --- | --- |\n| Events | `GET /v1/sessions/{session_id}/events?last_seq=<n>` | Disabled |\n| Commands | `POST /v1/sessions/{session_id}/commands` | Disabled |\n| Control | `POST /v1/sessions/{session_id}/control:claim`, `POST /v1/sessions/{session_id}/control:disconnect` | Disabled |\n| UI responses | `POST /v1/sessions/{session_id}/ui-responses/{correlation_id}` | Disabled |\n| Host tool results | `POST /v1/sessions/{session_id}/host-tool-results/{correlation_id}` | Disabled |\n| Host URI results | `POST /v1/sessions/{session_id}/host-uri-results/{correlation_id}` | Disabled |\n\nAuthenticated requests to disabled endpoints return:\n\n```json\n{ \"error\": \"endpoint_disabled\", \"endpoint\": \"commands\" }\n```\n\nThe `endpoint` value is one of `events`, `commands`, `control`, `uiResponses`,\n`hostToolResults`, or `hostUriResults`.\n\n## Protocol Catalog Kept for Internal Compatibility\n\nThe bridge protocol module still defines the v1 command and scope catalog so\nexisting internal tests can validate the dormant implementation and future\nre-enable work has a stable baseline.\n\nWhen internally enabled for compatibility tests, event replay still uses `last_seq` and the bounded replay reset marker `replay_window_exceeded`; command and UI response retries still use `Idempotency-Key`. These mechanisms are dormant for default external bridge clients because the endpoint matrix rejects the endpoints before they reach replay, body parsing, idempotency, scope, or dispatch logic.\n\nWorkflow-gate responses are part of the UI-response surface, not the dormant command surface: when internally enabled, an answerer responds to `workflow_gate` frame `wg_...` by posting `{ \"gate_id\": \"wg_...\", \"answer\": ... }` to `POST /v1/sessions/{session_id}/ui-responses/{gate_id}`. Gate answers are authorized by bearer auth, the `control` scope on this (default-disabled) endpoint, and the currently claimed controller owner token. `X-SKC-Bridge-Owner-Token` must match the claimed controller token; mismatches return `403 not_controller` and do not resolve the gate. `Idempotency-Key` is optional and is also forwarded as `idempotency_key` when supplied by SDK helpers.\n\n### Scopes\n\nThe configurable scope set (`BRIDGE_COMMAND_SCOPES`) is:\n\n- `prompt`\n- `control`\n- `bash`\n- `export`\n- `session`\n- `model`\n- `message:read`\n- `host_tools`\n- `host_uri`\n- `admin`\n\nThe mandatory compliance floor (`MANDATORY_FLOOR_COMMAND_SCOPES`) remains\n`prompt` for the dormant command surface. Because commands are disabled by the\nendpoint matrix, the default handshake advertises no accepted scopes.\n\n### Command catalog and scope mapping\n\n| Command | Scope |\n| --- | --- |\n| `prompt` | `prompt` |\n| `steer` | `prompt` |\n| `follow_up` | `prompt` |\n| `abort` | `prompt` |\n| `abort_and_prompt` | `prompt` |\n| `new_session` | `session` |\n| `get_state` | `message:read` |\n| `set_todos` | `control` |\n| `set_host_tools` | `host_tools` |\n| `set_host_uri_schemes` | `host_uri` |\n| `get_pending_workflow_gates` | `message:read` |\n| `set_capabilities` | `control` |\n| `set_model` | `model` |\n| `set_default_model_selection` | `model` |\n| `cycle_model` | `model` |\n| `get_available_models` | `model` |\n| `set_thinking_level` | `model` |\n| `cycle_thinking_level` | `model` |\n| `set_steering_mode` | `control` |\n| `set_follow_up_mode` | `control` |\n| `set_interrupt_mode` | `control` |\n| `compact` | `control` |\n| `set_auto_compaction` | `control` |\n| `set_auto_retry` | `control` |\n| `abort_retry` | `control` |\n| `bash` | `bash` |\n| `abort_bash` | `bash` |\n| `get_session_stats` | `message:read` |\n| `export_html` | `export` |\n| `switch_session` | `session` |\n| `branch` | `session` |\n| `get_branch_messages` | `session` |\n| `get_last_assistant_text` | `message:read` |\n| `set_session_name` | `session` |\n| `handoff` | `admin` |\n| `get_messages` | `message:read` |\n| `get_login_providers` | `admin` |\n| `login` | `admin` |\n| `negotiate_unattended` | `control` |\n| `workflow_gate_response` | `control` |\n\n### Dormant capabilities and frame types\n\nThese names remain in the protocol code for future compatibility and internal\nconformance tests, but they are not advertised by the default fail-closed\nhandshake:\n\nCapabilities: `events`, `prompt`, `permission`, `elicitation`, `ui.declarative`,\n`host_tools`, `host_uri`, `workflow_gate`.\n\nFrame types: `ready`, `event`, `response`, `ui_request`, `permission_request`,\n`host_tool_call`, `host_uri_request`, `reset`, `workflow_gate`, `error`.\n\n## UI Capability Parity\n\nBridge UI parity remains **semantic, not pixel-perfect** when the dormant UI\nsurface is explicitly enabled for internal validation. Local-only UI capabilities\ncontinue to report typed unsupported results instead of silent defaults:\n\n- `ui.terminal_input`\n- `ui.widget.component`\n- `ui.footer.component`\n- `ui.header.component`\n- `ui.custom.component`\n- `ui.editor.get_text`\n- `ui.editor.component`\n- `ui.tools_expanded`\n- Theme switching is unsupported (`setTheme` returns `{ success: false }`).\n\n## SDK Usage\n\n`@sayknow-cli/bridge-client` exposes `BridgeClient` with handshake, command\nhelpers mirroring the full RPC command catalog, an `events()` async generator,\ncontroller/UI/host-callback helpers, and an idempotency-key helper. The bridge\nsession-control surface remains fail-closed by default, so against an\nunconfigured bridge those helpers should be expected to fail because the server\nendpoint matrix disables the corresponding session endpoints until they are\nexplicitly enabled.\n\n`BridgeClient.respondGate(sessionId, gateId, ownerToken, answer, options)` posts to the fail-closed UI-response endpoint and returns the gate resolution envelope emitted by the bridge. It deliberately does not send `workflow_gate_response` through `/commands`. Gate answers are authorized by bearer auth, the `control` scope on the (by-default-disabled) `ui-responses` endpoint, and the current controller owner token; unauthorized owner-token attempts return `403 not_controller` without resolving the gate.\n\n> Response typing: in this experimental version, `command()` and the typed\n> command helpers return `Promise<unknown>`. Callers narrow the response\n> themselves. Importing `@sayknow-cli/coding-agent` internal `rpc-types` into the\n> SDK is intentionally avoided to preserve the package boundary; stable shared\n> protocol response types are tracked as follow-up work.\n\n## Limitations\n\n- **Single session per process.** A bridge process serves exactly one live\n `AgentSession`. The `session_id` is present in every frame and endpoint for\n ordering and future additive multiplexing, but multi-session multiplexing is\n **not** implemented in v1.\n- Session events, commands, controller ownership, UI responses, host tool\n results, and host URI results are disabled by default.\n- Coarse per-token scopes only (no fine-grained per-command policy yet).\n- UI parity is semantic, not pixel-perfect (see UI Capability Parity).\n\n## Hermes/Claw orchestration layering\n\nFor Hermes/Claw-style orchestration, treat `skc` as an external runner. The orchestration agent should choose or create the repository checkout first, preferably a dedicated Git worktree for branch-local work, then launch or attach a leader session with `skc --tmux` from that directory. SKC is not embedded runtime injection into Hermes, Claw Code, or another coding tool.\n\nPublic orchestration boundaries:\n\n1. Choose the repo/worktree and branch that will own changes, logs, and review evidence.\n2. Start or attach the SKC leader with `skc --tmux` from that directory. If you want SKC to create the sibling worktree, use `skc --tmux --worktree <branch-like-name>`; the argument is a worktree/branch name, not a filesystem path.\n3. Submit the workflow appropriate to the task: `/skill:deep-interview` for requirements discovery, `/skill:ralplan` for plan consensus, and `skc ultragoal ...` for durable goal tracking through execution and verification.\n4. Use `skc team ...` only when coordinated parallel tmux workers help with implementation or verification; single-lane work should stay in the leader session.\n5. Collect the handoff state: whether the session stopped cleanly, changed files, commands/checks run, failures, unresolved risks, and evidence summaries.\n\nBridge mode remains the public remote-control protocol for an already-running SKC session, but the session-control endpoints are fail-closed by default. Keep lifecycle, worktree selection, and evidence policy above the bridge frames, and avoid documenting private deployment, routing, or credential internals. Introducing another authenticated remote-control protocol for the same purpose should require ADR-level rationale.\n\nThe same external-runner workflow is summarized in the README section [Using SKC with other coding agents](../README.md#using-skc-with-other-coding-agents).\n",
|
|
19
19
|
"codebase-overview.md": "# Codebase Overview\n\nThis document maps the main parts of the `sayknow-cli` repository. The root README stays intentionally small; this file is the architecture-oriented companion.\n\n## Product shape\n\nSayknow-CLI (`skc`) is centered on `packages/coding-agent/`. The public workflow surface is intentionally fixed at four source-bundled skills and four public role subagents. Runtime state, specs, plans, goals, team state, and local overrides live under `.skc/`.\n\nDefault workflow skills are embedded from:\n\n```text\npackages/coding-agent/src/defaults/skc/skills/<name>/SKILL.md\n```\n\nPublic role subagent prompts are embedded from:\n\n```text\npackages/coding-agent/src/prompts/agents/<role>.md\n```\n\nThe runtime can still discover project/user overrides, but the bundled defaults are loaded from source so a missing project `.skc` directory does not remove the default workflow surface.\n\n## Packages\n\n### `packages/coding-agent/`\n\nMain `skc` CLI and product runtime.\n\n- `packages/coding-agent/package.json` exposes the `skc` binary at `src/cli.ts` and the SDK/barrel entrypoint at `src/index.ts`.\n- `packages/coding-agent/src/cli.ts` is the executable bootstrap. It registers CLI commands such as `setup`, `deep-interview`, `ralplan`, `ultragoal`, `team`, and the default launch path.\n- `packages/coding-agent/src/main.ts` adapts CLI options into session creation and dispatches interactive, print, RPC, RPC-UI, ACP, and Bridge modes.\n- `packages/coding-agent/src/sdk.ts` assembles settings, model registry, auth, workspace/context discovery, skills, rules, tools, system prompt, and the underlying `@sayknow-cli/agent-core` agent.\n- `packages/coding-agent/src/tools/index.ts` is the built-in tool registry for file/code/runtime tools such as read, bash, edit, AST tools, eval, find/search, LSP, browser, task/subagent, recipe, IRC, todo, web search, and write. Memory backends are private integrations, not public coding-harness tools.\n- `packages/coding-agent/src/defaults/skc-defaults.ts` embeds and installs the default workflow skills.\n- `packages/coding-agent/src/task/agents.ts` embeds bundled task-agent prompts. The public contract is `executor`, `architect`, `planner`, and `critic`; other bundled prompts are internal/runtime utilities.\n- `packages/coding-agent/src/coordinator/contract.ts` defines the transport-neutral third-party coordinator contract used by `skc mcp-serve coordinator`, `skc coordinator`, and `skc setup hermes`.\n- `packages/coding-agent/src/coordinator-mcp/server.ts` implements the outward MCP adapter for bot/coordinator integrations, including session start/register, turn state, question answering, status reports, and artifact reads.\n- `docs/external-control-readiness.md` classifies the public external-control surfaces: Coordinator MCP for multi-session control planes, RPC stdio for subprocess workers, ACP for editor/ACP clients, and Bridge HTTPS as experimental/fail-closed protocol scaffolding.\n\n### `packages/ai/`\n\nProvider/model boundary for LLM access.\n\n- `packages/ai/src/index.ts` exports model registry/resolution, provider implementations, auth broker/gateway/storage, streaming, usage, retry/overflow utilities, OAuth, discovery, and validation helpers.\n- `packages/ai/src/types.ts` defines provider, model, context, message, tool, usage, reasoning, and stream-event contracts.\n- `packages/ai/src/stream.ts` dispatches model-driven streams to the right provider/API implementation and normalizes streaming events.\n- `packages/ai/src/model-manager.ts` merges static, cached, dynamic, and remote model sources.\n- `packages/ai/README.md` documents tool calling, partial streaming tool calls, thinking/reasoning, provider configuration, context handoff, and OAuth flows.\n\n### `packages/agent/`\n\nStateful agent runtime built on `@sayknow-cli/ai`.\n\n- `packages/agent/src/index.ts` exports the `Agent`, loop APIs, append-only context, compaction, telemetry, proxy utilities, thinking helpers, and shared types.\n- `packages/agent/src/agent-loop.ts` owns the turn loop: transform context, call the model stream, execute tool calls, append tool results, and emit lifecycle events.\n- `packages/agent/src/agent.ts` wraps the loop with mutable state, subscriptions, prompt/continue/abort APIs, queues, provider session state, telemetry, and state mutation helpers.\n- `packages/agent/src/types.ts` defines `AgentMessage`, `AgentTool`, loop config, event, and runtime state contracts.\n\n### `packages/tui/`\n\nTerminal UI framework used by the CLI.\n\n- `packages/tui/src/index.ts` exports components, keybindings, autocomplete, terminal abstractions, image support, TUI core, and utilities.\n- `packages/tui/src/tui.ts` manages component rendering, focus, overlays, terminal dimensions, diff state, and synchronized output.\n- `packages/tui/src/terminal.ts` abstracts terminal lifecycle, dimensions, cursor controls, title/progress, Kitty protocol state, and appearance notifications.\n- `packages/tui/README.md` documents the component model and built-in components such as text, input, editor, markdown, loaders, select/settings lists, spacer, image, box, and container.\n\n### `packages/natives/` and Rust crates\n\nNative helper layer exposed through N-API.\n\n- `packages/natives/package.json` exports `native/index.js` and generated TypeScript definitions.\n- `packages/natives/native/loader-state.js` resolves platform/CPU-specific native binaries and validates package/native version alignment.\n- `crates/pi-natives/src/lib.rs` is the N-API root for appearance, AST search/editing, clipboard, filesystem scan/cache, grep/glob, syntax highlighting, HTML-to-Markdown, keyboard parsing, process/PTY/shell support, SIXEL, code summarization, token counting, text measurement/wrapping/truncation, workspace scanning, power assertions, and isolation helpers.\n- `crates/pi-shell/src/lib.rs` exposes brush-based shell execution primitives used by the native shell adapter.\n- `crates/pi-shell/src/shell.rs` implements persistent and one-shot shell execution, streaming, environment handling, cancellation, and output minimizer telemetry.\n- `crates/pi-shell/src/fixup.rs` performs conservative AST-based bash command fixups.\n- `crates/pi-natives/src/pty.rs` implements interactive PTY sessions.\n\n### `packages/utils/`\n\nShared TypeScript utilities.\n\n- `packages/utils/src/index.ts` exports abortable/async helpers, color/env/dir utilities, fetch retry, formatting, frontmatter, glob helpers, JSON helpers, logging, MIME detection, prompt rendering, process-tree helpers, sanitization, streams, temp files, tab spacing, type guards, and executable lookup.\n- `packages/utils/src/ptree.ts` and `packages/utils/src/procmgr.ts` wrap native process helpers for ergonomic TypeScript use.\n\n### `packages/stats/`\n\nLocal observability dashboard for session and model usage.\n\n- `packages/stats/src/index.ts` exposes the `skc-stats` CLI entrypoint and exports aggregation/server APIs.\n- `packages/stats/src/aggregator.ts` parses session-derived request metrics and writes aggregated data through SQLite.\n- `packages/stats/src/server.ts` serves local dashboard API routes and static SPA assets.\n- `packages/stats/src/types.ts` and `packages/stats/src/shared-types.ts` define dashboard and aggregate metric shapes.\n\n### `packages/typescript-edit-benchmark/`\n\nPrivate benchmark package for TypeScript edit tasks.\n\n- `packages/typescript-edit-benchmark/package.json` exposes `typescript-edit-benchmark` and depends on the coding-agent, agent-core, ai, tui, utils, diff, prettier, and Babel tooling.\n- `packages/typescript-edit-benchmark/src/index.ts` is the benchmark CLI: it resolves fixtures, loads tasks, runs edit attempts, records progress, and writes reports/conversation dumps under `runs/`.\n\n## Python packages\n\n### `python/skc-rpc/`\n\nTyped Python client for `skc --mode rpc`.\n\n- `python/skc-rpc/pyproject.toml` packages `skc-rpc` for Python 3.11+.\n- `python/skc-rpc/README.md` documents the process-backed stdio client, typed command methods, startup flags, event listeners, todo seeding, host-owned tools, and host-owned URI schemes.\n- `docs/bot-integration.md` is the practical entry guide for generic external controller and bot authors; it ties together coordinator MCP, RPC stdio, bridge limitations, visible tmux fallback, provider-independent smokes, errors, and artifact/report consumption.\n\n### `python/roboskc/`\n\nSelf-hosted GitHub triage/fix bot that drives `skc --mode rpc`.\n\n- `python/roboskc/AGENTS.md` is the authoritative local contract for this subtree.\n- `python/roboskc/pyproject.toml` packages `roboskc` for Python 3.11+ with FastAPI, httpx, pydantic settings, Click, and `skc-rpc`.\n- `python/roboskc/README.md` documents the webhook-to-worktree-to-skc flow, GitHub sidecar trust boundary, persistent per-issue sessions, and audit trail.\n- Important modules include `src/server.py`, `src/queue.py`, `src/tasks.py`, `src/worker.py`, `src/host_tools.py`, `src/sandbox.py`, `src/github_client.py`, `src/github_events.py`, `src/db.py`, and `src/config.py`.\n\n## Runtime flow\n\nA normal CLI session starts in `packages/coding-agent/src/cli.ts`, routes through command handling, then reaches `packages/coding-agent/src/main.ts`. `main.ts` converts CLI/runtime settings into `CreateAgentSessionOptions` and calls `createAgentSession()` in `packages/coding-agent/src/sdk.ts`.\n\nThe SDK builds the session context, loads the default skills, creates built-in tools, resolves model/auth state through `@sayknow-cli/ai`, constructs the system prompt, and instantiates `@sayknow-cli/agent-core`. The agent loop streams model events, executes tools, records tool results, and hands state back to the selected mode: interactive TUI, print, RPC, RPC-UI, ACP, or Bridge.\n\n## Verification and gates\n\nPackage-local checks are defined in each `package.json`. For workflow-definition or default-surface changes, the focused gates are:\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\nFor broader TypeScript verification, use the root script:\n\n```sh\nbun run check:ts\n```\n\nDo not use `tsc` or `npx tsc` directly in this repository.\n",
|
|
20
20
|
"codegraph-custom-tool.md": "# CodeGraph as a custom tool\n\n[CodeGraph](https://github.com/colbymchenry/codegraph) is a local, language-agnostic\ncode knowledge graph for AI agents. It pre-indexes symbols, call edges, and\ndependencies in a project so an agent can answer structural questions (\"how does X\nwork\", \"who calls X\", \"what breaks if I change X\") in a few graph queries instead\nof crawling files with `search`/`read`.\n\nThis guide shows how to wire CodeGraph into SKC through the **custom-tool extension\npath** — no core changes, no built-in provider. SKC intentionally keeps third-party\nCLI integrations like this in the user/project extension layer rather than bundling\nthem, so you own the integration and its lifecycle.\n\n> CodeGraph integrates with other agents over MCP, but this guide wires it as a SKC\n> custom tool around CodeGraph's local **CLI** — it does not add an MCP server or a\n> built-in provider. For how SKC treats MCP servers in standalone sessions, see\n> [`standalone-mcp.md`](standalone-mcp.md).\n\n## 1. Install and index\n\n```bash\n# Install the CodeGraph CLI (or use the install script from CodeGraph's README).\nnpm i -g @colbymchenry/codegraph\n\n# Build the local index for a project.\ncd your-project\ncodegraph init\n```\n\n`codegraph init` creates a local `.codegraph/` directory. No data leaves your\nmachine — it is a local SQLite index.\n\n## 2. Add the custom tool\n\nSKC discovers custom tools from a `tools/` directory in its config dirs:\n\n- **Project-scoped**: `<project>/.skc/tools/`\n- **User-scoped (all projects)**: `~/.skc/agent/tools/`\n\nA `*.ts` tool file's default export is a factory `(pi) => CustomTool`. The factory\nreceives an API (`pi`) with members such as `exec`, `cwd`, `zod`, and `logger` — so\nthe tool needs no imports from SKC internals.\n\nSave the following as `.skc/tools/codegraph.ts` (project) or\n`~/.skc/agent/tools/codegraph.ts` (user):\n\n```typescript\n/**\n * CodeGraph custom tool for sayknow-cli (SKC).\n *\n * Wraps the local CodeGraph CLI (https://github.com/colbymchenry/codegraph) so the\n * agent can query a project's code knowledge graph instead of crawling files.\n *\n * It only runs CodeGraph's query-style (read-only) subcommands and never edits your\n * source files. It does not run indexing or sync commands. (CodeGraph maintains its\n * own local `.codegraph/` index via its own CLI; this tool only reads from it.)\n *\n * Prereqs: `npm i -g @colbymchenry/codegraph` and `codegraph init` in the project.\n */\nimport type { CustomToolFactory } from \"@sayknow-cli/coding-agent\"; // optional: editor types only\n\nconst CODEGRAPH_CLI = \"codegraph\";\nconst TIMEOUT_MS = 60_000;\nconst SEARCH_LIMIT_DEFAULT = 10;\nconst MAX = 100;\n\nconst codegraph: CustomToolFactory = (pi) => {\n\tconst z = pi.zod;\n\n\tconst parameters = z\n\t\t.object({\n\t\t\top: z\n\t\t\t\t.enum([\"explore\", \"search\", \"callers\", \"callees\", \"impact\", \"status\"])\n\t\t\t\t.describe(\n\t\t\t\t\t\"explore: context (relevant source + call paths) for a natural-language query — prefer for 'how does X work'; search: full-text symbol search (target=query); callers: who calls target; callees: what target calls; impact: blast radius of changing target; status: index health (no target).\",\n\t\t\t\t),\n\t\t\ttarget: z\n\t\t\t\t.string()\n\t\t\t\t.optional()\n\t\t\t\t.describe(\n\t\t\t\t\t\"For explore: a natural-language query or symbol(s). For callers/callees/impact: a symbol name. For search: the query. Omit for status.\",\n\t\t\t\t),\n\t\t\tlimit: z.number().int().min(1).max(MAX).optional().describe(`Max search results (default ${SEARCH_LIMIT_DEFAULT}).`),\n\t\t\tmaxFiles: z.number().int().min(1).max(MAX).optional().describe(\"For explore: cap files whose source is included.\"),\n\t\t})\n\t\t.strict();\n\n\ttype Params = import(\"zod/v4\").infer<typeof parameters>;\n\n\tfunction buildArgs(params: Params): string[] {\n\t\tif (params.op === \"status\") return [\"status\", pi.cwd, \"--json\"];\n\t\tconst target = params.target?.trim();\n\t\tif (!target) throw new Error(`codegraph ${params.op} requires a non-empty \"target\".`);\n\t\tif (params.op === \"search\") {\n\t\t\tconst limit = Math.min(params.limit ?? SEARCH_LIMIT_DEFAULT, MAX);\n\t\t\treturn [\"query\", target, \"--json\", \"--limit\", String(limit), \"--path\", pi.cwd];\n\t\t}\n\t\tif (params.op === \"explore\") {\n\t\t\tconst args = [\"explore\", target, \"--path\", pi.cwd];\n\t\t\tif (params.maxFiles !== undefined) args.push(\"--max-files\", String(params.maxFiles));\n\t\t\treturn args;\n\t\t}\n\t\treturn [params.op, target, \"--json\", \"--path\", pi.cwd];\n\t}\n\n\tfunction ref(r: { name: string; kind: string; filePath: string; startLine: number }): string {\n\t\treturn ` - ${r.name} (${r.kind}) — ${r.filePath}:${r.startLine}`;\n\t}\n\n\tfunction render(params: Params, stdout: string): string {\n\t\tif (params.op === \"explore\") return stdout.trim() || `No exploration results for \"${params.target?.trim() ?? \"\"}\".`;\n\t\tlet data: any;\n\t\ttry {\n\t\t\tdata = JSON.parse(stdout);\n\t\t} catch {\n\t\t\tthrow new Error(`codegraph ${params.op} returned unparseable output.`);\n\t\t}\n\t\tif (params.op === \"search\") {\n\t\t\tconst hits = data as Array<{ node: any }>;\n\t\t\tif (hits.length === 0) return `No symbols matched \"${params.target?.trim() ?? \"\"}\".`;\n\t\t\treturn [\n\t\t\t\t`${hits.length} symbol(s) matching \"${params.target?.trim() ?? \"\"}\":`,\n\t\t\t\t...hits.map(({ node }) => {\n\t\t\t\t\tconst exp = node.isExported ? \" [exported]\" : \"\";\n\t\t\t\t\tconst sig = node.signature ? ` ${node.signature}` : \"\";\n\t\t\t\t\treturn ` - ${node.name} (${node.kind})${sig}${exp} — ${node.filePath}:${node.startLine}`;\n\t\t\t\t}),\n\t\t\t].join(\"\\n\");\n\t\t}\n\t\tif (params.op === \"callers\") {\n\t\t\tconst list = data.callers ?? [];\n\t\t\treturn list.length === 0\n\t\t\t\t? `No callers found for \"${data.symbol}\".`\n\t\t\t\t: [`${list.length} caller(s) of \"${data.symbol}\":`, ...list.map(ref)].join(\"\\n\");\n\t\t}\n\t\tif (params.op === \"callees\") {\n\t\t\tconst list = data.callees ?? [];\n\t\t\treturn list.length === 0\n\t\t\t\t? `\"${data.symbol}\" has no recorded callees.`\n\t\t\t\t: [`${list.length} callee(s) of \"${data.symbol}\":`, ...list.map(ref)].join(\"\\n\");\n\t\t}\n\t\tif (params.op === \"impact\") {\n\t\t\tconst header = `Impact of changing \"${data.symbol}\" (depth ${data.depth}): ${data.nodeCount} node(s), ${data.edgeCount} edge(s) affected.`;\n\t\t\tconst list = data.affected ?? [];\n\t\t\treturn list.length === 0 ? header : [header, \"Affected:\", ...list.map(ref)].join(\"\\n\");\n\t\t}\n\t\t// status\n\t\tif (!data.initialized) return `CodeGraph is not initialized for ${data.projectPath}. Run \\`codegraph init\\`.`;\n\t\tconst lines = [\n\t\t\t`CodeGraph index for ${data.projectPath}:`,\n\t\t\t` files: ${data.fileCount}, nodes: ${data.nodeCount}, edges: ${data.edgeCount}`,\n\t\t];\n\t\tif (data.languages?.length) lines.push(` languages: ${data.languages.join(\", \")}`);\n\t\tconst p = data.pendingChanges;\n\t\tif (p && (p.added || p.modified || p.removed)) lines.push(` pending sync: +${p.added} ~${p.modified} -${p.removed}`);\n\t\treturn lines.join(\"\\n\");\n\t}\n\n\treturn {\n\t\tname: \"codegraph\",\n\t\tlabel: \"CodeGraph\",\n\t\tdescription:\n\t\t\t\"Query the project's CodeGraph code knowledge graph (symbols, callers, callees, impact, and an 'explore' context query) via the local codegraph CLI. Read-only with respect to your source. Prefer over search/read for structural questions. Requires `codegraph init` to have been run in the project.\",\n\t\tparameters,\n\t\tstrict: true,\n\t\tasync execute(_id: string, params: Params, _onUpdate: unknown, _ctx: unknown, signal?: AbortSignal) {\n\t\t\tlet result: { stdout: string; stderr: string; code: number };\n\t\t\ttry {\n\t\t\t\tresult = await pi.exec(CODEGRAPH_CLI, buildArgs(params), { cwd: pi.cwd, signal, timeout: TIMEOUT_MS });\n\t\t\t} catch (e) {\n\t\t\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\t\t\tif (/not found|enoent/i.test(msg)) {\n\t\t\t\t\tthrow new Error(\"The `codegraph` CLI is not installed. Install it: npm i -g @colbymchenry/codegraph\");\n\t\t\t\t}\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t\tif (result.code !== 0) {\n\t\t\t\tconst err = result.stderr.toLowerCase();\n\t\t\t\tif (err.includes(\"not initialized\") || err.includes(\".codegraph\") || err.includes(\"no index\")) {\n\t\t\t\t\tthrow new Error(\"CodeGraph is not initialized for this project. Run `codegraph init` in the project root.\");\n\t\t\t\t}\n\t\t\t\tthrow new Error(result.stderr.trim() || \"codegraph failed with no diagnostic output.\");\n\t\t\t}\n\t\t\treturn { content: [{ type: \"text\", text: render(params, result.stdout) }] };\n\t\t},\n\t};\n};\n\nexport default codegraph;\n```\n\nThe `import type` line is optional — it only provides editor types when SKC is\nresolvable from your tool file. It is erased at runtime, so the tool loads fine\nwithout it.\n\n## 3. Use it\n\nStart SKC in the project. The `codegraph` tool is now available to the model. Ask\na structural question and it will call the tool, for example:\n\n- `codegraph` with `{ \"op\": \"explore\", \"target\": \"how requests are routed\" }` can\n return relevant source plus graph context in one call.\n- `{ \"op\": \"callers\", \"target\": \"MyClass.handle\" }` lists callers.\n- `{ \"op\": \"impact\", \"target\": \"parseConfig\" }` shows what a change would affect.\n- `{ \"op\": \"status\" }` reports index health.\n\n## Operations\n\n| `op` | `target` | Description |\n| --- | --- | --- |\n| `explore` | natural-language query | Context query: relevant symbols' source plus graph context (CodeGraph's `explore`). `maxFiles` caps included source. |\n| `search` | query | Full-text symbol search (`limit`, default 10). |\n| `callers` | symbol | Functions/methods that call the symbol, including dynamic dispatch. |\n| `callees` | symbol | Functions/methods the symbol calls. |\n| `impact` | symbol | Blast radius of changing the symbol. |\n| `status` | — | Index health: file/node/edge counts, languages, pending sync. |\n\n## Notes\n\n- **Read-only with respect to your code.** The tool only runs CodeGraph's\n query-style subcommands; it never edits your files and does not run indexing or\n sync commands. CodeGraph maintains its own local `.codegraph/` index via its CLI.\n If results look stale or `status` reports pending changes, refresh the index with\n CodeGraph's CLI (e.g. `codegraph sync`) outside SKC.\n- **Safe argument handling.** The example spawns CodeGraph with argv arrays via\n `pi.exec` (no shell), `op` is constrained to a fixed enum, and numeric inputs are\n capped — there is no shell interpolation of model-provided values.\n- **Scope.** Use a project-scoped file to limit the tool to one repo, or a\n user-scoped file to make it available everywhere `codegraph init` has been run.\n- **Naming.** The tool registers as `codegraph`; rename it in the file if it\n collides with another tool in your setup.\n- **Fallback.** If the graph reports a symbol is missing, a file is flagged as\n pending sync, or you need a non-structural text search, refresh the CodeGraph\n index outside SKC or fall back to `read`/`search`.\n- For background on how SKC treats external tools and MCP servers in standalone\n sessions, see [`standalone-mcp.md`](standalone-mcp.md).\n",
|
|
21
21
|
"compaction.md": "# Compaction and Branch Summaries\n\nCompaction and branch summaries are the two mechanisms that keep long sessions usable without losing prior work context.\n\n- **Compaction** rewrites old history into a summary on the current branch.\n- **Branch summary** captures abandoned branch context during `/tree` navigation.\n\nBoth are persisted as session entries and converted back into user-context messages when rebuilding LLM input.\n\n## Key implementation files\n\n- `packages/agent/src/compaction/compaction.ts` (context-full summarization and handoff generation)\n- `packages/agent/src/compaction/branch-summarization.ts`\n- `packages/agent/src/compaction/pruning.ts`\n- `packages/agent/src/compaction/utils.ts`\n- `packages/agent/src/compaction/openai.ts`\n- `packages/coding-agent/src/session/session-manager.ts`\n- `packages/coding-agent/src/session/agent-session.ts`\n- `packages/coding-agent/src/session/messages.ts`\n- `packages/coding-agent/src/extensibility/hooks/types.ts`\n- `packages/coding-agent/src/config/settings-schema.ts`\n\n## Session entry model\n\nCompaction and branch summaries are first-class session entries, not plain assistant/user messages.\n\n- `CompactionEntry`\n - `type: \"compaction\"`\n - `summary`, optional `shortSummary`\n - `firstKeptEntryId` (compaction boundary)\n - `tokensBefore`\n - optional `details`, `preserveData`, `fromExtension`\n- `BranchSummaryEntry`\n - `type: \"branch_summary\"`\n - `fromId`, `summary`\n - optional `details`, `fromExtension`\n\nWhen context is rebuilt (`buildSessionContext`):\n\n1. Latest compaction on the active path is converted to one `compactionSummary` message.\n2. Kept entries from `firstKeptEntryId` to the compaction point are re-included.\n3. Later entries on the path are appended.\n4. `branch_summary` entries are converted to `branchSummary` messages.\n5. `custom_message` entries are converted to `custom` messages.\n\nThose custom roles are then transformed into LLM-facing user messages in `convertToLlm()` using the static templates:\n\n- `packages/agent/src/compaction/prompts/compaction-summary-context.md`\n- `packages/agent/src/compaction/prompts/branch-summary-context.md`\n- `packages/agent/src/compaction/prompts/handoff-document.md`\n\n## Compaction pipeline\n\n### Triggers\n\nCompaction/context maintenance can run in four ways:\n\n1. **Manual context compaction**: `/compact [instructions]` calls `AgentSession.compact(...)`.\n2. **Automatic overflow recovery**: after a same-model assistant error that matches context overflow.\n3. **Automatic threshold maintenance**: after a successful turn when context exceeds the resolved threshold.\n4. **Idle maintenance**: `runIdleCompaction()` can invoke the same auto-maintenance path with reason `\"idle\"`.\n\n### Compaction shape (visual)\n\n```text\nBefore compaction:\n\n entry: 0 1 2 3 4 5 6 7 8 9\n ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬──────┐\n │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │\n └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┘\n └────────┬───────┘ └──────────────┬──────────────┘\n messagesToSummarize kept messages\n ↑\n firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n entry: 0 1 2 3 4 5 6 7 8 9 10\n ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬──────┬─────┐\n │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │ cmp │\n └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┴─────┘\n └──────────┬──────┘ └──────────────────────┬───────────────────┘\n not sent to LLM sent to LLM\n ↑\n starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n ↑ ↑ └─────────────────┬────────────────┘\n prompt from cmp messages from firstKeptEntryId\n```\n\n### Overflow-retry vs threshold/idle maintenance\n\nThe automatic paths are intentionally different:\n\n- **Overflow recovery**\n - Trigger: current-model assistant error is detected as context overflow and the error is not older than the latest compaction.\n - The failing assistant error message is removed from active agent state before retry.\n - Context promotion is tried first; if a configured larger model is available, the agent switches model and retries without compacting.\n - If promotion is unavailable and compaction is enabled, context-full compaction runs with `reason: \"overflow\"` and `willRetry: true`; handoff strategy is not used for overflow.\n - On success, agent auto-continues (`agent.continue()`) after compaction.\n\n- **Threshold maintenance**\n - Trigger: successful, non-error assistant message whose adjusted context tokens exceed `resolveThresholdTokens(...)`.\n - Tool-output pruning can reduce the measured token count before threshold comparison.\n - Context promotion is tried before compaction.\n - If promotion is unavailable, auto maintenance runs with `reason: \"threshold\"` and `willRetry: false`.\n - With `compaction.strategy: \"handoff\"`, threshold maintenance starts a new handoff session instead of writing a compaction entry; if handoff returns no document without aborting, it falls back to context-full compaction.\n - On success, if `compaction.autoContinue !== false`, schedules an agent-authored developer auto-continue prompt from `prompts/system/auto-continue.md`.\n\n- **Idle maintenance**\n - Trigger: `runIdleCompaction()` when not streaming or already compacting.\n - Uses `reason: \"idle\"` and does not auto-continue afterward.\n\n### Pre-compaction pruning\n\nBefore compaction checks, tool-result pruning may run (`pruneToolOutputs`).\n\nDefault prune policy:\n\n- Protect newest `40_000` tool-output tokens.\n- Require at least `20_000` total estimated savings.\n- Never prune tool results from `skill` or `read`.\n\nPruned tool results are replaced with:\n\n- `[Output truncated - N tokens]`\n\nIf pruning changes entries, session storage is rewritten and agent message state is refreshed before compaction decisions.\n\n### Boundary and cut-point logic\n\n`prepareCompaction()` only considers entries since the last compaction entry (if any).\n\n1. Find previous compaction index.\n2. Compute `boundaryStart = prevCompactionIndex + 1`.\n3. Adapt `keepRecentTokens` using measured usage ratio when available.\n4. Run `findCutPoint()` over the boundary window.\n\nValid cut points include:\n\n- message entries with roles: `user`, `assistant`, `bashExecution`, `hookMessage`, `branchSummary`, `compactionSummary`\n- `custom_message` entries\n- `branch_summary` entries\n\nHard rule: never cut at `toolResult`.\n\nIf there are non-message metadata entries immediately before the cut point (`model_change`, `thinking_level_change`, labels, etc.), they are pulled into the kept region by moving cut index backward until a message or compaction boundary is hit.\n\n### Split-turn handling\n\nIf cut point is not at a user-turn start, compaction treats it as a split turn.\n\nTurn start detection treats these as user-turn boundaries:\n\n- `message.role === \"user\"`\n- `message.role === \"bashExecution\"`\n- `custom_message` entry\n- `branch_summary` entry\n\nSplit-turn compaction generates two summaries:\n\n1. History summary (`messagesToSummarize`)\n2. Turn-prefix summary (`turnPrefixMessages`)\n\nFinal stored summary is merged as:\n\n```markdown\n<history summary>\n\n---\n\n**Turn Context (split turn):**\n\n<turn prefix summary>\n```\n\n### Summary generation\n\n`compact(...)` builds summaries from serialized conversation text:\n\n1. Convert messages via `convertToLlm()`.\n2. Serialize with `serializeConversation()`.\n3. Wrap in `<conversation>...</conversation>`.\n4. Optionally include `<previous-summary>...</previous-summary>`.\n5. Optionally inject hook context as `<additional-context>` list.\n6. Execute summarization prompt with `SUMMARIZATION_SYSTEM_PROMPT`.\n\nPrompt selection:\n\n- first compaction: `compaction-summary.md`\n- iterative compaction with prior summary: `compaction-update-summary.md`\n- split-turn second pass: `compaction-turn-prefix.md`\n- short UI summary: `compaction-short-summary.md`\n- handoff document: `handoff-document.md` (used by `generateHandoff(...)`, not serialized compaction)\n\nRemote summarization modes:\n\n- If `compaction.remoteEndpoint` is set and remote compaction is enabled, local summary generation POSTs:\n - `{ systemPrompt, prompt }`\n- Expects JSON containing at least `{ summary }`.\n- For OpenAI/OpenAI code provider models, compaction first tries the provider-native `/responses/compact` endpoint when remote compaction is enabled. It preserves provider replacement history in `preserveData.openaiRemoteCompaction` and falls back to local summarization if that native request fails.\n\n### Handoff generation\n\n`packages/agent/src/compaction/compaction.ts` also exports `generateHandoff(...)`. Handoff generation uses the same `completeSimple(...)` oneshot style as summarization, but it preserves the live agent cache prefix by sending the active system prompt, tool array, and real LLM message history, then appending one agent-attributed `user` message containing the handoff prompt. It forces `toolChoice: \"none\"` and returns joined text blocks directly.\n\nHandoff does not write a `CompactionEntry`. `AgentSession.handoff()` owns the session transition: it starts a new session, injects the generated document as a visible `custom_message` with `customType: \"handoff\"`, and rebuilds agent messages from that new session.\n\n### File-operation context in summaries\n\nCompaction tracks cumulative file activity using assistant tool calls:\n\n- `read(path)` → read set\n- `write(path)` → modified set\n- `edit(path)` → modified set\n\nCumulative behavior:\n\n- Includes prior compaction details only when prior entry is pi-generated (`fromExtension !== true`).\n- In split turns, includes turn-prefix file ops too.\n- `readFiles` excludes files also modified.\n\nSummary text gets file tags appended via prompt template:\n\n```xml\n<read-files>\n...\n</read-files>\n<modified-files>\n...\n</modified-files>\n```\n\n### Persist and reload\n\nAfter summary generation (or hook-provided summary), agent session:\n\n1. Appends `CompactionEntry` with `appendCompaction(...)` for context-full maintenance; handoff strategy creates a new session and injects a handoff `custom_message` instead.\n2. Rebuilds display context from the active leaf via `buildDisplaySessionContext()`.\n3. Replaces live agent messages with rebuilt context.\n4. Emits `session_compact` hook event.\n\n## Branch summarization pipeline\n\nBranch summarization is tied to tree navigation, not token overflow.\n\n### Trigger\n\nDuring `navigateTree(...)`:\n\n1. Compute abandoned entries from old leaf to common ancestor using `collectEntriesForBranchSummary(...)`.\n2. If caller requested summary (`options.summarize`), generate summary before switching leaf.\n3. If summary exists, attach it at the navigation target using `branchWithSummary(...)`.\n\nOperationally this is commonly driven by `/tree` flow when `branchSummary.enabled` is enabled.\n\n### Branch switch shape (visual)\n\n```text\nTree before navigation:\n\n ┌─ B ─ C ─ D (old leaf, being abandoned)\n A ───┤\n └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n ┌─ B ─ C ─ D ─ [summary of B,C,D]\n A ───┤\n └─ E ─ F (new leaf)\n```\n\n### Preparation and token budget\n\n`generateBranchSummary(...)` computes budget as:\n\n- `tokenBudget = model.contextWindow - branchSummary.reserveTokens`\n\n`prepareBranchEntries(...)` then:\n\n1. First pass: collect cumulative file ops from all summarized entries, including prior pi-generated `branch_summary` details.\n2. Second pass: walk newest → oldest, adding messages until token budget is reached.\n3. Prefer preserving recent context.\n4. May still include large summary entries near budget edge for continuity.\n\nCompaction entries are included as messages (`compactionSummary`) during branch summarization input.\n\n### Summary generation and persistence\n\nBranch summarization:\n\n1. Converts and serializes selected messages.\n2. Wraps in `<conversation>`.\n3. Uses custom instructions if supplied, otherwise `branch-summary.md`.\n4. Calls summarization model with `SUMMARIZATION_SYSTEM_PROMPT`.\n5. Prepends `branch-summary-preamble.md`.\n6. Appends file-operation tags.\n\nResult is stored as `BranchSummaryEntry` with optional details (`readFiles`, `modifiedFiles`).\n\n## Extension and hook touchpoints\n\n### `session_before_compact`\n\nPre-compaction hook.\n\nCan:\n\n- cancel compaction (`{ cancel: true }`)\n- provide full custom compaction payload (`{ compaction: CompactionResult }`)\n\n### `session.compacting`\n\nPrompt/context customization hook for default compaction.\n\nCan return:\n\n- `prompt` (override base summary prompt)\n- `context` (extra context lines injected into `<additional-context>`)\n- `preserveData` (stored on compaction entry)\n\n### `session_compact`\n\nPost-compaction notification with saved `compactionEntry` and `fromExtension` flag.\n\n### `session_before_tree`\n\nRuns on tree navigation before default branch summary generation.\n\nCan:\n\n- cancel navigation\n- provide custom `{ summary: { summary, details } }` used when user requested summarization\n\n### `session_tree`\n\nPost-navigation event exposing new/old leaf and optional summary entry.\n\n## Runtime behavior and failure semantics\n\n- Manual compaction aborts current agent operation first.\n- `abortCompaction()` cancels both manual and auto-compaction controllers.\n- Auto compaction emits start/end session events for UI/state updates.\n- Auto compaction can try multiple model candidates and retry transient failures; long retry delays prefer the next candidate when one is available.\n- Overflow errors are excluded from generic retry path because they are handled by context promotion/compaction.\n- If auto-compaction fails:\n - overflow path emits `Context overflow recovery failed: ...`\n - threshold path emits `Auto-compaction failed: ...`\n- Branch summarization can be cancelled via abort signal (e.g., Escape), returning canceled/aborted navigation result.\n\n## Settings and defaults\n\nFrom `settings-schema.ts`:\n\n- `compaction.enabled` = `true`\n- `compaction.strategy` = `\"context-full\"` (`\"handoff\"` and `\"off\"` are also supported)\n- `compaction.reserveTokens` = `16384`\n- `compaction.keepRecentTokens` = `20000`\n- `compaction.autoContinue` = `true`\n- `compaction.remoteEnabled` = `true`\n- `compaction.remoteEndpoint` = `undefined`\n- `compaction.thresholdPercent` = `-1` and `compaction.thresholdTokens` = `-1`; when no positive override is set, the threshold is `contextWindow - max(15% of contextWindow, reserveTokens)`\n- `compaction.idleEnabled` = `true`\n- `branchSummary.enabled` = `false`\n- `branchSummary.reserveTokens` = `16384`\n\nThese values are consumed at runtime by `AgentSession` and compaction/branch summarization modules.\n",
|
|
@@ -27,11 +27,12 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
27
27
|
"fs-scan-cache-architecture.md": "# Filesystem Scan Cache Architecture Contract\n\nThis document defines the current contract for the shared filesystem scan cache implemented in Rust (`crates/pi-natives/src/fs_cache.rs`) and consumed by native discovery/search APIs exposed to `packages/coding-agent`.\n\n## What this cache is\n\nThe cache stores full directory-scan entry lists (`GlobMatch[]`) keyed by scan scope and traversal policy, then lets higher-level operations (glob filtering, fuzzy scoring, grep file selection) run against those cached entries.\n\nPrimary goals:\n\n- avoid repeated filesystem walks for repeated discovery/search calls\n- keep consistency across `glob`, `fuzzyFind`, and `grep` when they share the same scan policy\n- allow explicit staleness recovery for empty results and explicit invalidation after file mutations\n\n## Ownership and public surface\n\n- Cache implementation and policy: `crates/pi-natives/src/fs_cache.rs`\n- Native consumers:\n - `crates/pi-natives/src/glob.rs`\n - `crates/pi-natives/src/fd.rs` (`fuzzyFind`)\n - `crates/pi-natives/src/grep.rs`\n- JS binding/export:\n - `packages/natives/src/glob/index.ts` (`invalidateFsScanCache`)\n - `packages/natives/src/glob/types.ts`\n - `packages/natives/src/grep/types.ts`\n- Coding-agent mutation invalidation helpers:\n - `packages/coding-agent/src/tools/fs-cache-invalidation.ts`\n\n## Cache key partitioning (hard contract)\n\nEach entry is keyed by:\n\n- canonicalized `root` directory path\n- `include_hidden` boolean\n- `use_gitignore` boolean\n- `skip_node_modules` boolean\n\nImplications:\n\n- Hidden and non-hidden scans do **not** share entries.\n- Gitignore-respecting and ignore-disabled scans do **not** share entries.\n- Scans that prune `node_modules` do **not** share entries with scans that include it.\n- Consumers must pass stable semantics for hidden/gitignore/node_modules behavior; changing any flag creates a different cache partition.\n\n## Scan collection behavior\n\nCache population uses a deterministic walker (`ignore::WalkBuilder`) configured by `include_hidden`, `use_gitignore`, and `skip_node_modules`:\n\n- `follow_links(false)`\n- sorted by file path\n- `.git` is always skipped\n- `node_modules` is pruned at traversal time when `skip_node_modules=true`\n- entry file type + `mtime` are captured via `symlink_metadata`\n\nSearch roots are resolved by `resolve_search_path`:\n\n- relative paths are resolved against current cwd\n- target must be an existing directory\n- root is canonicalized when possible\n\n## Freshness and eviction policy\n\nGlobal policy (environment-overridable):\n\n- `FS_SCAN_CACHE_TTL_MS` (default `1000`)\n- `FS_SCAN_EMPTY_RECHECK_MS` (default `200`)\n- `FS_SCAN_CACHE_MAX_ENTRIES` (default `16`)\n\nBehavior:\n\n- `get_or_scan(...)`\n - if TTL is `0`: bypass cache entirely, always fresh scan (`cache_age_ms = 0`)\n - on cache hit within TTL: return cached entries + non-zero `cache_age_ms`\n - on expired hit: evict key, rescan, store fresh entry\n- max entry enforcement is oldest-first eviction by `created_at`\n\n## Empty-result fast recheck (separate from normal hits)\n\nNormal cache hit:\n\n- a cache hit inside TTL returns cached entries and does nothing else.\n\nEmpty-result fast recheck:\n\n- this is a **caller-side** policy using `ScanResult.cache_age_ms`\n- if filtered/query result is empty and cached scan age is at least `empty_recheck_ms()`, caller performs one `force_rescan(...)` and retries\n- intended to reduce stale-negative results when files were recently added but cache is still within TTL\n\nCurrent consumers:\n\n- `glob`: rechecks when filtered matches are empty and scan age exceeds threshold\n- `fuzzyFind` (`fd.rs`): rechecks only when query is non-empty and scored matches are empty\n- `grep`: rechecks when selected candidate file list is empty\n\n## Consumer defaults and cache usage\n\nCache is opt-in on all exposed APIs (`cache?: boolean`, default `false`).\n\nCurrent defaults in native APIs:\n\n- `glob`: `hidden=false`, `gitignore=true`, `cache=false`, and `node_modules` included only when the pattern mentions `node_modules`\n- `fuzzyFind`: `hidden=false`, `gitignore=true`, `cache=false`, and `node_modules` is skipped\n- `grep`: `hidden=true`, `gitignore=true`, `cache=false`, and `node_modules` included only when the glob mentions `node_modules`\n\nCoding-agent callers today:\n\n- High-volume mention candidate discovery enables cache:\n - `packages/coding-agent/src/utils/file-mentions.ts`\n - profile: `hidden=true`, `gitignore=true`, `includeNodeModules=true`, `cache=true`\n- Tool-level `grep` integration currently disables scan cache (`cache: false`):\n - `packages/coding-agent/src/tools/grep.ts`\n\n## Invalidation contract\n\nNative invalidation entrypoint:\n\n- `invalidateFsScanCache(path?: string)`\n - with `path`: remove cache entries whose root is a prefix of target path\n - without path: clear all scan cache entries\n\nPath handling details:\n\n- relative invalidation paths are resolved against cwd\n- invalidation attempts canonicalization\n- if target does not exist (e.g., delete), fallback canonicalizes parent and reattaches filename when possible\n- this preserves invalidation behavior for create/delete/rename where one side may not exist\n\n## Coding-agent mutation flow responsibilities\n\nCoding-agent code must invalidate after successful filesystem mutations.\n\nCentral helpers:\n\n- `invalidateFsScanAfterWrite(path)`\n- `invalidateFsScanAfterDelete(path)`\n- `invalidateFsScanAfterRename(oldPath, newPath)` (invalidates both sides when paths differ)\n\nCurrent mutation tool callsites:\n\n- `packages/coding-agent/src/tools/write.ts`\n- `packages/coding-agent/src/patch/index.ts` (hashline/patch/replace flows)\n\nRule: if a flow mutates filesystem content or location and bypasses these helpers, cache staleness bugs are expected.\n\n## Adding a new cache consumer safely\n\nWhen introducing cache use in a new scanner/search path:\n\n1. **Use stable scan policy inputs**\n - decide hidden/gitignore/node_modules semantics first\n - pass them consistently to `get_or_scan`/`force_rescan` so cache partitions are intentional\n\n2. **Treat cache data as pre-filtered only by traversal policy**\n - apply tool-specific filtering (glob patterns, type filters, scoring) after retrieval\n - never assume cached entries already reflect your higher-level filters\n\n3. **Implement empty-result fast recheck only for stale-negative risk**\n - use `scan.cache_age_ms >= empty_recheck_ms()`\n - retry once with `force_rescan(..., store=true, ...)`\n - keep this path separate from normal cache-hit logic\n\n4. **Respect no-cache mode explicitly**\n - when caller disables cache, call `force_rescan(..., store=false, ...)`\n - do not populate shared cache in a no-cache request path\n\n5. **Wire mutation invalidation for any new write path**\n - after successful write/edit/delete/rename, call the coding-agent invalidation helper\n - for rename/move, invalidate both old and new paths\n\n6. **Do not add per-call TTL knobs**\n - current contract is global policy only (env-configured), no per-request TTL override\n\n## Known boundaries\n\n- Cache scope is process-local in-memory (`DashMap`), not persisted across process restarts.\n- Cache stores scan entries, not final tool results.\n- `glob`/`fuzzyFind`/`grep` share scan entries only when key dimensions (`root`, `hidden`, `gitignore`, `skip_node_modules`) match.\n- `.git` is always excluded at scan collection time regardless of caller options.\n",
|
|
28
28
|
"geobench.md": "# GEO benchmark for Sayknow-CLI\n\nThis repository includes a [`geobench`](https://github.com/NomaDamas/geobench) product spec for measuring LLM answer visibility: hit rate, MRR, share of voice, citation rate/share, and confidence intervals.\n\n```bash\n/path/to/geobench/dist/geobench estimate --product geobench/sayknow-cli.yaml --providers openai --tier cheap\n/path/to/geobench/dist/geobench profile geobench/sayknow-cli.yaml\n/path/to/geobench/dist/geobench bench --product geobench/sayknow-cli.yaml --providers openai --tier cheap --mode benchmark\n```\n\nPublish aggregate metrics only; do not publish raw provider answers, secrets, or private run logs.\n",
|
|
29
29
|
"git-daemon.md": "# Git daemon\n\nThe git daemon is the autonomous per-repo service that watches a repository and resolves referenced work items by opening reviewed pull requests.\n",
|
|
30
|
+
"gpt-5.6-codex-preset-benchmark.md": "# GPT-5.6 Codex preset benchmark\n\nThis report records descriptive local exact-edit evidence and the product judgments used to assign GPT-5.6 Sol, Terra, and Luna to SKC's built-in Codex-related model profiles.\n\n## Decision summary\n\nBuilt-in role assignments are product judgments. The selected TypeScript edit evidence below directly compares only bounded executor-style edits; it does not establish superiority, statistical significance, production reliability, or stability for any role.\n\n- **Luna high**: assigned to bounded execution in `codex-eco`.\n- **Terra high**: assigned to planning and lower-stakes criticism.\n- **Terra xhigh**: assigned to technically difficult execution and criticism in Medium, Pro, and combo profiles.\n- **Sol medium**: assigned to general orchestration for the `codex-medium` default agent.\n- **Sol high/xhigh/max**: assigned to architecture, high-stakes criticism, and Pro orchestration.\n\nThe edit benchmark does not measure default-agent interpretation, orchestration, explanation, or routing, and it does not measure planner, architect, or critic work. Those non-executor assignments are product judgments, not benchmark findings.\n\n## Environment\n\n- Date: 2026-07-11\n- SKC provider: local `layofflabs` OpenAI Responses-compatible endpoint\n- Models: `gpt-5.6-luna`, `gpt-5.6-terra`, `gpt-5.6-sol`\n- Benchmark: `packages/typescript-edit-benchmark`\n- Verification: exact expected-file comparison after formatting normalization\n- Required tools: at least one `read` and one `edit` call per successful sample\n- Guided edits: disabled\n- Attempts: one per sample\n\nThe local provider recorded zero cost. The amounts below are non-billing list-price estimates calculated from the listed rates; they are not provider charges or production-cost predictions.\n\n| Model | Input / 1M | Output / 1M |\n|---|---:|---:|\n| Luna | $1.00 | $6.00 |\n| Terra | $2.50 | $15.00 |\n| Sol | $5.00 | $30.00 |\n\n## Initial broad sample\n\nThe first pass used eight mutation tasks with one run per task:\n\n- multi-location identifier replacement\n- call-argument swap\n- early-return removal\n- `if`/`else` structural swap\n- named-import swap\n- duplicate-line disambiguation\n- off-by-one literal correction\n- optional-chain removal\n\n| Setup | Tasks passed | Avg time/run | Input tokens | Output tokens | Est. cost |\n|---|---:|---:|---:|---:|---:|\n| Luna high | 6/8 | 54.8s | 2.86M | 10.8K | $2.92 |\n| Luna xhigh | 7/8 | 31.2s | 784K | 6.6K | $0.82 |\n| Terra high | 7/8 | 51.1s | 1.13M | 5.9K | $2.92 |\n| Terra xhigh | 8/8 | 50.9s | 820K | 5.9K | $2.14 |\n| Sol medium | 6/8 | 30.1s | 376K | 4.3K | $2.01 |\n\nIn this eight-task, one-attempt-per-task sample, Terra xhigh recorded 8/8 verified edits. Luna xhigh recorded 7/8; one run per task does not establish stability.\n\n## Repeated selected-task sample\n\nThe selected pass ran four discriminating TypeScript edit tasks three times each, scheduling 12 samples per setup:\n\n1. Remove the intended early return from a file containing several similar returns.\n2. Swap the intended `if`/`else` branches without changing nearby equivalent logic.\n3. Correct one specific off-by-one value among several plausible candidates.\n4. Remove the intended optional chain without modifying similar occurrences.\n\nThe confirmation command shape was:\n\n```sh\nbun --cwd=packages/typescript-edit-benchmark run start \\\n --model \"layofflabs/<model>\" \\\n --thinking \"<effort>\" \\\n --runs 3 \\\n --task-concurrency 2 \\\n --timeout 180000 \\\n --max-turns 40 \\\n --tasks \"structural-remove-early-return-003,structural-swap-if-else-004,literal-off-by-one-003,access-remove-optional-chain-004\" \\\n --require-read-tool-call \\\n --require-edit-tool-call \\\n --format json\n```\n\n| Setup | Verified edits / recorded runs | Rate | Avg time | Input tokens | Output tokens | Est. list-price cost | Est. cost / verified edit |\n|---|---:|---:|---:|---:|---:|---:|---:|\n| Luna high | 8/12 | 66.7% | 75.2s | 3.61M | 18.9K | $3.73 | $0.47 |\n| Luna xhigh | 9/12 | 75.0% | 80.5s | 6.60M | 25.0K | $6.75 | $0.75 |\n| Terra high | 6/11 | 54.5% | 58.9s | 572K | 10.0K | $1.58 | $0.26 |\n| Terra xhigh | 9/12 | 75.0% | 57.3s | 1.86M | 14.2K | $4.86 | $0.54 |\n| Sol medium | 4/12 | 33.3% | 46.3s | 558K | 10.1K | $3.09 | $0.77 |\n\nTerra high had one transport/ghost failure, so it has 11 recorded runs rather than 12 scheduled samples; its rate and cost per verified edit use those recorded results.\n\n## Findings\n\n### Terra xhigh's selected-task executor result\n\nAcross these four selected TypeScript edit tasks under the documented local setup, Terra xhigh and Luna xhigh each recorded 9/12 verified edits. Terra xhigh's reported totals were 72% fewer input tokens, 43% fewer output tokens, 28% less estimated list-price cost, and 29% less time than Luna xhigh. These descriptive results inform, but do not prove, the Terra xhigh executor assignment.\n\n### Luna remains useful, but not as the premium executor\n\nLuna xhigh recorded 7/8 in the broad sample and 9/12 in the selected-task sample. Luna high remains the Eco executor as a product judgment for that preset's lower-priced-family-member trade-off; these local runs do not establish a capability ceiling or production behavior.\n\n### Terra high's product assignment\n\nTerra high recorded 6/11 verified edits after one transport/ghost failure in the selected-task sample. Its planning and lower-stakes critic assignments are product judgments; this edit benchmark does not measure those roles.\n\n### Sol medium's product assignment\n\nSol medium recorded 4/12 verified edits in the selected-task sample and was faster with fewer reported input tokens than the other listed xhigh setups. Its `codex-medium` default-agent assignment and the Sol-family architecture assignments are product judgments because the benchmark does not measure those broader roles.\n\n### Higher effort is not automatically cheaper\n\nThe selected-task data show that Luna xhigh used more reported tokens than Luna high in this local setup. They do not establish a general cost rule for thinking effort; effort selection remains a product decision informed by model tier and role shape.\n\n## Resulting built-in profiles\n\n| Profile | Default | Executor | Planner | Critic | Architect |\n|---|---|---|---|---|---|\n| `codex-eco` | Terra high | Luna high | Terra medium | Terra high | Sol medium |\n| `codex-medium` | Sol medium | Terra xhigh | Terra high | Terra xhigh | Sol high |\n| `codex-pro` | Sol xhigh | Terra xhigh | Sol high | Sol xhigh | Sol max |\n| `opus-codex` | Claude Opus xhigh | Terra xhigh | Terra high | Terra xhigh | Sol xhigh |\n| `codex-opencodego` | Terra xhigh | DeepSeek V4 Pro | Kimi K2.6 | MiMo 2.5 Pro | Sol xhigh |\n| `fable-opus-codex` | Claude Fable high | Terra xhigh | Claude Opus medium | Claude Opus high | Sol xhigh |\n\n## Limitations\n\n- The benchmark measures four selected precise TypeScript source mutations in the repeated sample, not full-session planning, architecture, criticism, or default-agent quality.\n- The corpus is small and intentionally adversarial; the results are descriptive, not statistically significant or a proof of general superiority, production reliability, or stability.\n- Samples used a local OpenAI-compatible provider rather than OpenAI's production endpoint.\n- Terra high has 11 recorded runs because one of 12 scheduled samples ended in a transport/ghost failure.\n- Token accounting reflects the local transport and benchmark context construction. The provider recorded zero cost; displayed costs are rounded list-price estimates, not billing predictions.\n- Model behavior can change as provider snapshots are updated.\n\nThe raw JSON reports and conversation dumps were generated under `runs/gpt-5.6-local-2026-07-11/` and `runs/gpt-5.6-confirmation-2026-07-11/`, but are not committed. The committed tables support the displayed denominators and rounded comparisons, not reconstruction of unrounded token totals or list-price estimates.\n",
|
|
30
31
|
"grok-build-provider-design.md": "# Grok Build provider design\n\n## Status\n\nProposal for maintainer design review. This document intentionally does not add a bundled provider implementation. It records the product/API decisions that must be accepted before any Grok Build implementation PR should land.\n\nThis is not an authorization claim for xAI endpoints, not a final naming decision, not approval for a bundled-loading exception, and not trademark/display-name approval. Those items require explicit owner sign-off before implementation.\n\n## Required owner sign-off gates\n\nImplementation should remain blocked until the owner signs off on these gates:\n\n1. **Authorized use / ToS** — confirm that SKC may use `cli-chat-proxy.grok.com` and the xAI CLI OAuth public client from a third-party tool. A public OAuth client id is not proof that this use is authorized.\n2. **Bundled-loading trust boundary** — confirm whether a source-controlled bundled provider may load even when ordinary user extension discovery is disabled.\n3. **Public selector naming** — choose the stable provider selector prefix: `grok-cli`, `grok-build`, or another owner-selected id.\n4. **Trademark/display-name** — confirm whether SKC may present the provider/profile using `Grok Build` or should use a more neutral owner-approved label.\n\nIf gate 1 is not accepted, the Grok Build provider implementation should not ship against `cli-chat-proxy.grok.com`. The fallback direction would be a documented user-supplied xAI/API-key provider or a different officially authorized integration path.\n\n## Problem\n\nSKC can load third-party extensions, but the first-run interactive path needs a maintainer-owned decision before a bundled Grok Build provider can be accepted. The desired product flow is:\n\n```text\nskc -> /login -> OAuth -> Grok Build -> browser xAI login -> /model -> <provider-id>/grok-composer-2.5-fast\n```\n\nThe previously proposed implementation touched bundled extension loading, OAuth registration, model profiles, vendor code, usage reporting, and tests in one PR. That is too much surface for review without first agreeing on the provider contract and the owner sign-off gates above.\n\n## Goals\n\n- Keep Grok Build, if accepted, as a bundled provider extension rather than a workflow skill.\n- Preserve the existing four bundled workflow skills and four role agents.\n- Define the `/login` OAuth contract for an owner-approved display name, with `Grok Build` only as a candidate label.\n- Define the `/model` contract for `grok-composer-2.5-fast` without committing to the final selector prefix before owner sign-off.\n- Define the guardrails for any bundled provider that loads while ordinary extension discovery is disabled.\n- Keep credentials in the existing auth storage path; no tokens or user env values are checked into the repo.\n- Keep implementation PRs small enough for independent review, rejection, or rollback.\n\n## Non-goals\n\n- No new workflow command or `/skill` surface.\n- No automatic installation from npm or remote code at runtime.\n- No direct `packages/ai/src/models.json` edits.\n- No broad model-profile reshuffle.\n- No provider-specific secrets in source.\n- No claim that xAI has authorized this endpoint/client usage without owner review.\n\n## Candidate provider contract\n\nThese are candidate values for owner review, not final commitments:\n\n| Field | Candidate value | Decision status | Notes |\n| --- | --- | --- | --- |\n| Public provider id | `grok-cli` or `grok-build` | **Owner decision required** | See naming section below. |\n| Display name | `Grok Build` or owner-selected label | **Owner decision required** | Name shown in `/login` and UI surfaces; see trademark/display-name section below. |\n| Default model id | `grok-composer-2.5-fast` | Proposed | Full selector depends on final provider id. |\n| Secondary model id | `grok-build` | Proposed | Candidate for executor/architect roles if a profile is accepted. |\n| Base URL | `https://cli-chat-proxy.grok.com/v1` | **Authorized-use sign-off required** | Undocumented/private-looking endpoint; do not ship without owner approval. |\n| OAuth issuer | `https://auth.x.ai` | **Authorized-use sign-off required** | OIDC discovery must validate xAI-owned HTTPS endpoints. |\n| OAuth callback | loopback `127.0.0.1` | Proposed | Uses PKCE + state validation. |\n| API adapter | `grok-cli-responses` | Proposed internal name | Provider-specific stream adapter; not a new generic API shape. |\n| Env bypass | `GROK_CLI_OAUTH_TOKEN` | Optional follow-up | Local bypass only; no refresh or discovery guarantees. |\n\n## Authorized-use and ToS caveat\n\n`cli-chat-proxy.grok.com` and the xAI CLI OAuth public client appear to be designed for xAI/Grok CLI traffic. Reusing them from SKC may be technically possible but still unauthorized or contrary to xAI terms.\n\nBefore implementation, the owner should explicitly decide one of:\n\n- **Accept** — proceed with this integration after reviewing the legal/product risk.\n- **Defer** — keep this design document only; no code ships until authorization is clarified.\n- **Reject** — do not integrate against `cli-chat-proxy.grok.com`; use only an official public API path.\n\nImplementation PRs must not describe the public client id as a secret, but they also must not present it as authorization. Tests should avoid real tokens and should not require an xAI account.\n\n## Trademark/display-name caveat\n\n`Grok` and `xAI` are third-party marks. `Grok Build` may also imply an official xAI/Grok product relationship even when the integration is third-party. Before implementation, the owner should explicitly choose one of:\n\n- **Use `Grok Build`** — acceptable as the user-facing provider/profile label after trademark/product-risk review.\n- **Use a neutral label** — for example `xAI Grok`, `Grok OAuth`, or another owner-selected name that avoids implying official endorsement.\n- **Avoid built-in branding** — keep any Grok-specific naming only in user-provided configuration until authorization/branding is clarified.\n\nImplementation PRs should avoid lock-in language such as \"official\" unless there is explicit authorization. UI labels, profile names, docs, tests, and screenshots must all use the owner-approved label consistently.\n\n## OAuth behavior\n\nIf authorized-use is accepted, the OAuth implementation should use the existing custom OAuth provider path:\n\n1. The chosen provider id registers an OAuth provider using the owner-approved display name.\n2. `/login` calls the existing auth storage login path for that provider.\n3. The provider opens an xAI authorization URL using OIDC discovery, PKCE, `state`, and a loopback callback.\n4. The callback exchanges the authorization code for access and refresh tokens.\n5. Credentials are stored by the existing auth storage code path.\n6. Refresh uses the stored refresh token and validates the token endpoint origin.\n\nSecurity constraints:\n\n- OIDC `authorization_endpoint` and `token_endpoint` must be HTTPS and under owner-approved xAI hosts.\n- The callback server binds to loopback by default.\n- The callback must reject state mismatches.\n- Access and refresh tokens must not be logged, rendered, committed, or included in tests.\n- Error messages may include status and provider error text, but not credential values.\n- Env overrides for base URL, scope, callback host, or client id must be treated as local developer/debug escape hatches, not default product behavior.\n\n## Bundled-loading trust boundary\n\nA bundled provider is different from ordinary user extension discovery, but loading it while `disableExtensionDiscovery: true` still expands the bootstrap trust boundary. Owner sign-off is required before implementation.\n\nMinimum guardrails if accepted:\n\n- Load only source-controlled, maintainer-reviewed bundled provider paths.\n- Use a static allowlist or exported enumerator; never scan arbitrary user directories for this path.\n- Do not install, fetch, or resolve remote package code at runtime.\n- Keep ordinary user extension discovery disabled when `disableExtensionDiscovery: true`; the exception is only for bundled provider defaults.\n- Add tests proving bundled providers load before model selection and caller-supplied `additionalExtensionPaths` still coexist.\n- Keep this bootstrap change separate from the Grok vendor implementation so it can be reviewed independently.\n\nAlternatives the owner may choose:\n\n- Do not load bundled providers when extension discovery is disabled; require explicit setup/defaults install.\n- Gate bundled provider loading behind a setting or compile-time default.\n- Allow bundled loading only in packaged builds, not arbitrary source checkouts.\n\n## Provider selector naming\n\nThe selector prefix is a stable user-facing contract and must be chosen before implementation.\n\n| Option | Example selector | Pros | Cons |\n| --- | --- | --- | --- |\n| `grok-cli` | `grok-cli/grok-composer-2.5-fast` | Matches the upstream CLI/proxy lineage and existing prototype. | User-facing name is less aligned with `Grok Build`; may expose implementation detail. |\n| `grok-build` | `grok-build/grok-composer-2.5-fast` | Matches UI label and requested product wording. | Diverges from existing prototype and env names; migration needed if prototypes used `grok-cli`. |\n| Owner-selected third id | `<id>/grok-composer-2.5-fast` | Lets maintainers align with broader provider taxonomy. | Requires updating all docs/tests before implementation. |\n\nUntil this is decided, implementation docs and PRs should use `<provider-id>` when describing the public selector. Internal adapter names may still use `grok-cli-responses` if maintainers accept that as an implementation detail.\n\n## Model/profile behavior\n\nModel registration should be provider-owned. If accepted, the provider should register at least:\n\n- `grok-composer-2.5-fast`\n- `grok-build`\n\nA built-in profile is optional and should be reviewed separately. If accepted, a candidate profile is:\n\n```text\ngrok-pro.default -> <provider-id>/grok-composer-2.5-fast\ngrok-pro.planner -> <provider-id>/grok-composer-2.5-fast\ngrok-pro.critic -> <provider-id>/grok-composer-2.5-fast\ngrok-pro.executor -> <provider-id>/grok-build\ngrok-pro.architect -> <provider-id>/grok-build\n```\n\nIf maintainers prefer not to add a built-in profile, the provider can still satisfy the core `/login` and `/model` flow through direct model selection.\n\n## Usage reporting behavior\n\nUsage reporting should be an optional follow-up after login/model support lands:\n\n- Provider id: the owner-selected `<provider-id>`.\n- Fetches usage with the effective OAuth access token.\n- Returns `null` when no token is available.\n- Does not require the usage provider for chat/model selection to work.\n- Should be skipped entirely if the authorized-use gate is not accepted.\n\n## Staged PR plan\n\n### PR 1: this design document\n\nPurpose: agree on caveats, owner sign-off gates, provider id, OAuth contract, bundled-loading trust boundary, model selector, security boundaries, and implementation split.\n\n### PR 2: bundled provider bootstrap contract\n\nSmall core change only, after owner sign-off on the bundled-loading gate:\n\n- Add a maintainer-owned way to enumerate bundled provider extension paths.\n- Load those paths during session/bootstrap only under the accepted guardrails.\n- Add tests proving bundled providers and caller-supplied extension paths coexist.\n\nNo Grok vendor implementation in this PR.\n\n### PR 3: Grok Build provider extension\n\nProvider implementation only, after owner sign-off on authorized use, public selector naming, and trademark/display-name:\n\n- Add bundled Grok Build provider source.\n- Register the chosen provider id, OAuth provider, and models.\n- Include sanitize and provider-specific stream handling.\n- Test `/login` provider registration and `grok-composer-2.5-fast` model availability.\n\n### PR 4: profile and model defaults\n\nOptional product-surface PR:\n\n- Add `grok-pro` only if maintainers accept a built-in profile.\n- Add model profile catalog tests.\n\n### PR 5: usage reporting\n\nOptional observability PR:\n\n- Add usage provider for the owner-selected provider id.\n- Add focused usage tests.\n\n## Acceptance criteria for the implementation series\n\n- Owner sign-off is recorded for authorized use, bundled loading, selector naming, and trademark/display-name before implementation lands.\n- Fresh checkout test proves `createAgentSession` registers the bundled provider under the accepted bootstrap rules.\n- `/login` includes the owner-approved display name for the owner-selected provider id.\n- `/model` includes `<provider-id>/grok-composer-2.5-fast`.\n- A real OAuth URL redirects to the owner-approved xAI account login page.\n- Third-party extension paths still load alongside bundled providers when configured.\n- Token values never appear in tests, logs, checked-in docs, or git history.\n\n## Open maintainer decisions\n\n- Is using `cli-chat-proxy.grok.com` plus the xAI CLI OAuth client from SKC authorized and acceptable for this project?\n- Should bundled provider defaults load while `disableExtensionDiscovery: true`, and under which guardrails?\n- Should the final public provider id be `grok-cli`, `grok-build`, or another id?\n- May SKC use `Grok Build` as the display/profile name, or should the integration use a neutral owner-selected label?\n- Should `grok-pro` be a built-in profile or documented as a user profile?\n- Should usage reporting be included in the initial provider PR or kept as a separate follow-up?",
|
|
31
32
|
"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 write a timestamped `handoff-*.md` artifact under the session artifacts directory when `compaction.handoffSaveToDisk` is enabled. Manual `/handoff` does not write that 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## 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 write a timestamped `handoff-*.md` artifact when `compaction.handoffSaveToDisk` is enabled; write failure is logged and does not fail the handoff.\n",
|
|
32
|
-
"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 list sessions, start worktree/tmux-oriented sessions, queue bounded follow-up prompts, read status/tail/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`, so spawned sessions launch inside a SKC-managed sibling worktree while SKC still records the original repo as the project identity for tmux/session resume. Users who need a stable named branch can set `--worktree-name`; users who need a specific local wrapper, dev checkout, or provider/model can opt in explicitly:\n\n```bash\nskc setup hermes \\\n --root /path/to/repo \\\n --worktree-name hermes-sayknow-cli\n```\n\n```bash\nskc setup hermes \\\n --root /path/to/repo \\\n --session-command \"skc --worktree hermes-custom --model <provider/model>\"\n```\n\nProvider/model examples are examples only; SKC does not hard-code GPT, Anthropic, or any other provider as the Hermes bridge default.\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 must also include `allow_mutation: true`. Missing startup opt-in or missing per-call consent returns an error instead of falling back to shell or terminal relay.\n\nReal tmux/SKC actuation uses the configured SKC-compatible session command. `skc setup hermes` writes this as `skc --worktree` by default so SKC owns worktree creation and resume identity:\n\n```bash\nexport SKC_COORDINATOR_MCP_SESSION_COMMAND=\"skc --worktree\"\n```\n\nWith that command configured, `skc_coordinator_start_session` launches a detached tmux session, `skc_coordinator_send_prompt` creates a durable turn and sends input to that pane, `skc_coordinator_read_coordination_status` returns a canonical polling snapshot for sessions, session states, turns, questions, reports, and bounded event summaries, and `skc_coordinator_read_tail` reads bounded advisory pane output. Tmux tail parsing is not the completion source of truth; turn completion comes from explicit durable turn state such as runtime session state or `skc_coordinator_report_status`.\n\nFor resume safety, prefer the generated SKC-native worktree command 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\nWhen an operator needs the session to stay visible in a routed tmux pane (for example a Clawhip/Hermes/OpenClaw channel that watches stale sessions and accepts follow-up prompts), use the documented visible-session fallback instead of inventing a private terminal protocol: [`docs/skc-session-clawhip-routing.md`](./skc-session-clawhip-routing.md). It keeps the same worktree isolation discipline while making the router, not SKC internals, own channel ids, mentions, and notification policy.\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) a session and sends one workflow-tagged turn that runs `/skill:ralplan`, `/skill:ultragoal`, or `/skill:team` to completion, 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 `cwd` and `task`; set `allow_mutation: true` only with startup mutation opt-in plus per-call consent. Prefer these over manual `start_session` + `send_prompt` when delegating a whole workflow.\n\n\n`skc_coordinator_register_session` registers an existing visible tmux-backed SKC pane as the coordinator-authoritative session. Use it when an operator has already launched a visible terminal/tmux lane and the external coordinator must send prompts to that same pane instead of creating a hidden `skc-coordinator-*` session. The tool validates the workdir allowlist, safe session/target tokens, and tmux target liveness before writing session state.\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`.\n2. Call `skc_coordinator_send_prompt` with `allow_mutation: true`.\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. If `skc_coordinator_list_questions` shows a question for that turn, answer with `skc_coordinator_submit_question_answer`.\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` preserves the legacy `queued` and `delivered` fields and adds turn fields:\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 kill the underlying tmux 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 plus advisory pane status:\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 \"live\": true,\n \"state\": \"idle_or_unknown\"\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 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.\n",
|
|
33
|
+
"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 list sessions, start worktree/tmux-oriented sessions, queue bounded follow-up prompts, read status/tail/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`, so spawned sessions launch inside a SKC-managed sibling worktree while SKC still records the original repo as the project identity for tmux/session resume. Users who need a stable named branch can set `--worktree-name`; users who need a specific local wrapper, dev checkout, or provider/model can opt in explicitly:\n\n```bash\nskc setup hermes \\\n --root /path/to/repo \\\n --worktree-name hermes-sayknow-cli\n```\n\n```bash\nskc setup hermes \\\n --root /path/to/repo \\\n --session-command \"skc --worktree hermes-custom --model <provider/model>\"\n```\n\nProvider/model examples are examples only; SKC does not hard-code GPT, Anthropic, or any other provider as the Hermes bridge default.\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 must also include `allow_mutation: true`. Missing startup opt-in or missing per-call consent returns an error instead of falling back to shell or terminal relay.\n\nReal tmux/SKC actuation uses the configured SKC-compatible session command. `skc setup hermes` writes this as `skc --worktree` by default so SKC owns worktree creation and resume identity:\n\n```bash\nexport SKC_COORDINATOR_MCP_SESSION_COMMAND=\"skc --worktree\"\n```\n\nWith that command configured, `skc_coordinator_start_session` launches a detached tmux session, `skc_coordinator_send_prompt` creates a durable turn and sends input to that pane, `skc_coordinator_read_coordination_status` returns a canonical polling snapshot for sessions, session states, turns, questions, reports, and bounded event summaries, and `skc_coordinator_read_tail` reads bounded advisory pane output. Tmux tail parsing is not the completion source of truth; turn completion comes from explicit durable turn state such as runtime session state or `skc_coordinator_report_status`.\n\nFor resume safety, prefer the generated SKC-native worktree command 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\nWhen an operator needs the session to stay visible in a routed tmux pane (for example a Clawhip/Hermes/OpenClaw channel that watches stale sessions and accepts follow-up prompts), use the documented visible-session fallback instead of inventing a private terminal protocol: [`docs/skc-session-clawhip-routing.md`](./skc-session-clawhip-routing.md). It keeps the same worktree isolation discipline while making the router, not SKC internals, own channel ids, mentions, and notification policy.\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) a session and sends one workflow-tagged turn that runs `/skill:ralplan`, `/skill:ultragoal`, or `/skill:team` to completion, 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 `cwd` and `task`; set `allow_mutation: true` 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\n`skc_coordinator_register_session` registers an existing visible tmux-backed SKC pane as the coordinator-authoritative session. Use it when an operator has already launched a visible terminal/tmux lane and the external coordinator must send prompts to that same pane instead of creating a hidden `skc-coordinator-*` session. The tool validates the workdir allowlist, safe session/target tokens, and tmux target liveness before writing session state.\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`.\n2. Call `skc_coordinator_send_prompt` with `allow_mutation: true`.\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. If `skc_coordinator_list_questions` shows a question for that turn, answer with `skc_coordinator_submit_question_answer`.\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` preserves the legacy `queued` and `delivered` fields and adds turn fields:\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 kill the underlying tmux 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 plus advisory pane status:\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 \"live\": true,\n \"state\": \"idle_or_unknown\"\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 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.\n",
|
|
33
34
|
"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",
|
|
34
|
-
"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 and use the same notation shown in the UI, such as `Ctrl+P`, `Alt+N`, `Alt+Shift+P`, `Shift+Enter`, and `Ctrl+Backspace`.\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 win32) | Explicitly queue a message for the next turn |\n| `app.message.dequeue` | `Alt+Up` | Dequeue a queued message back into the editor |\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\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 native Windows terminals, SKC defaults `app.message.queue` to `Alt+Q` because 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\nIn the main SKC composer, plain `PageUp` / `PageDown` page the visible transcript viewport instead of browsing prompt history; 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 | Notes |\n| --- | --- | --- |\n| `app.interrupt` | `escape` | |\n| `app.clear` | `ctrl+c` | |\n| `app.exit` | `ctrl+d` | |\n| `app.suspend` | `ctrl+z` | |\n| `app.thinking.cycle` | `shift+tab` | |\n| `app.thinking.toggle` | `ctrl+t` | |\n| `app.commandPalette.open` | `ctrl+p` | Open command palette from the editor |\n| `app.model.cycleForward` | `alt+n` | |\n| `app.model.cycleBackward` | `alt+shift+n` | |\n| `app.model.select` | `ctrl+l` | |\n| `app.model.selectTemporary` | `alt+p` | |\n| `app.tools.expand` | `ctrl+o` | |\n| `app.tool.backgroundFold` | `ctrl+b` | |\n| `app.editor.external` | `ctrl+g` | |\n| `app.message.followUp` | _(none)_ | `Ctrl+Enter` remains newline unless the user explicitly remaps this action; while idle the chord still falls through to newline |\n| `app.message.queue` | `alt+enter` (`alt+q` on win32) | platform-aware; avoids the Windows Terminal fullscreen shortcut |\n| `app.message.dequeue` | `alt+up` | |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on win32) | platform-aware; single source of truth in `KEYBINDINGS` |\n| `app.clipboard.copyLine` | `alt+shift+l` | registry-backed via input-controller custom handler |\n| `app.clipboard.copyPrompt` | `alt+shift+c` | |\n| `app.session.new` | _(none)_ | command-driven; empty default |\n| `app.session.tree` | _(none)_ | command-driven; empty default |\n| `app.session.fork` | _(none)_ | command-driven; empty default |\n| `app.session.resume` | _(none)_ | command-driven; empty default |\n| `app.session.observe` | `ctrl+s` | also `app.session.toggleSort` (session list) |\n| `app.jobs.open` | `alt+j` | |\n| `app.session.togglePath` | `ctrl+p` | session-list context |\n| `app.session.toggleSort` | `ctrl+s` | session-list context |\n| `app.session.rename` | `ctrl+r` | also `app.history.search` |\n| `app.session.delete` | `ctrl+d` | session-list context |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | |\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | |\n| `app.plan.toggle` | `alt+shift+p` | |\n| `app.history.search` | `ctrl+r` | |\n| `app.stt.toggle` | `alt+h` | |\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",
|
|
35
|
+
"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 and use the same notation shown in the UI, such as `Ctrl+P`, `Alt+N`, `Alt+Shift+P`, `Shift+Enter`, and `Ctrl+Backspace`.\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 win32) | Explicitly queue a message for the next turn |\n| `app.message.dequeue` | `Alt+Up` | Dequeue a queued message back into the editor |\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 native Windows terminals, SKC defaults `app.message.queue` to `Alt+Q` because 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\nIn the main SKC composer, plain `PageUp` / `PageDown` page the visible transcript viewport instead of browsing prompt history; 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 | Notes |\n| --- | --- | --- |\n| `app.interrupt` | `escape` | |\n| `app.clear` | `ctrl+c` | |\n| `app.exit` | `ctrl+d` | |\n| `app.suspend` | `ctrl+z` | |\n| `app.thinking.cycle` | `shift+tab` | |\n| `app.thinking.toggle` | `ctrl+t` | |\n| `app.commandPalette.open` | `ctrl+p` | Open command palette from the editor |\n| `app.model.cycleForward` | `alt+n` | |\n| `app.model.cycleBackward` | `alt+shift+n` | |\n| `app.model.select` | `ctrl+l` | |\n| `app.model.selectTemporary` | `alt+p` | |\n| `app.tools.expand` | `ctrl+o` | |\n| `app.tool.backgroundFold` | `ctrl+b` | |\n| `app.editor.external` | `ctrl+g` | |\n| `app.message.followUp` | _(none)_ | `Ctrl+Enter` remains newline unless the user explicitly remaps this action; while idle the chord still falls through to newline |\n| `app.message.queue` | `alt+enter` (`alt+q` on win32) | platform-aware; avoids the Windows Terminal fullscreen shortcut |\n| `app.message.dequeue` | `alt+up` | |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on win32) | platform-aware; single source of truth in `KEYBINDINGS` |\n| `app.clipboard.copyLine` | `alt+shift+l` | registry-backed via input-controller custom handler |\n| `app.clipboard.copyPrompt` | `alt+shift+c` | |\n| `app.session.new` | _(none)_ | command-driven; empty default |\n| `app.session.tree` | _(none)_ | command-driven; empty default |\n| `app.session.fork` | _(none)_ | command-driven; empty default |\n| `app.session.resume` | _(none)_ | command-driven; empty default |\n| `app.session.observe` | `ctrl+s` | also `app.session.toggleSort` (session list) |\n| `app.jobs.open` | `alt+j` | |\n| `app.session.togglePath` | `ctrl+p` | session-list context |\n| `app.session.toggleSort` | `ctrl+s` | session-list context |\n| `app.session.rename` | `ctrl+r` | also `app.history.search` |\n| `app.session.delete` | `ctrl+d` | session-list context |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | |\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | |\n| `app.plan.toggle` | `alt+shift+p` | |\n| `app.history.search` | `ctrl+r` | |\n| `app.stt.toggle` | `alt+h` | |\n| `app.irc.sidebar.toggle` | `alt+i` | |\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",
|
|
35
36
|
"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 available — checked in project-local bin directories first (e.g., `node_modules/.bin/`, `.venv/bin/`), then `$PATH`.\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| 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`). Files are merged in order: higher-priority files override lower-priority fields for the same server. Servers not mentioned in any override file remain at their built-in defaults.\n\n**Recommended locations:**\n\n- User-wide preferences → `~/.skc/agent/lsp.json`\n- Project-specific overrides → `<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 all servers that have matching `rootMarkers`, an available binary, and are not explicitly `disabled`.\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` | yes | Binary name (resolved via PATH/local bins) or absolute path |\n| `args` | `string[]` | no | Arguments passed to the binary |\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` | no | Sent as `initializationOptions` during LSP handshake |\n| `settings` | `object` | no | 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\n\nPartial overrides are merged onto the built-in defaults. You only need to specify the fields you want to change.\n\n```json\n{\n \"servers\": {\n \"typescript-language-server\": {\n \"args\": [\"--stdio\", \"--log-level\", \"4\"]\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\nNew servers require `command`, `fileTypes`, and `rootMarkers`. All other fields are optional.\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## 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",
|
|
36
37
|
"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",
|
|
37
38
|
"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\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```\n\nPresets only write `models.yml` entries that reference documented environment variable names (`MINIMAX_CODE_API_KEY`, `MINIMAX_CODE_CN_API_KEY`, or `ZAI_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.\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-4-8`\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` tier favors cheaper/faster defaults, `medium` matches normal production defaults, and `pro` raises reasoning for architect, critic, and planner roles. Effort suffixes are clamped to each model's supported thinking range at preview and activation time (for example `codex-eco`'s executor `:minimal` resolves to effective `low` on `gpt-5.5`). 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`, or `lm-studio`\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`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### 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## 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 Grok and zAI).\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",
|
|
@@ -48,7 +49,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
48
49
|
"natives-text-search-pipeline.md": "# Natives Text/Search Pipeline\n\nThis document maps the `@sayknow-cli/natives` text/search/code surface from generated JS/TS exports to Rust N-API modules and back to JS result objects.\n\nTerminology follows `docs/natives-architecture.md`:\n\n- **Generated binding**: public API in `packages/natives/native/index.d.ts`.\n- **Rust module layer**: N-API exports in `crates/pi-natives/src/*`.\n- **Shared scan cache**: `fs_cache`-backed directory-entry cache used by discovery/search flows.\n\n## Implementation files\n\n- `packages/natives/native/index.d.ts`\n- `crates/pi-natives/src/grep.rs`\n- `crates/pi-natives/src/glob.rs`\n- `crates/pi-natives/src/glob_util.rs`\n- `crates/pi-natives/src/fs_cache.rs`\n- `crates/pi-natives/src/fd.rs`\n- `crates/pi-natives/src/ast.rs`\n- `crates/pi-natives/src/text.rs`\n- `crates/pi-natives/src/highlight.rs`\n- `crates/pi-natives/src/tokens.rs`\n\n## JS API ↔ Rust export mapping\n\n| JS API | Rust export (`#[napi]`, snake_case -> camelCase) | Rust module |\n| ------------------------------------------------------------------------------- | ------------------------------------------------ | -------------- |\n| `grep(options, onMatch?)` | `grep` | `grep.rs` |\n| `search(content, options)` | `search` | `grep.rs` |\n| `hasMatch(content, pattern, ignoreCase?, multiline?)` | `hasMatch` | `grep.rs` |\n| `fuzzyFind(options)` | `fuzzyFind` | `fd.rs` |\n| `glob(options, onMatch?)` | `glob` | `glob.rs` |\n| `invalidateFsScanCache(path?)` | `invalidateFsScanCache` | `fs_cache.rs` |\n| `astGrep(options)` | `astGrep` | `ast.rs` |\n| `astEdit(options)` | `astEdit` | `ast.rs` |\n| `wrapTextWithAnsi(text, width, tabWidth)` | `wrapTextWithAnsi` | `text.rs` |\n| `truncateToWidth(text, maxWidth, ellipsis, pad, tabWidth)` | `truncateToWidth` | `text.rs` |\n| `sliceWithWidth(line, startCol, length, strict, tabWidth)` | `sliceWithWidth` | `text.rs` |\n| `extractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, tabWidth)` | `extractSegments` | `text.rs` |\n| `visibleWidth(text, tabWidth)` | `visibleWidth` | `text.rs` |\n| `highlightCode(code, lang, colors)` | `highlightCode` | `highlight.rs` |\n| `supportsLanguage(lang)` | `supportsLanguage` | `highlight.rs` |\n| `getSupportedLanguages()` | `getSupportedLanguages` | `highlight.rs` |\n| `countTokens(input, encoding?)` | `countTokens` | `tokens.rs` |\n\n## Pipeline overview by subsystem\n\n## 1) Regex search (`grep`, `search`, `hasMatch`)\n\n### Input/options flow\n\n1. Callers invoke generated native exports directly; there is no package-local TS wrapper that renames `search` to `searchContent`.\n2. Rust option structs in `grep.rs` deserialize camelCase fields (`ignoreCase`, `maxCount`, `contextBefore`, `contextAfter`, `maxColumns`, `timeoutMs`).\n3. `grep` creates `CancelToken` from `timeoutMs` + `AbortSignal` and runs inside `task::blocking(\"grep\", ...)`.\n4. `search` and `hasMatch` operate on provided string/`Uint8Array` content and do not scan the filesystem.\n\n### Execution branches\n\n- **In-memory branch**\n - `search` -> `search_sync` / search helpers over provided content bytes.\n - `hasMatch` compiles/checks pattern against provided content and returns a boolean.\n - No filesystem scan, no `fs_cache`.\n- **Single-file branch**\n - `grep` resolves path, checks metadata is file, and searches that file.\n- **Directory branch**\n - Optional cache lookup via `fs_cache::get_or_scan` when `cache: true`.\n - Fresh scan via `fs_cache::force_rescan` when `cache: false`.\n - Optional empty-result recheck when cached results are older than the empty-result recheck threshold.\n - Entry filtering: file-only + optional glob filter (`glob_util`) + optional type filter mapping (`js`, `ts`, `rust`, etc.).\n\n### Search/collection semantics\n\n- Regex engine: `grep_regex::RegexMatcherBuilder` with `ignoreCase` and `multiline`.\n- Context resolution:\n - `contextBefore/contextAfter` override legacy `context`.\n - Non-content modes do not collect context.\n- Output modes:\n - `content` -> one `GrepMatch` per hit.\n - `count` and `filesWithMatches` map to count-style entries (`lineNumber=0`, `line=\"\"`, `matchCount` set).\n- Limits:\n - Global `offset` and `maxCount` apply across files.\n - Parallel path is used only when `maxCount` is unset and `offset == 0`; otherwise sequential path preserves deterministic global offset/limit semantics.\n\n### Result shaping back to JS\n\n- Rust `SearchResult`/`GrepResult` fields map to TS interfaces via N-API object conversion.\n- Counters are clamped before crossing N-API where needed.\n- `GrepResult.limitReached` is optional and emitted when true.\n- Streaming callback receives each shaped `GrepMatch` for content or count-style entries.\n\n### Failure behavior\n\n- `search` returns `SearchResult.error` for regex/search failures instead of throwing.\n- `grep` rejects on hard errors such as invalid path, invalid glob/regex, or cancellation timeout/abort.\n- `hasMatch` returns a boolean on success and throws on invalid pattern/UTF-8 conversion errors.\n- File open/search errors in multi-file scans are skipped per-file; scan continues.\n\n### Malformed regex handling\n\n`grep.rs` sanitizes braces before regex compile:\n\n- Invalid repetition-like braces are escaped (`{`/`}` -> `\\{`/`\\}`) when they cannot form `{N}`, `{N,}`, `{N,M}`.\n- This prevents common literal-template fragments (for example `${platform}`) from failing as malformed repetition.\n- Remaining invalid regex syntax still returns a regex error.\n\n## 2) File discovery (`glob`) and fuzzy path search (`fuzzyFind`)\n\n`glob` and `fuzzyFind` share `fs_cache` scans; matching logic differs.\n\n### `glob` flow\n\n1. Caller passes `GlobOptions` directly. `pattern` and `path` are required in the generated type.\n2. Rust resolves the search path and compiles pattern via `glob_util::compile_glob`.\n3. Entry source:\n - `cache=true` -> `get_or_scan` + optional stale-empty `force_rescan`.\n - `cache=false` -> `force_rescan(..., store=false)` (fresh only).\n4. Filtering:\n - skip `.git` always;\n - skip `node_modules` unless requested (`includeNodeModules`) or pattern mentions `node_modules`;\n - apply glob match;\n - apply file-type filter; symlink `file`/`dir` filters resolve target metadata.\n5. Optional sort by mtime descending (`sortByMtime`) before truncating to `maxResults`.\n\n### `fuzzyFind` flow\n\n1. Rust implementation lives in `fd.rs`; generated export is `fuzzyFind`.\n2. Shared scan source from `fs_cache` with the same cache/no-cache split and stale-empty recheck policy.\n3. Scoring:\n - exact / starts-with / contains / subsequence-based fuzzy score;\n - separator/punctuation-normalized scoring path;\n - directory bonus and deterministic tie-break (`score desc`, then `path asc`).\n4. Symlink entries are excluded from fuzzy results.\n\n### Failure behavior\n\n- Invalid glob pattern returns an error from `glob_util::compile_glob`.\n- Search root must resolve to an existing directory for directory discovery flows.\n- Cancellation/timeouts propagate as abort errors via `CancelToken::heartbeat()` checks in loops.\n\n### Malformed glob handling\n\n`glob_util::build_glob_pattern` is tolerant:\n\n- normalizes `\\` to `/`,\n- auto-prefixes simple recursive patterns with `**/` when `recursive=true`,\n- auto-closes unbalanced `{...` alternation groups before compile.\n\n## 3) AST search/edit (`astGrep`, `astEdit`)\n\n`ast.rs` exposes syntax-aware code search and rewrite operations.\n\n- `astGrep(options)` returns matches with byte/line/column coordinates and optional metavariable bindings.\n- `astEdit(options)` returns replacement changes, per-file counts, searched/touched file counts, parse errors, and whether edits were applied.\n- `dryRun` defaults to true for edit options in the generated documentation.\n- Options include language override, path/glob/selector, strictness, limits, parse-error policy, `signal`, and `timeoutMs`.\n\nThese exports are direct native APIs used by tooling; they are not mediated by a TS wrapper in `packages/natives`.\n\n## 4) Shared scan/cache lifecycle (`fs_cache`)\n\n`fs_cache` stores scan results as normalized relative entries (`path`, `fileType`, optional `mtime`) keyed by:\n\n- canonical search root,\n- `include_hidden`,\n- `use_gitignore`.\n\n### Cache state transitions\n\n1. **Miss / disabled**\n - TTL is `0` or key absent/expired -> fresh collection.\n2. **Hit**\n - Entry age is within TTL -> return cached entries + `cache_age_ms`.\n3. **Stale-empty recheck**\n - If query yields zero matches and cache age exceeds the empty-result threshold, force one rescan.\n4. **Invalidation**\n - `invalidateFsScanCache(path?)`:\n - no arg: clear all keys;\n - path arg: remove keys for roots affected by that path.\n\n### Stale-result tradeoff\n\n- Cache favors low-latency repeated scans over immediate consistency.\n- TTL window can return stale positives/negatives.\n- Empty-result recheck reduces stale negatives for older cached scans at the cost of one extra scan.\n- Explicit invalidation is the intended correctness hook after file mutations.\n\n## 5) ANSI text utilities (`text`)\n\nThese are pure, in-memory utilities.\n\n### Boundaries and responsibilities\n\n- `text.rs` owns terminal-cell semantics:\n - ANSI sequence parsing,\n - grapheme-aware width and slicing,\n - wrap/truncate/sanitize behavior,\n - explicit tab-width parameter on width-sensitive APIs.\n- `grep.rs` line truncation (`maxColumns`) is separate:\n - simple character-boundary truncation of matched lines with `...`,\n - not ANSI-state-preserving and not terminal-cell width aware.\n\n### Key behaviors\n\n- `wrapTextWithAnsi`: wraps by visible width, carries active SGR codes across wrapped lines.\n- `truncateToWidth`: visible-cell truncation with ellipsis policy (`Unicode`, `Ascii`, `Omit`), optional right padding.\n- `sliceWithWidth`: column slicing with optional strict width enforcement.\n- `extractSegments`: extracts before/after segments around an overlay while restoring ANSI state for the `after` segment.\n- `sanitizeText` (ANSI/control/surrogate stripping with line-ending normalization) no longer lives in `text.rs`; it moved to `@sayknow-cli/utils` as a pure-JS implementation in `packages/utils/src/sanitize-text.ts`. The native binding was removed in the same change because the JS version was competitive on the benchmarked workloads, and keeping a Rust copy forced every caller (including `pi-utils`) to pull in `@sayknow-cli/natives`.\n- `visibleWidth`: counts visible terminal cells using caller-supplied tab width.\n\n### Failure behavior\n\nText functions generally return deterministic transformed output; errors are limited to N-API argument/string conversion boundaries.\n\n## 6) Syntax highlighting (`highlight`)\n\n`highlight.rs` is pure transformation; it does not use the filesystem scan cache.\n\n### Flow\n\n1. Caller passes `code`, optional `lang`, and ANSI color palette.\n2. Rust resolves syntax by token/name lookup, extension lookup, alias table fallback, then plain-text fallback.\n3. Each line is parsed with syntect `ParseState` and scope stack.\n4. Scopes map to semantic color categories and ANSI color codes are injected/reset.\n\n### Failure behavior\n\n- Per-line parse failure does not fail the call: that line is appended unhighlighted and processing continues.\n- Unknown/unsupported language falls back to plain text syntax.\n\n## 7) Token counting (`tokens`)\n\n`countTokens(input, encoding?)` is an in-memory utility.\n\n- `input` may be a single string or an array of strings.\n- Arrays return one aggregate count and are encoded in parallel in Rust.\n- Default encoding is `O200kBase`; `Cl100kBase` remains available as a compatibility alias routing to `o200k_base` in default builds.\n- The implementation uses ordinary tokenization, not special-token handling.\n\n## Pure utility vs filesystem-dependent flows\n\n| Flow | Filesystem access | Shared cache | Notes |\n| ---------------------------- | ----------------- | -------------------- | --------------------------------------------- |\n| `search` / `hasMatch` | No | No | regex on provided bytes/string only |\n| `text` module functions | No | No | ANSI/width/sanitization only |\n| `highlight` module functions | No | No | syntax + ANSI coloring only |\n| `countTokens` | No | No | tokenization only |\n| `astGrep` / `astEdit` | Yes | No | syntax-aware file search/edit |\n| `glob` | Yes | Optional | directory scans + glob filtering |\n| `fuzzyFind` | Yes | Optional | directory scans + fuzzy scoring |\n| `grep` (file/dir path) | Yes | Optional in dir mode | ripgrep over files, optional filters/callback |\n\n## End-to-end lifecycle summary\n\n1. Caller invokes generated native export with typed options.\n2. Rust validates/normalizes options and builds matcher/search config.\n3. For filesystem flows, entries are scanned (cache hit/miss/rescan where applicable) then filtered/scored/searched.\n4. Worker loops periodically call cancel heartbeat; timeout/abort can terminate execution.\n5. Rust shapes outputs into N-API objects (`lineNumber`, `matchCount`, `limitReached`, etc.).\n6. Generated bindings return typed JS objects and optional per-match callbacks for `grep`/`glob`.\n",
|
|
49
50
|
"non-compaction-retry-policy.md": "# Non-compaction auto-retry policy\n\nThis document describes the standard API-error retry path in `AgentSession`.\n\nIt explicitly excludes context-overflow recovery via auto-compaction. Overflow is handled by compaction logic and is documented separately in [`compaction.md`](../docs/compaction.md).\n\n## Implementation files\n\n- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)\n- [`../src/config/settings-schema.ts`](../packages/coding-agent/src/config/settings-schema.ts)\n- [`../src/modes/controllers/event-controller.ts`](../packages/coding-agent/src/modes/controllers/event-controller.ts)\n- [`../src/modes/rpc/rpc-mode.ts`](../packages/coding-agent/src/modes/rpc/rpc-mode.ts)\n- [`../src/modes/rpc/rpc-client.ts`](../packages/coding-agent/src/modes/rpc/rpc-client.ts)\n- [`../src/modes/rpc/rpc-types.ts`](../packages/coding-agent/src/modes/rpc/rpc-types.ts)\n\n## Scope boundary vs compaction\n\nRetry and compaction are checked from the same `agent_end` path, but they are intentionally separated:\n\n1. `agent_end` inspects the last assistant message.\n2. `#isRetryableError(...)` runs first.\n3. If retry is initiated, compaction checks are skipped for that turn.\n4. Context-overflow errors are hard-excluded from retry classification (`isContextOverflow(...)` short-circuits retry).\n5. Overflow therefore falls through to `#checkCompaction(...)` instead of standard retry.\n\nSo: overload/rate/server/network-style failures use this retry policy; context-window overflow uses compaction recovery.\n\n## Retry classification\n\n`#isRetryableError(...)` requires all of the following:\n\n- assistant `stopReason === \"error\"`\n- `errorMessage` exists\n- message is **not** context overflow\n- `errorMessage` matches transient transport/envelope patterns or `isUsageLimitError(...)`\n\nCurrent retryable inputs are regex/string-classified:\n\n- transient transport/envelope failures, including Anthropic stream-envelope failures before `message_start`\n- overloaded/provider-returned-error wording\n- rate limit / usage limit / too many requests\n- HTTP-like server classes: 429, 500, 502, 503, 504\n- service unavailable / server/internal error\n- provider-suggested retry wording, including OpenAI `retry your request` failures\n- network/connection/socket failures, refused/closed connections, upstream connect/reset-before-headers, socket hang up, timeout/timed out, fetch failed, terminated, retry delay wording, and unexpected socket close messages\n\nThis is string-pattern classification, not typed provider error codes.\n\n## Retry lifecycle and state transitions\n\nSession state used by retry:\n\n- `#retryAttempt: number` (`0` means idle)\n- `#retryPromise: Promise<void> | undefined` (tracks in-progress retry lifecycle)\n- `#retryResolve: (() => void) | undefined` (resolves `#retryPromise`)\n- `#retryAbortController: AbortController | undefined` (cancels backoff sleep)\n\nFlow (`#handleRetryableError`):\n\n1. Read `retry` settings group.\n2. If `retry.enabled === false`, stop immediately (`false`, no retry started).\n3. Increment `#retryAttempt`.\n4. Create `#retryPromise` once (first attempt in a chain).\n5. If attempt exceeded `retry.maxRetries`, emit final failure event and stop.\n6. Compute base delay: `retry.baseDelayMs * 2^(attempt-1)`.\n7. For usage-limit errors, parse retry hints and call auth storage (`markUsageLimitReached(...)`); if credential switching succeeds, force delay to `0`, otherwise use a larger retry-after/backoff hint when present.\n8. If no credential switch occurred, suppress the current model selector for cooldown, try configured retry model fallback chains, and force delay to `0` on model switch.\n9. Emit `auto_retry_start`.\n10. Remove the trailing assistant error message from agent runtime state (kept in persisted session history).\n11. Sleep with abort support.\n12. Schedule `agent.continue()` through the post-prompt task scheduler (`delayMs: 1`) for the same prompt generation.\n\n### What resets retry counters\n\n`#retryAttempt` resets to `0` in these cases:\n\n- first successful non-error, non-aborted assistant message after retries started (emits `auto_retry_end { success: true }`)\n- retry cancellation during backoff sleep\n- max retries exceeded path\n\n`#retryPromise` resolves/clears when retry chain ends (success, cancellation, or max-exceeded), via `#resolveRetry()`.\n\n## Backoff and max-attempt semantics\n\nSettings:\n\n- `retry.enabled` (default `true`)\n- `retry.maxRetries` (default `3`)\n- `retry.baseDelayMs` (default `2000`)\n- `retry.maxDelayMs` (default `300000`)\n- `retry.requestMaxRetries` (default `5`) — provider request retries before a stream is established; counts retries, not the initial request\n- `retry.streamMaxRetries` (default `5`) — provider stream replay retries for replay-safe transient stream failures; counts retries, not the initial stream attempt\n\nAttempt numbering:\n\n- attempt counter is incremented before max-check\n- start events use current attempt (1-based)\n- max-exceeded end event reports `attempt: this.#retryAttempt - 1` (last attempted retry count)\n\nBackoff sequence with default settings:\n\n- attempt 1: 2000 ms\n- attempt 2: 4000 ms\n- attempt 3: 8000 ms\n\nDelay override inputs can come from parsed retry headers (`retry-after-ms`, `retry-after`, `x-ratelimit-reset-ms`, `x-ratelimit-reset`) or usage-limit backoff. Credential/model fallback switches set delay to `0`; otherwise parsed hints can extend the exponential local delay.\n\n## Abort mechanics\n\n### Explicit retry abort\n\n`abortRetry()`:\n\n- aborts `#retryAbortController` (if present)\n- resolves retry promise (`#resolveRetry()`) so awaiters are unblocked\n\nIf abort hits while sleeping, catch path emits:\n\n- `auto_retry_end { success: false, finalError: \"Retry cancelled\" }`\n- resets attempt/controller\n\n### Global operation abort interaction\n\n`abort()` calls `abortRetry()` before aborting the active agent stream. This guarantees retry backoff is cancelled when user issues a general abort.\n\n### TUI interaction\n\nOn `auto_retry_start`, EventController:\n\n- swaps `Esc` handler to `session.abortRetry()`\n- renders loader text: `Retrying (attempt/maxAttempts) in Ns… (esc to cancel)`\n\nOn `auto_retry_end`, it restores prior `Esc` handler and clears loader state.\n\n## Streaming and prompt completion behavior\n\n`prompt()` ultimately waits on `#waitForRetry()` after `agent.prompt(...)` returns.\n\nEffect:\n\n- a prompt call does not fully resolve until any started retry chain finishes (success/failure/cancel)\n- retry lifecycle is part of one logical prompt execution boundary\n\nThis prevents callers from treating a retrying turn as complete too early.\n\n## Controls: settings and RPC\n\n### Configuration knobs\n\nDefined in settings schema under retry group:\n\n- `retry.enabled`\n- `retry.maxRetries`\n- `retry.baseDelayMs`\n- `retry.fallbackChains`\n- `retry.fallbackRevertPolicy` (`\"cooldown-expiry\"` by default; `\"never\"` disables automatic restoration)\n\nProgrammatic toggles in session:\n\n- `setAutoRetryEnabled(enabled)` writes `retry.enabled`\n- `autoRetryEnabled` reads `retry.enabled`\n- `isRetrying` reports whether retry lifecycle promise is active\n\n### RPC controls\n\nRPC command surface:\n\n- `set_auto_retry` → `session.setAutoRetryEnabled(command.enabled)`\n- `abort_retry` → `session.abortRetry()`\n\nClient helpers:\n\n- `RpcClient.setAutoRetry(enabled)`\n- `RpcClient.abortRetry()`\n\nBoth commands return success responses; retry progress/failure details come from streamed session events, not command response payloads.\n\n## Event emission and failure surfacing\n\nSession-level retry events:\n\n- `auto_retry_start { attempt, maxAttempts, delayMs, errorMessage }`\n- `auto_retry_end { success, attempt, finalError? }`\n- `retry_fallback_applied { from, to, role }`\n- `retry_fallback_succeeded { model, role }`\n\nPropagation:\n\n- emitted through `AgentSession.subscribe(...)`\n- forwarded to extension runner as extension events\n- in RPC mode, forwarded directly as JSON event objects (`session.subscribe(event => output(event))`)\n- in TUI, consumed by `EventController` for loader/error UI\n\nFinal failure surfacing:\n\n- On max-exceeded or cancellation, `auto_retry_end.success === false`\n- TUI shows: `Retry failed after N attempts: <finalError>`\n- Extensions/hooks receive `auto_retry_end` with same fields\n- RPC consumers receive same event object on stdout stream\n\n## Permanent stop conditions\n\nRetry stops and will not auto-continue when any of these occur:\n\n- `retry.enabled` is false\n- error is not retry-classified\n- error is context overflow (delegated to compaction path)\n- max retries exceeded\n- user cancels retry (`abort_retry` or `Esc` during retry loader)\n- global abort (`abort`) cancels retry first\n\nA new retry chain can still start later on a future retryable error after counters reset.\n\n## Operational caveats\n\n- Classification is regex text matching; provider-specific structured errors are not used here.\n- Retry strips the failing assistant error from **runtime context** before re-continue, but session history still keeps that error entry.\n- `RpcSessionState` currently exposes `autoCompactionEnabled` but not an `autoRetryEnabled` field; RPC callers must track their own toggle state or query settings through other APIs.\n- Model fallback changes append temporary `model_change` entries and may later restore the primary model when its cooldown expires, depending on `retry.fallbackRevertPolicy`.\n\n## Provider request/stream retry budgets\n\nThe provider budgets are deliberately separate from session auto-retry:\n\n```yaml\nretry:\n requestMaxRetries: 4\n streamMaxRetries: 100\n```\n\n`requestMaxRetries` maps to provider SDK/fetch retry counts for request setup failures such as retryable 5xx/408/429/network errors. `streamMaxRetries` maps to provider-specific stream replay loops that are safe to repeat without duplicating visible assistant output. Providers that cannot safely replay a stream continue to surface the terminal error so the session-level auto-retry layer can decide whether to retry the turn.\n\nFail-fast cases stay fail-fast: invalid credentials (after any credential-refresh path is exhausted), unsupported model/provider configuration, malformed requests, context overflow, explicit user aborts, and permanent quota failures are not treated as transient provider budget candidates.\n",
|
|
50
51
|
"notebook-tool-runtime.md": "# Notebook tool runtime internals\n\nThis document describes the current `notebook` tool implementation and its relationship to the kernel-backed Python runtime.\n\nThe critical distinction: **`notebook` is a JSON notebook editor, not a notebook executor**. It edits `.ipynb` cell sources directly; it does not start or talk to a Python kernel.\n\n## Implementation files\n\n- [`src/tools/notebook.ts`](../packages/coding-agent/src/tools/notebook.ts)\n- [`src/eval/py/executor.ts`](../packages/coding-agent/src/eval/py/executor.ts)\n- [`src/eval/py/kernel.ts`](../packages/coding-agent/src/eval/py/kernel.ts)\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts)\n- [`src/tools/eval.ts`](../packages/coding-agent/src/tools/eval.ts)\n\n## 1) Runtime boundary: editing vs executing\n\n## `notebook` tool (`src/tools/notebook.ts`)\n\n- Supports `action: edit | insert | delete` on a `.ipynb` file.\n- Resolves path relative to session CWD (`resolveToCwd`).\n- Loads notebook JSON, validates `cells` array, validates `cell_index` bounds.\n- Applies source edits in-memory and writes full notebook JSON back with `JSON.stringify(notebook, null, 1)`.\n- Returns textual summary + structured `details` (`action`, `cellIndex`, `cellType`, `totalCells`, `cellSource`).\n\nNo kernel lifecycle exists in this tool:\n\n- no gateway acquisition\n- no kernel session ID\n- no `execute_request`\n- no stream chunks from kernel channels\n- no rich display capture (`image/png`, JSON display, status MIME)\n\n## Notebook-like execution path (`src/tools/eval.ts` + `src/eval/py/*`)\n\nWhen the agent needs to run cell-style Python code (sequential cells, persistent state, rich displays), that goes through the **`eval` tool** with `language: \"python\"`, not `notebook`.\n\nThat path is where kernel modes, restart/cancel behavior, chunk streaming, and output artifact truncation live.\n\n## 2) Notebook cell handling semantics (`notebook` tool)\n\n## Source normalization\n\n`content` is split into `source: string[]` with newline preservation:\n\n- each non-final line keeps trailing `\\n`\n- final line has no forced trailing newline\n\nThis mirrors notebook JSON conventions and avoids accidental line concatenation on later edits.\n\n## Action behavior\n\n- `edit`\n - replaces `cells[cell_index].source`\n - preserves existing `cell_type`\n- `insert`\n - inserts at `[0..cellCount]`\n - `cell_type` defaults to `code`\n - code cells initialize `execution_count: null` and `outputs: []`\n - markdown cells initialize only `metadata` + `source`\n- `delete`\n - removes `cells[cell_index]`\n - returns removed `source` in details for renderer preview\n\n## Error surfaces\n\nHard failures are thrown for:\n\n- missing notebook file\n- invalid JSON\n- missing/non-array `cells`\n- out-of-range index (insert and non-insert have different valid ranges)\n- missing `content` for `edit`/`insert`\n\nThese become `Error:` tool responses upstream; renderer uses notebook path + formatted error text.\n\n## 3) Kernel session semantics (where they actually exist)\n\nKernel semantics are implemented in `executePython` / `PythonKernel` and apply to the Python backend of the `eval` tool.\n\n## Modes\n\n`PythonKernelMode`:\n\n- `session` (default)\n - kernels cached in `kernelSessions` map\n - max 4 sessions; oldest evicted on overflow\n - idle/dead cleanup every 30s, timeout after 5 minutes\n - per-session queue serializes execution (`session.queue`)\n- `per-call`\n - creates kernel for request\n - executes\n - always shuts down kernel in `finally`\n\n## Reset behavior\n\n`eval` passes `reset` only for the first cell in a multi-cell Python call; later cells always run with `reset: false`.\n\n## Kernel death / restart / retry\n\nIn session mode (`withKernelSession`):\n\n- dead kernel detected by heartbeat (`kernel.isAlive()` check every 5s) or execute failure.\n- pre-run dead state triggers `restartKernelSession`.\n- execute-time crash path retries once: restart kernel, rerun handler.\n- `restartCount > 1` in same session throws `Python kernel restarted too many times in this session`.\n\nStartup retry behavior:\n\n- shared gateway kernel creation retries once on `SharedGatewayCreateError` with HTTP 5xx.\n\nResource exhaustion recovery:\n\n- detects `EMFILE`/`ENFILE`/\"Too many open files\" style failures\n- clears tracked sessions\n- calls `shutdownSharedGateway()`\n- retries kernel session creation once\n\n## 4) Environment/session variable injection\n\nKernel startup receives the optional session file path from executor:\n\n- `SKC_SESSION_FILE` (session state file path)\n\n`PythonKernel.#initializeKernelEnvironment(...)` then runs init script inside kernel to:\n\n- `os.chdir(cwd)`\n- inject env entries into `os.environ`\n- prepend cwd to `sys.path` if missing\n\nImplication:\n\n- prelude helpers that read session context rely on this env var in Python process state.\n\n## 5) Streaming/chunk and display handling (kernel-backed path)\n\nThe kernel client processes Jupyter protocol messages per execution:\n\n- `stream` -> text chunk to `onChunk`\n- `execute_result` / `display_data` ->\n - display text chosen by MIME precedence: `text/markdown` > `text/plain` > converted `text/html`\n - structured outputs captured separately:\n - `application/json` -> `{ type: \"json\" }`\n - `image/png` -> `{ type: \"image\" }`\n - `application/x-skc-status` -> `{ type: \"status\" }` (no text emission)\n- `error` -> traceback text pushed to chunk stream + structured error metadata\n- `input_request` -> emits stdin warning text, sends empty `input_reply`, marks stdin requested\n- completion waits for both `execute_reply` and kernel `status=idle`\n\nCancellation/timeout:\n\n- abort signal triggers `interrupt()` (REST `/interrupt` + control-channel `interrupt_request`)\n- result marks `cancelled=true`\n- timeout path annotates output with `Command timed out after <n> seconds`\n\n## 6) Truncation and artifact behavior\n\n`OutputSink` in `src/session/streaming-output.ts` is used by kernel execution paths (`executeWithKernel`):\n\n- sanitizes every chunk (`sanitizeText`)\n- tracks total/output lines and bytes\n- optional artifact spill file (`artifactPath`, `artifactId`)\n- when in-memory buffer exceeds threshold (`DEFAULT_MAX_BYTES` unless overridden):\n - marks truncated\n - keeps tail bytes in memory (UTF-8 safe boundary)\n - can spill full stream to artifact sink\n\n`dump()` returns:\n\n- visible output text (possibly tail-truncated)\n- truncation flag + counts\n- artifact ID (for `artifact://<id>` references)\n\n`eval` converts this metadata into result truncation notices and TUI warnings.\n\n`notebook` tool does **not** use `OutputSink`; it has no stream/artifact truncation pipeline because it does not execute code.\n\n## 7) Renderer assumptions and formatting\n\n## Notebook renderer (`notebookToolRenderer`)\n\n- call view: status line with action + notebook path + cell/type metadata\n- result view:\n - success summary derived from `details`\n - `cellSource` rendered via `renderCodeCell`\n - markdown cells set language hint `markdown`; other cells have no explicit language override\n - collapsed code preview limit is `PREVIEW_LIMITS.COLLAPSED_LINES * 2`\n - supports expanded mode via shared render options\n - uses render cache keyed by width + expanded state\n\nError rendering assumption:\n\n- if first text content starts with `Error:`, renderer formats as notebook error block.\n\n## Python renderer (for actual execution output)\n\nKernel-backed execution rendering expects:\n\n- per-cell status transitions (`pending/running/complete/error`)\n- optional structured status event section\n- optional JSON output trees\n- truncation warnings + optional `artifact://<id>` pointer\n\nThis renderer behavior is unrelated to `notebook` JSON editing results except that both reuse shared TUI primitives.\n\n## 8) Divergence from eval Python backend behavior\n\nIf \"plain Python execution\" means the `eval` tool with `language: \"python\"`:\n\n- `eval` executes code in a kernel, persists state by mode, streams chunks, captures rich displays, handles interrupts/timeouts, and supports output truncation/artifacts.\n- `notebook` performs deterministic notebook JSON mutations only; no execution, no kernel state, no chunk stream, no display outputs, no artifact pipeline.\n\nIf a workflow needs both:\n\n1. edit notebook source with `notebook`\n2. execute code cells via `eval` with `language: \"python\"` (manually passing code), not through `notebook`\n\nCurrent implementation does not provide a single tool that both mutates `.ipynb` and executes notebook cells through kernel context.\n",
|
|
51
|
-
"notifications-sdk.md": "# Notifications SDK\n\n<p align=\"center\">\n <img src=\"../assets/telegram-mobile-hero.png\" alt=\"Sayknow-CLI mobile answers for coding agents hero illustration\" width=\"100%\" />\n</p>\n\nA small, transport-agnostic way to get **action-needed** signals out of a SKC\nsession and deliver **replies** back — without scraping the terminal and without\nthe depth of the RPC / Coordinator / Bridge surfaces.\n\nThe stable contract is deliberately generic: every running session exposes one\nloopback WebSocket endpoint, and integrations are user-written clients that\nconnect to that endpoint. Telegram, Discord, Slack, mobile apps, and local tools\nall use the same JSON protocol. No upstream Rust, N-API, or wire-protocol change\nis required for a new integration.\n\n> Status: the Rust core (`crates/skc-notifications`) provides the wire protocol,\n> action lifecycle, loopback WebSocket server, and endpoint discovery file. The\n> bundled Telegram daemon is a reference client layered on top of this SDK; it is\n> not the upstream topology.\n\n## Architecture\n\n```\nSKC session (upstream) your client (anywhere)\n┌───────────────────────────────┐ ┌──────────────────────────┐\n│ ask-tool fires / agent idle │ action_needed │ Telegram / Discord / ... │\n│ → notifications core │ ─────────────▶ │ render + collect reply │\n│ ws://127.0.0.1:<port> (+token) │ ◀───────────── │ │\n│ reply → resolve ask gate │ reply │ │\n└───────────────────────────────┘ └──────────────────────────┘\n```\n\n- **One endpoint per session.** Each session runs its own loopback WebSocket\n server. Upstream does not maintain a shared daemon, singleton, or\n chat-to-session registry; multiplexing many sessions into one integration is a\n client-side concern.\n- **Integrations are clients.** A client discovers endpoint files, connects to\n one or more WebSockets, renders `action_needed`, and sends `reply` messages.\n- **Zero upstream change.** New transports do not require changes to\n `crates/skc-notifications` or the JSON protocol.\n- **Off unless configured.** No endpoint exists unless notifications are enabled\n and a token is present.\n- **tmux-agnostic.** The endpoint behaves identically with or without tmux.\n\n## Endpoint discovery\n\nA running session writes a discovery file at:\n\n```\n<repo>/.skc/state/notifications/<sessionId>.json\n```\n\n(`.skc/state/` is git-ignored.) Shape:\n\n```json\n{\n \"version\": 1,\n \"sessionId\": \"019edd41-...\",\n \"pid\": 12345,\n \"host\": \"127.0.0.1\",\n \"port\": 53124,\n \"url\": \"ws://127.0.0.1:53124\",\n \"token\": \"<per-session token>\",\n \"startedAt\": 1718760000000,\n \"updatedAt\": 1718760000000,\n \"stale\": false\n}\n```\n\n- The file is created `0700`/`0600` (unix) and written atomically.\n- The **token is in the file** because clients need it; never log it raw.\n Stale files (dead PID, past TTL, or explicitly marked) are cleaned up on the\n next start.\n\nConnect with the token as a query parameter:\n\n```\nws://127.0.0.1:<port>/?token=<token>\n```\n\nA wrong/missing token is rejected at the handshake with HTTP `401`.\n\n## Protocol\n\nJSON text frames. Field names are `camelCase`; the `type` discriminator is\n`snake_case`.\n\n### Server → client\n\n`action_needed` — something needs attention:\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"wg_run_stage_1\", \"kind\": \"ask\",\n \"sessionId\": \"sess-1\", \"question\": \"Proceed?\", \"options\": [\"Yes\", \"No\"] }\n```\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"idle-sess-1-7\", \"kind\": \"idle\",\n \"sessionId\": \"sess-1\", \"summary\": \"finished refactor; awaiting next step\" }\n```\n\n- `kind: \"ask\"` is answerable in both interactive/TUI and unattended/RPC modes.\n The `id` is the real workflow-gate id.\n- `kind: \"idle\"` is notify-only and ephemeral (not replayed to clients that\n connect later).\n\n`action_resolved` — a pending action is now terminal and **non-repliable**:\n\n```json\n{ \"type\": \"action_resolved\", \"id\": \"wg_run_stage_1\", \"resolvedBy\": \"local\" }\n```\n\n`resolvedBy` is `local` (answered in the CLI/TUI), `client` (a remote reply won),\nor `timeout`.\n\n`reply_rejected` — sent only to the client whose reply failed:\n\n```json\n{ \"type\": \"reply_rejected\", \"id\": \"wg_run_stage_1\", \"reason\": \"already_answered\" }\n```\n\nReasons: `already_answered`, `unknown_action`, `invalid_answer`,\n`resolver_unavailable`, `idempotency_conflict`, `unauthorized`.\n\nThe frames above are the minimal contract every client implements. Threaded\nclients (like the managed Telegram daemon) may also receive optional\nserver → client frames they can render or ignore: `identity_header` (one-time\nper-session repo/branch/machine header), `context_update` (last message, task,\ngoal, token usage, model, diff), `turn_stream` (live/finalized turn output),\n`image_attachment` (agent-produced images), `activity` (busy/idle, drives the\ntyping indicator), `inbound_ack` (delivery state of an injected user message),\n`session_closed` (endpoint teardown; threaded clients may delete/archive the\nremote conversation), `config_update` (current verbosity/redact), `hello`\n(server capability/version), and `pong`. A minimal client only needs\n`action_needed`, `action_resolved`, and `reply_rejected`.\n\n### Client → server\n\n`reply` — answer a pending `ask`:\n\n```json\n{ \"type\": \"reply\", \"id\": \"wg_run_stage_1\", \"answer\": 0, \"token\": \"<token>\" }\n```\n\n`answer` accepts:\n\n- a number — zero-based option index (`0` = first option);\n- a string — an option label, or free text;\n- an object — `{ \"selected\": [0, \"Maybe\"], \"custom\": \"...\" }` for multi-select.\n\nOptional `idempotencyKey` makes retries safe: the same key + same body re-acks;\nthe same key + different body is rejected with `idempotency_conflict`.\n\nThreaded clients may also send optional client → server frames: `user_message`\n(inject/steer a turn with free text), `config_command` (toggle verbosity/redact\nin-thread), `hello` (capability/version), and `ping`. A minimal client only\nneeds `reply`.\n\n## Answer semantics\n\nA remote reply answers a pending ask in **both** modes — RPC is not required:\n\n- **Interactive / TUI mode:** the ask tool races the local selector against the\n remote reply (first valid answer wins). If you tap a button in the client, the\n ask resolves with that option; if you answer locally, the client receives\n `action_resolved` (`resolvedBy: \"local\"`) and the action becomes non-repliable.\n- **Unattended / RPC mode:** the reply resolves the real workflow-gate, driving\n the session the same way a local answer would.\n\nIn both modes the first valid reply wins; later replies get `already_answered`.\nIdle pings are notify-only.\n\n## Minimal client example\n\n```js\nimport { readFileSync } from \"node:fs\";\nimport WebSocket from \"ws\";\n\nconst { url, token } = JSON.parse(\n readFileSync(`.skc/state/notifications/${sessionId}.json`, \"utf8\"),\n);\n\nconst ws = new WebSocket(`${url}/?token=${encodeURIComponent(token)}`);\n\nws.on(\"message\", (data) => {\n const msg = JSON.parse(data.toString());\n if (msg.type === \"action_needed\" && msg.kind === \"ask\") {\n // present msg.question / msg.options to the human, then:\n ws.send(JSON.stringify({ type: \"reply\", id: msg.id, answer: 0, token }));\n } else if (msg.type === \"action_resolved\") {\n // mark this action as no longer answerable in your UI\n } else if (msg.type === \"reply_rejected\") {\n // e.g. reason === \"already_answered\" → the ask was answered elsewhere\n }\n});\n```\n\nSwap `ws` for a Telegram bot's long-poll loop, a Discord gateway client, or a\nSlack socket-mode app — the contract above is all you implement.\n\n## Managed notification adapters\n\nFor the exact user setup flow (`skc notify setup`, BotFather token, private-chat pairing, status, and troubleshooting), see [Telegram notification onboarding](./telegram-onboarding.md).\n\n## Managed Telegram daemon (bundled reference client)\n\nSKC also ships a managed Telegram reference client for the common phone-notify\nworkflow. It remains a client of the generic SDK: it scans session discovery\nfiles, opens each session WebSocket, and routes Telegram replies back to the\nmatching endpoint.\n\nThe daemon/session engine is shared. Session discovery, WebSocket protocol,\nredaction decisions, rate-limit pooling, reply routing, singleton ownership, and\nlifecycle control are not reimplemented by each chat surface. Telegram, Discord,\nand Slack adapters are thin presentation layers: they render internal notification\nevents into transport payloads and map transport interactions back to `{sessionId,\nactionId,answer}` replies.\n\n### Setup and auto-connect\n\nRun the setup command once:\n\n```sh\nskc notify setup\n```\n\nThe wizard validates the bot token with Telegram, verifies private-chat Threaded\nMode capability via `getMe.has_topics_enabled`, waits for a private DM to the bot,\nand writes canonical global Settings under `config.yml` in the SKC agent\ndirectory. It enables:\n\n- `notifications.enabled`\n- `notifications.telegram.botToken`\n- `notifications.telegram.chatId`\n- `notifications.redact` (optional; default false)\n- `notifications.discord.botToken` / `notifications.discord.channelId` (optional Discord adapter)\n- `notifications.slack.botToken` / `notifications.slack.channelId` (optional Slack adapter)\n\nAfter setup, sessions auto-connect when notifications are enabled. Each session\nstill publishes its own loopback endpoint; the daemon is only the Telegram-side\nmultiplexer.\n\nFor Telegram forum topics, the daemon deletes the per-session topic when the local\nnotification endpoint shuts down, so it disappears from the topic list. A resumed\nsession creates a fresh topic before sending again. The bot must be allowed to\ndelete messages in that chat; without that permission, deletion is best-effort and\ndelivery continues.\n\n### Singleton poller and trust model\n\nTelegram `getUpdates` allows only one active long-poll owner per bot token. The\nmanaged daemon enforces **one bot token = one getUpdates poller** with a local\nlock/state file under the agent directory. New sessions attach to the existing\nfresh daemon owner instead of starting another poller, preventing Telegram 409\nconflicts.\n\nThe trust model is intentionally strict:\n\n- setup pairs exactly one private Telegram chat;\n- runtime accepts updates only from that paired chat id;\n- groups, supergroups, channels, and unpaired users never receive session names,\n action ids, pending status, or configuration hints;\n- daemon state stores a token fingerprint, not the raw bot token.\n\n### Routing in private-chat topics\n\nThe paired private chat prefers per-session Telegram topics (Threaded Mode). The\ndaemon tags messages by session, stores compact callback aliases for inline\nbuttons, and routes replies back to the exact session/action. A forum-enabled\nsupergroup is no longer required: when the bot owner enables Threaded Mode in\n@BotFather, the daemon creates one topic per session in the paired private chat.\nSKC cannot enable Threaded Mode through the Bot API; setup only verifies the\ncapability and guides the manual BotFather toggle.\n\nIf BotFather's per-bot **Bot Settings** menu does not show **Threads Settings**\nor **Threaded Mode**, the supported fallback is the normal private-chat pairing.\nSetup can be saved as `threaded=unverified`/`threaded=unknown`, and the daemon\nstill tries topics when Telegram allows them. When `createForumTopic` is refused,\nthe daemon does not drop the send: it routes the notification to the normal\n(flat) paired private chat and posts a one-time nudge: `Flat Telegram private chat\nsupports outbound notifications and inline ask buttons only. Enable Threaded Mode\nin @BotFather > Bot Settings > Threads Settings for free-text replies and session\ncommands.` Pairing is private-only, so flat delivery stays within the user's own\nprivate DM.\n\nSupported reply paths:\n\n- tap an inline button on an ask notification;\n- reply inside the session's thread/topic (replies are thread-native; the\n topic identifies the session, so no session tag is needed).\n\nIn threaded mode the user can also adjust per-session behaviour with in-thread\nconfig commands: `/verbose`, `/lean`, `/verbosity <lean|verbose>`, and\n`/redact <on|off>`. The legacy `/answer <session-tag> <answer>` command is\nremoved — replies are routed by the topic they arrive in.\n\nFlat fallback keeps outbound notifications and inline-button answers working, but\nplain free-text never guesses from the global pending-ask set. Free-text replies\nand `/verbose`/`/lean`/`/verbosity`/`/redact` commands are thread-native and\nrequire Threaded Mode/topic routing. Enable Threaded Mode in @BotFather > Bot\nSettings > Threads Settings when you need free-text replies or session commands.\nDo not pair a group, supergroup, or channel to work around a missing BotFather\nmenu; the bundled setup flow is\nprivate-chat only, and non-private chat ids remain fail-closed to avoid session\ndata leaks.\n\nUnknown, expired, or restart-unvalidated callback aliases fail closed: the daemon\nsends guidance and does not guess a target session or action.\n\n### Discord and Slack setup\n\nDiscord and Slack use the same internal notification events and reply protocol as\nTelegram. Store only runtime credentials in local SKC settings or environment;\nnever paste bot tokens, webhook URLs, transcripts, prompts, host paths, or raw logs\ninto docs, tests, issues, or PR comments.\n\nConfiguration keys:\n\n```yaml\nnotifications:\n enabled: true\n discord:\n botToken: \"<local Discord bot token>\"\n channelId: \"<Discord channel id>\"\n slack:\n botToken: \"<local Slack bot token>\"\n channelId: \"<Slack channel id>\"\n redact: true\n```\n\nThe bundled adapters intentionally render public-safe message bodies and return\nroute metadata only for pending internal actions. They do not own polling,\nsession scans, daemon locks, rate limits, or SDK lifecycle. Production transport\nsenders should consume the adapter payloads and keep all credential-bearing HTTP\nor gateway details outside logged payloads.\n### Redaction\n\n`notifications.redact` strips sensitive content before remote delivery, but\n**asks are exempt**: an ask is an interactive prompt the human must read and\nanswer remotely, so its `question` and `options` are always sent unredacted\n(otherwise it would be unanswerable). When redaction is enabled, `idle`\nsummaries are removed and streamed content frames (`turn_stream`,\n`context_update`, `image_attachment`) are suppressed at their emit sites. When\nredaction is disabled, all content is delivered unchanged.\n\n### Local `/notify`\n\nInside a SKC session, `/notify` controls the current session only:\n\n- `/notify status` reports enabled/disabled state, daemon observation when known,\n and redaction state without printing secrets;\n- `/notify off` disables the current session's notification endpoint and removes\n its discovery record without mutating global Settings;\n- `/notify on` re-enables the current session when global setup is complete and\n `SKC_NOTIFICATIONS=0` is not forcing opt-out.\n\n### Manual Telegram CLI is for debugging\n\n`packages/coding-agent/src/notifications/telegram-cli.ts` remains as a manual\nreference/debug client and template for other integrations. It is not the primary\nTelegram UX.\n\n```sh\nbun run packages/coding-agent/src/notifications/telegram-cli.ts --bot-token \"$BOT_TOKEN\"\n```\n\nBy default it refuses to start when a fresh managed daemon already owns the same\nbot token for the same paired chat, because a second poller will cause Telegram\n409 conflicts. Use `--force` only for deliberate debugging when you have stopped\nor intentionally want to override the daemon guard.\n## Two client surfaces: per-session vs daemon-owned lifecycle control\n\nThe SDK now exposes **two distinct surfaces**. Do not confuse them:\n\n1. **Per-session notification clients (the normal, documented contract above).**\n A client discovers `<repo>/.skc/state/notifications/<sessionId>.json`, connects\n to that session's loopback WebSocket, and handles `action_needed`,\n `action_resolved`, `reply_rejected`, and the optional threaded frames. This is\n all an ordinary integration (Telegram, Discord, Slack, mobile, local tools)\n needs. It requires **zero** upstream changes.\n\n2. **The daemon-owned session *lifecycle* control endpoint (privileged).**\n A separate, **session-independent**, loopback-only, authenticated control\n endpoint that accepts `session_create` / `session_close` / `session_resume`\n frames. It exists because creating a session cannot use a per-session socket\n (none exists before the session does). It is **not** part of the normal\n integration contract: ordinary clients never implement it. Only the bundled,\n trusted daemon (e.g. the managed Telegram daemon) speaks it.\n\n### Lifecycle control endpoint\n\n- **Discovery:** `<agentDir>/notifications/control.json` (daemon-owned, mode\n `0600`), distinct from per-session endpoint files. It carries only non-secret\n endpoint metadata (url/host/port/pid/owner). The control token is held **in\n memory** by the daemon (the sole client) and is **never** written to disk.\n- **Auth:** loopback-only bind (a non-loopback bind is refused). The WebSocket\n upgrade requires `?token=<control-token>` (HTTP `401` otherwise), and every\n lifecycle frame's `token` is re-checked (`unauthorized` on mismatch). The Rust\n ingress authenticates and forwards; it never spawns or applies policy.\n- **Frames:** `session_create` (target `existing_path` | `worktree` |\n `plain_dir`), `session_close` (hard-kill, history preserved, recoverable),\n `session_resume` (reattach if alive, else cold-restart from history); responses\n `session_create_response` / `session_close_response` / `session_resume_response`\n / `session_lifecycle_error`. The protocol also defines a replayable\n `session_ready` per-session frame for readiness-gated creates; the current MVP\n daemon replies once the tmux launch is requested (see the phone guide) rather\n than waiting on it. Inline prompt text (`-- <prompt>`) is rejected in the MVP.\n\n### Trust model and hardening (daemon side)\n\nThe control endpoint trusts the configured paired chat for any path (an accepted\nrisk). It is hardened around that boundary:\n\n- **Strict paired-chat gating** — non-paired chats are rejected *before* any path\n parsing, filesystem, or process action.\n- **Durable idempotency** — a locked, atomic, fsynced ledger keyed by\n `chatId:updateId` + request hash (`telegram-lifecycle-idempotency.json`).\n Duplicate updates never repeat side effects, including across daemon restart; a\n duplicate while in-progress reports pending (never a second spawn); a same id\n with a different body is `duplicate_conflict`; an effect failure is recorded\n `terminal_uncertain` (never auto-respawned).\n- **Per-chat create rate limit.**\n- **Audit log** — append-only `telegram-lifecycle-audit.jsonl` (`0600`) recording\n every accept/reject/duplicate/rate-limit/spawn/success/failure. Raw control\n tokens and raw prompts are never logged (prompt hash + byte length only).\n- **Inline prompts rejected (MVP)** — `session_create` with `-- <prompt>` text is\n rejected with usage; no prompt is ever placed in argv, audit, or responses. (A\n redacted prompt-ref flow is reserved for a future revision.)\n- **SKC-managed-only close** — force-close re-reads the exact `@skc-profile`\n immediately before kill and requires the `@skc-session-id` (and optional\n `@skc-session-state-file`) tag to match; it never touches non-SKC tmux.\n- **Recent-activity picker** — sessions are ranked by history-file mtime and\n enriched with terminal breadcrumbs so the operator picks a recent repo/session\n instead of typing raw paths. Ambiguous resumes fail closed with candidates.\n### Phone test guide (create / close / resume from Telegram)\n\nEnd-to-end manual check once `skc notify setup` has paired your private chat:\n\n1. **Pair + start.** Run `skc notify setup` (BotFather token, DM the bot to pair).\n Start any SKC session with notifications enabled so the daemon owner is\n running (`skc launch` in a repo, or `SKC_NOTIFICATIONS=1`). The owner starts\n the loopback control endpoint and accepts `/session_*` while running; with zero\n active sessions it still idle-exits after the inactivity timeout.\n2. **Create.** From your paired chat, pick `/session_create` from the Telegram\n command menu or send `/session_create path <repo-dir>` (or\n `/session_create worktree <repo> <branch>`, or `/session_create dir <newdir>`).\n `<repo-dir>`, `<repo>`, and `<newdir>` may use `~`/`~/...` for your own home\n directory; named-user forms such as `~alice/repo` are rejected. The bot replies\n once the tmux launch is requested; the session shows up in `/session_recent`\n once it is ready. (Inline prompts via `-- <text>` are rejected for now with\n usage text.)\n3. **List.** `/session_recent` shows recent sessions (most-recent first) to copy\n an id from.\n4. **Close.** `/session_close <sessionId>` hard-kills the SKC-managed session\n (history is preserved); the bot confirms.\n5. **Resume.** `/session_resume <sessionId|prefix>` reattaches if it is still\n alive, otherwise cold-restarts it from saved history. An ambiguous prefix\n replies with the matching candidates instead of guessing.\n\nCommands are accepted **only** from the paired chat; **create** is rate-limited,\nand all lifecycle commands are idempotent per Telegram update id and audited (no\ntokens or prompts are logged).\nFor an automated proof of the wire path without a real bot, see\n`packages/coding-agent/scripts/g011-daemon-path-smoke.ts` (real native control\nendpoint + loopback WebSocket).\n",
|
|
52
|
+
"notifications-sdk.md": "# Notifications SDK\n\n<p align=\"center\">\n <img src=\"../assets/telegram-mobile-hero.png\" alt=\"Sayknow-CLI mobile answers for coding agents hero illustration\" width=\"100%\" />\n</p>\n\nA small, transport-agnostic way to get **action-needed** signals out of a SKC\nsession and deliver **replies** back — without scraping the terminal and without\nthe depth of the RPC / Coordinator / Bridge surfaces.\n\nThe stable contract is deliberately generic: every running session exposes one\nloopback WebSocket endpoint, and integrations are user-written clients that\nconnect to that endpoint. Telegram, Discord, Slack, mobile apps, and local tools\nall use the same JSON protocol. No upstream Rust, N-API, or wire-protocol change\nis required for a new integration.\n\n> Status: the Rust core (`crates/skc-notifications`) provides the wire protocol,\n> action lifecycle, loopback WebSocket server, and endpoint discovery file. The\n> bundled Telegram daemon is a reference client layered on top of this SDK; it is\n> not the upstream topology.\n\n## Architecture\n\n```\nSKC session (upstream) your client (anywhere)\n┌───────────────────────────────┐ ┌──────────────────────────┐\n│ ask-tool fires / agent idle │ action_needed │ Telegram / Discord / ... │\n│ → notifications core │ ─────────────▶ │ render + collect reply │\n│ ws://127.0.0.1:<port> (+token) │ ◀───────────── │ │\n│ reply → resolve ask gate │ reply │ │\n└───────────────────────────────┘ └──────────────────────────┘\n```\n\n- **One endpoint per session.** Each session runs its own loopback WebSocket\n server. Upstream does not maintain a shared daemon, singleton, or\n chat-to-session registry; multiplexing many sessions into one integration is a\n client-side concern.\n- **Integrations are clients.** A client discovers endpoint files, connects to\n one or more WebSockets, renders `action_needed`, and sends `reply` messages.\n- **Zero upstream change.** New transports do not require changes to\n `crates/skc-notifications` or the JSON protocol.\n- **Off unless configured.** No endpoint exists unless notifications are enabled\n and a token is present.\n- **tmux-agnostic.** The endpoint behaves identically with or without tmux.\n\n### N-API action emission\n\n`NotificationServer#pushFrame(frameJson)` rejects (throws for) every `ActionNeeded`\nframe. Emit asks with `registerAsk(...)` and idle notifications with `noteIdle(...)`\ninstead, so action delivery remains capability-gated per connection.\n\n\n## Endpoint discovery\n\nA running session writes a discovery file at:\n\n```\n<repo>/.skc/state/notifications/<sessionId>.json\n```\n\n(`.skc/state/` is git-ignored.) Shape:\n\n```json\n{\n \"version\": 1,\n \"sessionId\": \"019edd41-...\",\n \"pid\": 12345,\n \"host\": \"127.0.0.1\",\n \"port\": 53124,\n \"url\": \"ws://127.0.0.1:53124\",\n \"token\": \"<per-session token>\",\n \"startedAt\": 1718760000000,\n \"updatedAt\": 1718760000000,\n \"stale\": false\n}\n```\n\n- The file is created `0700`/`0600` (unix) and written atomically.\n- The **token is in the file** because clients need it; never log it raw.\n Stale files (dead PID, past TTL, or explicitly marked) are cleaned up on the\n next start.\n\nConnect with the token as a query parameter:\n\n```\nws://127.0.0.1:<port>/?token=<token>\n```\n\nA wrong/missing token is rejected at the handshake with HTTP `401`.\n\n## Protocol\n\nJSON text frames. Field names are `camelCase`; the `type` discriminator is\n`snake_case`.\n\n### Server → client\n\n`action_needed` — something needs attention:\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"wg_run_stage_1\", \"kind\": \"ask\",\n \"sessionId\": \"sess-1\", \"question\": \"Proceed?\", \"options\": [\"Yes\", \"No\"] }\n```\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"idle-sess-1-7\", \"kind\": \"idle\",\n \"sessionId\": \"sess-1\", \"summary\": \"finished refactor; awaiting next step\" }\n```\n\n- `kind: \"ask\"` is answerable in both interactive/TUI and unattended/RPC modes.\n The `id` is the real workflow-gate id.\n- `kind: \"idle\"` is notify-only and ephemeral (not replayed to clients that\n connect later).\n\n`action_resolved` — a pending action is now terminal and **non-repliable**:\n\n```json\n{ \"type\": \"action_resolved\", \"id\": \"wg_run_stage_1\", \"resolvedBy\": \"local\" }\n```\n\n`resolvedBy` is `local` (answered in the CLI/TUI), `client` (a remote reply won),\nor `timeout`.\n\n`reply_rejected` — sent only to the client whose reply failed:\n\n```json\n{ \"type\": \"reply_rejected\", \"id\": \"wg_run_stage_1\", \"reason\": \"already_answered\" }\n```\n\nReasons: `already_answered`, `unknown_action`, `invalid_answer`,\n`resolver_unavailable`, `idempotency_conflict`, `unauthorized`.\n\n`action_unavailable` — a controlled ask cannot be presented on this connection:\n\n```json\n{ \"type\": \"action_unavailable\", \"id\": \"wg_run_stage_1\", \"sessionId\": \"sess-1\",\n \"reason\": \"missing_capability\", \"requiredCapabilities\": [\"ask_controls_v1\"] }\n```\n\nThis frame is diagnostic and non-actionable: do not render it as an ask or\nattempt a `reply` for it.\n\nThe minimal contract depends on the asks a client handles. A client that handles\nonly ordinary asks (those with empty `controls`) needs `action_needed`,\n`action_resolved`, and `reply_rejected`, and may omit `hello`. A client that\nwants controlled asks **MUST** send a ClientHello on socket open with\n`{ \"type\": \"hello\", \"protocolVersion\": 3, \"capabilities\": [\"ask_controls_v1\"] }`\nand handle `action_unavailable` as a non-actionable diagnostic. Threaded clients\n(like the managed Telegram daemon) may also receive optional server → client\nframes they can render or ignore: `identity_header` (one-time per-session\nrepo/branch/machine header), `context_update` (last message, task, goal, token\nusage, model, diff), `turn_stream` (live/finalized turn output),\n`image_attachment` (agent-produced images), `activity` (busy/idle, drives the\ntyping indicator), `inbound_ack` (delivery state of an injected user message),\n`session_closed` (endpoint teardown; threaded clients may delete/archive the\nremote conversation), `config_update` (current verbosity/redact), `hello`\n(server capability/version), and `pong`.\n\n### Client → server\n\n`reply` — answer a pending `ask`:\n\n```json\n{ \"type\": \"reply\", \"id\": \"wg_run_stage_1\", \"answer\": 0, \"token\": \"<token>\" }\n```\n\n`answer` accepts:\n\n- a number — zero-based option index (`0` = first option);\n- a string — an option label, or free text;\n- an object — `{ \"selected\": [0, \"Maybe\"], \"custom\": \"...\" }` for multi-select;\n- an object — `{ \"controlId\": \"navigation_forward\" }` to commit a controlled\n multi-select only when that navigation control was presented as enabled.\n\nOptional `idempotencyKey` makes retries safe: the same key + same body re-acks;\nthe same key + different body is rejected with `idempotency_conflict`.\n\nClients may also send optional client → server frames: `user_message` (inject/steer\na turn with free text), `config_command` (toggle verbosity/redact in-thread),\n`ping`, and `hello` (capability/version). A ClientHello is optional for ordinary\nempty-controls asks, but is required for controlled asks as specified below.\n\n## Answer semantics\n\nA remote reply answers a pending ask in **both** modes — RPC is not required:\n\n- **Interactive / TUI mode:** the ask tool races the local selector against the\n remote reply (first valid answer wins). If you tap a button in the client, the\n ask resolves with that option; if you answer locally, the client receives\n `action_resolved` (`resolvedBy: \"local\"`) and the action becomes non-repliable.\n- **Unattended / RPC mode:** the reply resolves the real workflow-gate, driving\n the session the same way a local answer would.\n\nIn both modes the first valid reply wins; later replies get `already_answered`.\nIdle pings are notify-only.\n\n### Controlled-ask capability negotiation (protocol v3)\n\nProtocol v3 adds optional `ask_controls_v1` and `ask_selected_ack_v1`\ncapabilities. A controlled ask carries both `options` and non-empty `controls`:\n\n```json\n{ \"type\": \"action_needed\", \"id\": \"wg_run_stage_1\", \"kind\": \"ask\",\n \"sessionId\": \"sess-1\", \"question\": \"Choose all that apply\",\n \"options\": [\"A\", \"B\"],\n \"controls\": [{ \"id\": \"navigation_forward\", \"kind\": \"navigation\", \"label\": \"Done\", \"enabled\": false }] }\n```\n\nControls are typed data. Clients must return\n`{\"controlId\":\"navigation_forward\"}` rather than infer a control from an\noption label.\n\nClients that want controlled asks **MUST** send this ClientHello immediately in\ntheir WebSocket `open` handler:\n\n```json\n{ \"type\": \"hello\", \"protocolVersion\": 3, \"capabilities\": [\"ask_controls_v1\"] }\n```\n\nThe server sends its ServerHello before it processes a ClientHello. A controlled\nask is deferred while the connection awaits that Hello. After the approximately\none-second Hello grace expires, or after a negotiated Hello lacks\n`ask_controls_v1`, the server sends exactly one non-actionable\n`action_unavailable`; it never sends a stripped `action_needed` frame with option\nbuttons. A later ClientHello that adds `ask_controls_v1` may upgrade that\nconnection to the full `action_needed` presentation.\n\nCapabilities are monotonic for one connection: repeated ClientHellos can add\ncapabilities but cannot remove an already-advertised capability or retract a\nfull presentation. Disconnecting and reconnecting starts a fresh negotiation\nwith no capabilities or delivery state. Ordinary asks with empty `controls`\nretain their existing immediate delivery and reply behavior for Hello-less and\nolder clients.\n\n### Protocol v3 semantic acknowledgement\n\nA remote reply is first **claimed** by the native server. The host retains the raw reply JSON, idempotency key, and reply receipt id, then resolves or closes that claim only after ask/gate semantic settlement. Invalid input terminally closes that interaction; a retry is a fresh action id, never a reopened claim.\n\nManaged Telegram clients may receive `ask_selected_ack_request` and return one correlated `ask_selected_ack_result`. Requests are `mode:\"live\"` (the current pending action on the authenticated session connection) or `mode:\"recovery\"` (the persisted session topic only). Recovery neither recreates an ask, keyboard, alias, nor reply route; a missing persisted topic is `failed/route_missing`. `ask_selected_ack_cancel` removes an exact queued request or aborts an in-flight attempt. The request/result/cancel correlation is `requestId` plus `commitKey` and is authorized by the authenticated WebSocket connection and endpoint generation; acknowledgement frames carry no second token.\n\nThe managed daemon sends the literal `Selected!` at most once per generation/commit key, on its `ask` rate-limit lane, with an absolute 8-second deadline and no application retry. A response is `delivered` only with Telegram `{ok:true,result.message_id:number}`; explicit rejection is `failed`, and timeout/network/in-flight abort uncertainty is `unknown`. Callback-query acknowledgement remains independent. The durable workflow answer is exactly-once; visible Telegram delivery is not.\n\nEvery accepted workflow-gate origin, including generic RPC/bridge winners, invokes canonical gate-presentation cleanup before continuation. Generic origins have no Telegram acknowledgement policy. Cancellation, shutdown, and endpoint replacement cancel retained interactions and acknowledgement work. A connection loss before dispatch is definitive `failed/session_closed`; after successful request-frame dispatch but before a correlated result it is `unknown/origin_disconnected`—the host does not infer whether Telegram was called.\n\n## Minimal client example\n\nThis copyable example advertises `ask_controls_v1`, so it can receive and\ncomplete controlled asks. An ordinary client that handles only empty-controls\nasks omits the `open` handler and ClientHello, and keeps only the numeric\nordinary-ask branch below.\n\n```js\nimport { readFileSync } from \"node:fs\";\nimport WebSocket from \"ws\";\n\nconst { url, token } = JSON.parse(\n readFileSync(`.skc/state/notifications/${sessionId}.json`, \"utf8\"),\n);\n\nconst ws = new WebSocket(`${url}/?token=${encodeURIComponent(token)}`);\nlet currentControlledAsk;\n\nfunction sendOptionReply(id, index) {\n ws.send(JSON.stringify({ type: \"reply\", id, answer: index, token }));\n}\n\nfunction commitControlledMultiSelect() {\n const ask = currentControlledAsk;\n const navigation = ask?.controls?.find(\n (control) =>\n control.id === \"navigation_forward\" &&\n control.kind === \"navigation\" &&\n control.enabled,\n );\n if (!navigation) return;\n\n ws.send(JSON.stringify({\n type: \"reply\",\n id: ask.id,\n answer: { controlId: \"navigation_forward\" },\n token,\n }));\n}\n\nws.on(\"open\", () => {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({\n type: \"hello\",\n protocolVersion: 3,\n capabilities: [\"ask_controls_v1\"],\n }));\n }\n});\n\nws.on(\"message\", (data) => {\n const msg = JSON.parse(data.toString());\n if (msg.type === \"action_needed\" && msg.kind === \"ask\") {\n if (msg.controls?.length) {\n currentControlledAsk = msg;\n // Present msg.question / msg.options / msg.controls to the human.\n // An option click calls sendOptionReply(msg.id, index). For a controlled\n // multi-select, that numeric reply only toggles and is non-terminal.\n // Wait for the fresh action_needed frame, then call\n // commitControlledMultiSelect() only from its enabled navigation control.\n return;\n }\n\n // Ordinary ask (`controls: []`): the minimal no-Hello client replies normally.\n sendOptionReply(msg.id, 0);\n } else if (msg.type === \"action_resolved\") {\n if (currentControlledAsk?.id === msg.id) currentControlledAsk = undefined;\n // Mark this action as no longer answerable in your UI.\n } else if (msg.type === \"reply_rejected\") {\n // e.g. reason === \"already_answered\" → the ask was answered elsewhere.\n } else if (msg.type === \"action_unavailable\") {\n // Diagnostic only: do not fabricate an ask or option buttons.\n }\n});\n```\n\n`sendOptionReply(msg.id, index)` has the numeric option/toggle shape\n`{ type:\"reply\", id, answer:index, token }`. The enabled navigation control\nuses the distinct typed commit shape\n`{ type:\"reply\", id, answer:{ controlId:\"navigation_forward\" }, token }`.\nDo not send that commit against the pre-toggle action id: a multi-select option\ntoggle reissues the action, and the enabled navigation control on the latest\nframe commits the selection.\n\nSwap `ws` for a Telegram bot's long-poll loop, a Discord gateway client, or a\nSlack socket-mode app — the contract above is all you implement.\n\n## Managed notification adapters\n\nFor the exact user setup flow (`skc notify setup`, BotFather token, private-chat pairing, status, and troubleshooting), see [Telegram notification onboarding](./telegram-onboarding.md).\n\n## Managed Telegram daemon (bundled reference client)\n\nSKC also ships a managed Telegram reference client for the common phone-notify\nworkflow. It remains a client of the generic SDK: it scans session discovery\nfiles, opens each session WebSocket, and routes Telegram replies back to the\nmatching endpoint.\n\nThe daemon/session engine is shared. Session discovery, WebSocket protocol,\nredaction decisions, rate-limit pooling, reply routing, singleton ownership, and\nlifecycle control are not reimplemented by each chat surface. Telegram, Discord,\nand Slack adapters are thin presentation layers: they render internal notification\nevents into transport payloads and map transport interactions back to `{sessionId,\nactionId,answer}` replies.\n\n### Setup and auto-connect\n\nRun the setup command once:\n\n```sh\nskc notify setup\n```\n\nThe wizard validates the bot token with Telegram, verifies private-chat Threaded\nMode capability via `getMe.has_topics_enabled`, waits for a private DM to the bot,\nand writes canonical global Settings under `config.yml` in the SKC agent\ndirectory. It enables:\n\n- `notifications.enabled`\n- `notifications.telegram.botToken`\n- `notifications.telegram.chatId`\n- `notifications.redact` (optional; default false)\n- `notifications.discord.botToken` / `notifications.discord.channelId` (optional Discord adapter)\n- `notifications.slack.botToken` / `notifications.slack.channelId` (optional Slack adapter)\n\nAfter setup, sessions auto-connect when notifications are enabled. Each session\nstill publishes its own loopback endpoint; the daemon is only the Telegram-side\nmultiplexer.\n\nFor Telegram forum topics, the daemon deletes the per-session topic when the local\nnotification endpoint shuts down, so it disappears from the topic list. A resumed\nsession creates a fresh topic before sending again. The bot must be allowed to\ndelete messages in that chat; without that permission, deletion is best-effort and\ndelivery continues.\n\nPer-session topics are titled `{repo}/{branch} - {title}` by default (falling back\nto `{repo}/{branch}`, then the session title, then `SKC <id>` when identity is\nmissing). Set `notifications.telegram.topics.nameTemplate` to reorder or reshape\nthat title — for example `\"{title} · {repo}/{branch}\"` puts the session title\nfirst so concurrent sessions on the same checkout stay distinguishable in the\nsidebar. The template supports the `{repo}`, `{branch}`, and `{title}`\nplaceholders; unknown placeholders are left verbatim. The template is applied\nonly when every placeholder it references has a value for that session — otherwise\nthe daemon falls back to the built-in composition, so provisional and identity-less\ntopics never render with dangling separators. Leaving the setting unset preserves\nthe default naming exactly.\n\n### Singleton poller and trust model\n\nTelegram `getUpdates` allows only one active long-poll owner per bot token. The\nmanaged daemon enforces **one bot token = one getUpdates poller** with a local\nlock/state file under the agent directory. New sessions attach to the existing\nfresh daemon owner instead of starting another poller, preventing Telegram 409\nconflicts.\n\nThe trust model is intentionally strict:\n\n- setup pairs exactly one private Telegram chat;\n- runtime accepts updates only from that paired chat id;\n- groups, supergroups, channels, and unpaired users never receive session names,\n action ids, pending status, or configuration hints;\n- daemon state stores a token fingerprint, not the raw bot token.\n\n### Routing in private-chat topics\n\nThe paired private chat prefers per-session Telegram topics (Threaded Mode). The\ndaemon tags messages by session, stores compact callback aliases for inline\nbuttons, and routes replies back to the exact session/action. A forum-enabled\nsupergroup is no longer required: when the bot owner enables Threaded Mode in\n@BotFather, the daemon creates one topic per session in the paired private chat.\nSKC cannot enable Threaded Mode through the Bot API; setup only verifies the\ncapability and guides the manual BotFather toggle.\n\nIf BotFather's per-bot **Bot Settings** menu does not show **Threads Settings**\nor **Threaded Mode**, the supported fallback is the normal private-chat pairing.\nSetup can be saved as `threaded=unverified`/`threaded=unknown`, and the daemon\nstill tries topics when Telegram allows them. When `createForumTopic` is refused,\nthe daemon does not drop the send: it routes the notification to the normal\n(flat) paired private chat and posts a one-time nudge: `Flat Telegram private chat\nsupports outbound notifications and inline ask buttons only. Enable Threaded Mode\nin @BotFather > Bot Settings > Threads Settings for free-text replies and session\ncommands.` Pairing is private-only, so flat delivery stays within the user's own\nprivate DM.\n\nSupported reply paths:\n\n- tap an inline button on an ask notification;\n- reply inside the session's thread/topic (replies are thread-native; the\n topic identifies the session, so no session tag is needed).\n\nIn threaded mode the user can also adjust per-session behaviour with in-thread\nconfig commands: `/verbose`, `/lean`, `/verbosity <lean|verbose>`, and\n`/redact <on|off>`. The legacy `/answer <session-tag> <answer>` command is\nremoved — replies are routed by the topic they arrive in.\n\nFlat fallback keeps outbound notifications and inline-button answers working, but\nplain free-text never guesses from the global pending-ask set. Free-text replies\nand `/verbose`/`/lean`/`/verbosity`/`/redact` commands are thread-native and\nrequire Threaded Mode/topic routing. Enable Threaded Mode in @BotFather > Bot\nSettings > Threads Settings when you need free-text replies or session commands.\nDo not pair a group, supergroup, or channel to work around a missing BotFather\nmenu; the bundled setup flow is\nprivate-chat only, and non-private chat ids remain fail-closed to avoid session\ndata leaks.\n\nUnknown, expired, or restart-unvalidated callback aliases fail closed: the daemon\nsends guidance and does not guess a target session or action.\n\n### Discord and Slack setup\n\nDiscord and Slack use the same internal notification events and reply protocol as\nTelegram. Store only runtime credentials in local SKC settings or environment;\nnever paste bot tokens, webhook URLs, transcripts, prompts, host paths, or raw logs\ninto docs, tests, issues, or PR comments.\n\nConfiguration keys:\n\n```yaml\nnotifications:\n enabled: true\n discord:\n botToken: \"<local Discord bot token>\"\n channelId: \"<Discord channel id>\"\n slack:\n botToken: \"<local Slack bot token>\"\n channelId: \"<Slack channel id>\"\n redact: true\n```\n\nThe bundled adapters intentionally render public-safe message bodies and return\nroute metadata only for pending internal actions. They do not own polling,\nsession scans, daemon locks, rate limits, or SDK lifecycle. Production transport\nsenders should consume the adapter payloads and keep all credential-bearing HTTP\nor gateway details outside logged payloads.\n### Redaction\n\n`notifications.redact` strips sensitive content before remote delivery, but\n**asks are exempt**: an ask is an interactive prompt the human must read and\nanswer remotely, so its `question` and `options` are always sent unredacted\n(otherwise it would be unanswerable). When redaction is enabled, `idle`\nsummaries are removed and streamed content frames (`turn_stream`,\n`context_update`, `image_attachment`) are suppressed at their emit sites. When\nredaction is disabled, all content is delivered unchanged.\n\n### Local `/notify`\n\nInside a SKC session, `/notify` controls the current session only:\n\n- `/notify status` reports enabled/disabled state, daemon observation when known,\n and redaction state without printing secrets;\n- `/notify off` disables the current session's notification endpoint and removes\n its discovery record without mutating global Settings;\n- `/notify on` re-enables the current session when global setup is complete and\n `SKC_NOTIFICATIONS=0` is not forcing opt-out.\n\n### Manual Telegram CLI is for debugging\n\n`packages/coding-agent/src/notifications/telegram-cli.ts` remains as a manual\nreference/debug client and template for other integrations. It is not the primary\nTelegram UX.\n\n```sh\nbun run packages/coding-agent/src/notifications/telegram-cli.ts --bot-token \"$BOT_TOKEN\"\n```\n\nBy default it refuses to start when a fresh managed daemon already owns the same\nbot token for the same paired chat, because a second poller will cause Telegram\n409 conflicts. Use `--force` only for deliberate debugging when you have stopped\nor intentionally want to override the daemon guard.\n## Two client surfaces: per-session vs daemon-owned lifecycle control\n\nThe SDK now exposes **two distinct surfaces**. Do not confuse them:\n\n1. **Per-session notification clients (the normal, documented contract above).**\n A client discovers `<repo>/.skc/state/notifications/<sessionId>.json`, connects\n to that session's loopback WebSocket, and handles `action_needed`,\n `action_resolved`, `reply_rejected`, and the optional threaded frames. This is\n all an ordinary integration (Telegram, Discord, Slack, mobile, local tools)\n needs. It requires **zero** upstream changes.\n\n2. **The daemon-owned session *lifecycle* control endpoint (privileged).**\n A separate, **session-independent**, loopback-only, authenticated control\n endpoint that accepts `session_create` / `session_close` / `session_resume`\n frames. It exists because creating a session cannot use a per-session socket\n (none exists before the session does). It is **not** part of the normal\n integration contract: ordinary clients never implement it. Only the bundled,\n trusted daemon (e.g. the managed Telegram daemon) speaks it.\n\n### Lifecycle control endpoint\n\n- **Discovery:** `<agentDir>/notifications/control.json` (daemon-owned, mode\n `0600`), distinct from per-session endpoint files. It carries only non-secret\n endpoint metadata (url/host/port/pid/owner). The control token is held **in\n memory** by the daemon (the sole client) and is **never** written to disk.\n- **Auth:** loopback-only bind (a non-loopback bind is refused). The WebSocket\n upgrade requires `?token=<control-token>` (HTTP `401` otherwise), and every\n lifecycle frame's `token` is re-checked (`unauthorized` on mismatch). The Rust\n ingress authenticates and forwards; it never spawns or applies policy.\n- **Frames:** `session_create` (target `existing_path` | `worktree` |\n `plain_dir`), `session_close` (hard-kill, history preserved, recoverable),\n `session_resume` (reattach if alive, else cold-restart from history); responses\n `session_create_response` / `session_close_response` / `session_resume_response`\n / `session_lifecycle_error`. The protocol also defines a replayable\n `session_ready` per-session frame for readiness-gated creates; the current MVP\n daemon replies once the tmux launch is requested (see the phone guide) rather\n than waiting on it. Inline prompt text (`-- <prompt>`) is rejected in the MVP.\n\n### Trust model and hardening (daemon side)\n\nThe control endpoint trusts the configured paired chat for any path (an accepted\nrisk). It is hardened around that boundary:\n\n- **Strict paired-chat gating** — non-paired chats are rejected *before* any path\n parsing, filesystem, or process action.\n- **Durable idempotency** — a locked, atomic, fsynced ledger keyed by\n `chatId:updateId` + request hash (`telegram-lifecycle-idempotency.json`).\n Duplicate updates never repeat side effects, including across daemon restart; a\n duplicate while in-progress reports pending (never a second spawn); a same id\n with a different body is `duplicate_conflict`; an effect failure is recorded\n `terminal_uncertain` (never auto-respawned).\n- **Per-chat create rate limit.**\n- **Audit log** — append-only `telegram-lifecycle-audit.jsonl` (`0600`) recording\n every accept/reject/duplicate/rate-limit/spawn/success/failure. Raw control\n tokens and raw prompts are never logged (prompt hash + byte length only).\n- **Inline prompts rejected (MVP)** — `session_create` with `-- <prompt>` text is\n rejected with usage; no prompt is ever placed in argv, audit, or responses. (A\n redacted prompt-ref flow is reserved for a future revision.)\n- **SKC-managed-only close** — force-close re-reads the exact `@skc-profile`\n immediately before kill and requires the `@skc-session-id` (and optional\n `@skc-session-state-file`) tag to match; it never touches non-SKC tmux.\n- **Recent-activity picker** — sessions are ranked by history-file mtime and\n enriched with terminal breadcrumbs so the operator picks a recent repo/session\n instead of typing raw paths. Ambiguous resumes fail closed with candidates.\n### Phone test guide (create / close / resume from Telegram)\n\nEnd-to-end manual check once `skc notify setup` has paired your private chat:\n\n1. **Pair + start.** Run `skc notify setup` (BotFather token, DM the bot to pair).\n Start any SKC session with notifications enabled so the daemon owner is\n running (`skc launch` in a repo, or `SKC_NOTIFICATIONS=1`). The owner starts\n the loopback control endpoint and accepts `/session_*` while running; with zero\n active sessions it still idle-exits after the inactivity timeout.\n2. **Create.** From your paired chat, pick `/session_create` from the Telegram\n command menu or send `/session_create path <repo-dir>` (or\n `/session_create worktree <repo> <branch>`, or `/session_create dir <newdir>`).\n `<repo-dir>`, `<repo>`, and `<newdir>` may use `~`/`~/...` for your own home\n directory; named-user forms such as `~alice/repo` are rejected. The bot replies\n once the tmux launch is requested; the session shows up in `/session_recent`\n once it is ready. (Inline prompts via `-- <text>` are rejected for now with\n usage text.)\n3. **List.** `/session_recent` shows recent sessions (most-recent first) to copy\n an id from.\n4. **Close.** `/session_close <sessionId>` hard-kills the SKC-managed session\n (history is preserved); the bot confirms.\n5. **Resume.** `/session_resume <sessionId|prefix>` reattaches if it is still\n alive, otherwise cold-restarts it from saved history. An ambiguous prefix\n replies with the matching candidates instead of guessing.\n\nCommands are accepted **only** from the paired chat; **create** is rate-limited,\nand all lifecycle commands are idempotent per Telegram update id and audited (no\ntokens or prompts are logged).\nFor an automated proof of the wire path without a real bot, see\n`packages/coding-agent/scripts/g011-daemon-path-smoke.ts` (real native control\nendpoint + loopback WebSocket).\n",
|
|
52
53
|
"onboarding-packet.md": "# Sayknow-CLI Onboarding Packet\n\nThis packet is a docs-only, public-safe context seed for the `sayknow-cli` repository as inspected on 2026-06-01. It is intentionally a no-new-skill artifact: not a new workflow skill, command, agent, configuration surface, issue template, or runtime behavior.\n\n## Purpose in one paragraph\n\nSayknow-CLI is the `skc` coding-agent CLI and supporting monorepo. The product centers on a small public workflow loop: clarify with `deep-interview`, plan with `ralplan`, execute and verify through `ultragoal`, and use `team` only when parallel tmux workers are useful. The main product package is `packages/coding-agent/`; supporting packages provide LLM/provider access, agent runtime, TUI rendering, native helpers, stats, utilities, benchmarks, and Python RPC/bot integrations.\n\n## Fixed public surface\n\nKeep this invariant front-and-center when onboarding to the repo:\n\n- Default workflow skills: `deep-interview`, `ralplan`, `team`, `ultragoal`.\n- Public role agents: `executor`, `architect`, `planner`, `critic`.\n- Bundled default workflow skill sources live under `packages/coding-agent/src/defaults/skc/skills/`.\n- Bundled role-agent prompt sources live under `packages/coding-agent/src/prompts/agents/`.\n- Runtime state, specs, plans, goals, team state, and local overrides belong under `.skc/` for the product and `.omx/` only for this agent-run orchestration.\n\nDo not add a fifth default skill, fifth public role agent, new command, new config surface, or feature-intake behavior unless that product decision has already been made and the default-surface gates are updated.\n\n## Primary entrypoints\n\n| Area | Repo-relative path | Why it matters |\n| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- |\n| CLI bootstrap | `packages/coding-agent/src/cli.ts` | Registers top-level CLI commands and routes default launch behavior. |\n| Session launch | `packages/coding-agent/src/main.ts` | Converts CLI/runtime settings into agent-session creation and mode dispatch. |\n| Agent assembly | `packages/coding-agent/src/sdk.ts` | Loads settings, default skills, rules, tools, auth/model state, system prompt, and agent runtime. |\n| Built-in tools | `packages/coding-agent/src/tools/index.ts` | Registers file, shell, edit, search, browser, task/subagent, and related public coding-harness tools. Memory backends are private integrations, not public tools. |\n| Default skills | `packages/coding-agent/src/defaults/skc-defaults.ts` | Embeds and installs the four default workflow skills plus deep-interview fragments. |\n| Role agents | `packages/coding-agent/src/task/agents.ts` | Embeds bundled task-agent prompts; tests enforce public role-agent expectations. |\n| Product overview | `README.md` | Explains installation, product story, fixed workflow surface, and development entry commands. |\n| Architecture map | `docs/codebase-overview.md` | Public package map and runtime-flow reference. |\n\n## Package map\n\n- `packages/coding-agent/` — main `skc` CLI, workflows, session runtime, tool registry, discovery, settings, prompts, and tests.\n- `packages/ai/` — provider/model boundary, streaming, auth, model registry, retries, and provider integrations.\n- `packages/agent/` — stateful agent loop and append-only context runtime.\n- `packages/tui/` — terminal UI framework and rendering primitives.\n- `packages/natives/` plus `crates/*` — native helpers, Rust/N-API bindings, shell/PTY, text search, AST, filesystem, and media utilities.\n- `packages/utils/` — shared TypeScript utilities, logging, formatting, process helpers, JSON/frontmatter, and sanitization.\n- `packages/stats/` — local observability dashboard and session/model usage aggregation.\n- `packages/typescript-edit-benchmark/` — TypeScript edit benchmark tooling.\n- `python/skc-rpc/` — Python client for `skc --mode rpc`.\n- `python/roboskc/` — GitHub triage/fix bot that drives `skc --mode rpc`; this subtree has its own `AGENTS.md`.\n\n## Build, test, and validation commands\n\nPrefer targeted checks first, then broader checks when code changes justify them. For this docs-only packet, lightweight validation is enough.\n\n| Command | Scope | When to use |\n| --------------------------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------- |\n| `bun install` | Workspace dependencies | Initial local setup. |\n| `bun run install:defaults` | Local default install | Installs source-bundled default workflow definitions for local development. |\n| `bun packages/coding-agent/src/cli.ts --help` | CLI smoke/discovery | Fast source checkout CLI sanity check. |\n| `bun run check:ts` | Type/lint/default UI checks | Broad TypeScript validation; heavier than docs-only changes. |\n| `bun run test` | Full TS + Rust tests | Broad regression check; use for runtime/product changes. |\n| `bun run ci:test:smoke` | CLI version/help/stats worker smoke | Useful before release/install changes. |\n| `bun scripts/check-visible-definitions.ts` | Default surface gate | Required after workflow-definition changes. |\n| `bun scripts/verify-g002-gates.ts` | Rebrand/default-surface gate | Required after workflow-definition or public-surface changes. |\n| `bun scripts/rebrand-inventory.ts --strict` | Rebrand inventory gate | Required after workflow-definition or public-surface changes. |\n| `bun test packages/coding-agent/test/default-skc-definitions.test.ts` | Four-skills/four-agents contract | Required after default workflow/agent surface changes. |\n\nRepository rule: do not run `tsc` or `npx tsc`; use the Bun scripts above.\n\n## Danger zones\n\n- **Default surface expansion:** `packages/coding-agent/src/defaults/skc/skills/`, `packages/coding-agent/src/defaults/skc-defaults.ts`, `packages/coding-agent/src/prompts/agents/`, and model-assignment tests are contract-heavy. Changes here can accidentally alter the fixed four-skills/four-agents shape.\n- **CLI commands:** `packages/coding-agent/src/cli.ts` and `packages/coding-agent/src/commands/` define visible behavior. Adding commands or aliases is a product-surface change.\n- **Runtime/session assembly:** `packages/coding-agent/src/main.ts`, `packages/coding-agent/src/sdk.ts`, discovery, settings, tools, and system-prompt paths can affect every session.\n- **TUI/logging:** Avoid `console.log`, `console.warn`, or `console.error` inside `packages/coding-agent/`; use the centralized logger to avoid corrupting TUI rendering.\n- **Secrets/auth/config:** Keep `docs/secrets.md`, auth broker/gateway code, settings, and environment-variable docs public-safe. Do not expose tokens or private infrastructure.\n- **Native/Rust build:** `packages/natives/` and `crates/*` can require platform-specific toolchains and CI artifact behavior.\n- **Python bot subtree:** `python/roboskc/` has its own local instructions and trust boundaries.\n- **Generated model data:** Do not edit `packages/ai/src/models.json` directly; update generators/descriptors/resolvers and regenerate with `bun --cwd=packages/ai run generate-models`.\n\n## Unknowns worth preserving\n\n- Which onboarding packet shape will be most useful for future `skc` context ingestion is still an experiment, not a product contract.\n- Public issue #158 / `sayknow-deep-onboarding` context is summarized only from the user-provided prompt in this run; this packet does not add issue intake or feature workflow behavior.\n- Full CI may depend on runner/system dependencies and native artifacts; docs-only changes usually do not need the full matrix locally.\n- Some packages contain internal or hidden utility prompts/agents beyond the four public role agents. Public-facing docs should keep the four-role contract clear.\n\n## First safe tasks for a new contributor or agent\n\n1. Read `README.md`, `docs/codebase-overview.md`, and this packet.\n2. Run `bun packages/coding-agent/src/cli.ts --help` for a fast CLI surface check after dependencies are installed.\n3. For docs-only edits, run formatting/check commands that do not mutate runtime behavior.\n4. For default-surface edits, run the four required gates listed in the command table before claiming completion.\n5. For package code edits, start with the nearest package test, then escalate to `bun run check:ts` or `bun run test` as risk increases.\n6. Before changing `packages/coding-agent/src/defaults/skc/skills/`, `packages/coding-agent/src/prompts/agents/`, `packages/coding-agent/src/commands/`, or config/settings paths, write down whether the change alters public surface area.\n\n## Context seed checklist\n\nA future agent can use this packet as context if it preserves these constraints:\n\n- Keep changes public-safe and repo-relative.\n- Prefer docs and tests over new runtime abstractions for onboarding experiments.\n- Treat the fixed four-skills/four-agents shape as a product constraint.\n- Verify claims with repo files before summarizing them.\n- Report validation evidence and caveats instead of implying hidden automation.\n",
|
|
53
54
|
"onboarding-receipt.md": "# Onboarding Packet Receipt\n\n- Date: 2026-06-01\n- Scope: docs-only no-new-skill onboarding packet experiment for this repository.\n- Output files:\n - `docs/onboarding-packet.md`\n - `docs/onboarding-receipt.md`\n- Public-safe boundary: no secrets, tokens, hidden prompts, private infrastructure, internal ops, or private paths beyond repo-relative paths.\n- Product boundary: no new skill, command, agent slot, issue, config, or runtime behavior.\n\n## Evidence inspected\n\n- `README.md`\n- `docs/codebase-overview.md`\n- `package.json`\n- `packages/coding-agent/package.json`\n- `packages/coding-agent/src/cli.ts`\n- `packages/coding-agent/src/main.ts`\n- `packages/coding-agent/src/sdk.ts`\n- `packages/coding-agent/src/defaults/skc-defaults.ts`\n- `packages/coding-agent/src/task/agents.ts`\n- `packages/coding-agent/test/default-skc-definitions.test.ts`\n- `.github/workflows/ci.yml`\n- `.github/workflows/dev-ci.yml`\n\n## Result\n\nThe packet records repo purpose, package layout, main entrypoints, build/test commands, danger zones, unknowns, and first safe tasks without changing the product surface. It is suitable as a public context seed for future onboarding experiments, not as a feature intake mechanism.\n\n## Caveats\n\n- The attempted `omx question --input '<json>' --json` interview round failed before user input because the runtime reported no attached tmux client; no human answer was inferred from that failed call.\n- Public issue context is limited to the user-provided prompt summary for this run.\n- Full CI was not required for the docs-only artifact unless later code/runtime files change.\n",
|
|
54
55
|
"ooo-bridge-extension-contract.md": "# Ouroboros `ooo` bridge extension contract\n\nSKC exposes the `ooo` bridge through the existing extension input-event surface. It is not a default workflow skill, hook, slash command, or built-in agent.\n\n## Interception surface\n\nExtensions register an `input` handler:\n\n```ts\nimport { createOuroborosOooBridge } from \"@sayknow-cli/coding-agent/extensibility/extensions\";\n\nexport default function activate(skc) {\n skc.on(\"input\", createOuroborosOooBridge());\n}\n```\n\nThe handler matches only the bare exact prefix:\n\n- `ooo`\n- `ooo ...`\n\nIt does not match embedded or longer-token text such as `please ooo status`, `oooo`, or `/ooo`.\n\nThe extension runner already treats `InputEventResult.handled === true` as terminal: the input is not sent through normal model flow. An empty result (`{}`) means continue/pass-through, preserving existing chained input handlers and normal prompt handling.\n\n## Dispatch and result semantics\n\n`createOuroborosOooBridge()` is a small specialization of `createExactPrefixCommandBridge()`:\n\n- command: `ouroboros`\n- arguments: `dispatch`, then the full submitted input text\n- recursion guard variable: `_OUROBOROS_SKC_BRIDGE_DEPTH`\n- continue/pass-through exit code: `78`\n\nExit-code mapping:\n\n| Dispatch result | SKC input result |\n| --- | --- |\n| `0` | `{ handled: true }`; do not send input to the model. |\n| `78` | `{}`; continue/pass-through so SKC processes the input normally. |\n| any other non-zero | Surface an extension error notification using stderr, then stdout, then a generic exit-code message, and return `{ handled: true }`; the failed `ooo` command is terminal and is not sent to the model. |\n\n## Recursion guard\n\nBefore dispatch, the helper sets `_OUROBOROS_SKC_BRIDGE_DEPTH` to the next numeric depth and restores the previous value after dispatch finishes. A current numeric depth of `0` or `1` is dispatchable, which preserves concurrent independent interactive inputs while marking child dispatcher processes with depth `1`. A current numeric depth greater than `1`, or any non-empty non-numeric value, returns `{}` without dispatching.\n\nThis means the bridge allows exactly one inherited bridge-marked dispatcher level and blocks recursive re-entry from deeper bridge-marked children. The guard also passes through `event.source === \"extension\"` to avoid extension-originated messages re-entering the bridge.\n\n## Installation and discovery\n\nThe canonical install location is the agent extensions directory discovered by the native SKC provider:\n\n- user-level: `${SKC_CODING_AGENT_DIR:-$HOME/.skc/agent}/extensions`\n- project-level: `<cwd>/${SKC_CONFIG_DIR:-.skc}/extensions`\n\nFor native discovery, install one of:\n\n- `extensions/<name>.ts` or `extensions/<name>.js`\n- `extensions/<name>/index.ts` or `extensions/<name>/index.js`\n- `extensions/<name>/package.json` declaring extension entries\n\nThe loader scans one level under each `extensions` directory. Complex packages should use a package manifest instead of relying on recursive discovery.\n\n`SKC_CONFIG_DIR` selects the project config directory name. `SKC_CODING_AGENT_DIR` selects the user agent directory name under `$HOME`. The native provider resolves those locations before loading extension modules, skills, rules, hooks, and related capabilities.\n\nHooks are not the input bridge surface: `packages/coding-agent/src/capability/hook.ts` defines pre/post tool hooks only.\n",
|
|
@@ -74,7 +75,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
74
75
|
"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",
|
|
75
76
|
"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",
|
|
76
77
|
"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.ts`](../packages/coding-agent/src/sdk.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",
|
|
77
|
-
"rpc.md": "# RPC Protocol Reference\n\nRPC mode runs the coding agent as a newline-delimited JSON protocol over stdio.\n\n- **stdin**: commands (`RpcCommand`), `workflow_gate_response`, extension UI responses, and host-tool updates/results\n- **stdout**: a ready frame, command responses (`RpcResponse`), session/agent events, `workflow_gate`, extension UI requests, host-tool requests/cancellations\n\nPrimary implementation:\n\n- `src/modes/rpc/rpc-mode.ts`\n- `src/modes/rpc/rpc-types.ts`\n- `src/session/agent-session.ts`\n- `packages/agent/src/agent.ts`\n- `packages/agent/src/agent-loop.ts`\n\n## Startup\n\n```bash\nskc --mode rpc [regular CLI options]\n```\n\nBehavior notes:\n\n- `@file` CLI arguments are rejected in RPC mode.\n- RPC mode disables automatic session title generation by default to avoid an extra model call.\n- RPC mode resets workflow-altering `todo.*`, `task.*`, `async.*`, and `bash.autoBackground.*` settings to their built-in defaults instead of inheriting user overrides.\n- The process reads stdin as JSONL (`readJsonl(Bun.stdin.stream())`).\n- At startup it writes `{ \"type\": \"ready\" }` before processing commands.\n- When stdin closes, pending host-tool calls are rejected and the process exits with code `0`.\n- Responses/events are written as one JSON object per line.\n\n## Transport and Framing\n\nEach frame is a single JSON object followed by `\\n`.\n\nAgent session events are wrapped in canonical `event` frames. Ready, response, workflow gate, extension UI/error, host tool, and host URI frames remain flat.\n\n### Outbound frame categories (stdout)\n\n1. Ready frame (`{ type: \"ready\" }`)\n2. `RpcResponse` (`{ type: \"response\", ... }`)\n3. Canonical event frames wrapping `AgentSessionEvent` objects (`{ type: \"event\", ... }`)\n4. `RpcWorkflowGateEvent` (`{ type: \"workflow_gate\", ... }`)\n5. `RpcExtensionUIRequest` (`{ type: \"extension_ui_request\", ... }`)\n6. Host tool requests/cancellations (`host_tool_call`, `host_tool_cancel`)\n7. Host URI requests/cancellations (`host_uri_request`, `host_uri_cancel`)\n8. Extension errors (`{ type: \"extension_error\", extensionPath, event, error }`)\n\n### Inbound frame categories (stdin)\n\n1. `RpcCommand`\n2. `RpcWorkflowGateResponse` (`{ type: \"workflow_gate_response\", gate_id, answer }`)\n3. `RpcExtensionUIResponse` (`{ type: \"extension_ui_response\", ... }`)\n4. Host tool updates/results (`host_tool_update`, `host_tool_result`)\n5. Host URI results (`host_uri_result`)\n\n## Request/Response Correlation\n\nAll commands accept optional `id?: string`.\n\n- If provided, normal command responses echo the same `id`.\n- `RpcClient` relies on this for pending-request resolution.\n\nImportant edge behavior from runtime:\n\n- Unknown command responses are emitted with `id: undefined` (even if the request had an `id`).\n- Parse/handler exceptions in the input loop emit `command: \"parse\"` with `id: undefined`.\n- `prompt` and `abort_and_prompt` return immediate success, then may emit a later error response with the **same** id if async prompt scheduling fails.\n\n## Command Schema (canonical)\n\n`RpcCommand` is defined in `src/modes/rpc/rpc-types.ts`:\n\n### Prompting\n\n- `{ id?, type: \"prompt\", message: string, images?: ImageContent[], streamingBehavior?: \"steer\" | \"followUp\" }`\n- `{ id?, type: \"steer\", message: string, images?: ImageContent[] }`\n- `{ id?, type: \"follow_up\", message: string, images?: ImageContent[] }`\n- `{ id?, type: \"abort\" }`\n- `{ id?, type: \"abort_and_prompt\", message: string, images?: ImageContent[] }`\n- `{ id?, type: \"new_session\", parentSession?: string }`\n\n### State\n\n- `{ id?, type: \"get_state\", include?: (\"tools\" | \"dumpTools\" | \"systemPrompt\")[] }` (`dumpTools` is accepted as an alias for the older response field name.)\n- `{ id?, type: \"set_todos\", phases: TodoPhase[] }`\n- `{ id?, type: \"set_host_tools\", tools: RpcHostToolDefinition[] }`\n- `{ id?, type: \"set_host_uri_schemes\", schemes: RpcHostUriSchemeDefinition[] }`\n- `{ id?, type: \"workflow_gate_response\", gate_id: string, answer: unknown }`\n\n### Model\n\n- `{ id?, type: \"set_model\", provider: string, modelId: string }`\n- `{ id?, type: \"cycle_model\" }`\n- `{ id?, type: \"get_available_models\" }`\n\n### Thinking\n\n- `{ id?, type: \"set_thinking_level\", level: ThinkingLevel }`\n- `{ id?, type: \"cycle_thinking_level\" }`\n\n### Queue modes\n\n- `{ id?, type: \"set_steering_mode\", mode: \"all\" | \"one-at-a-time\" }`\n- `{ id?, type: \"set_follow_up_mode\", mode: \"all\" | \"one-at-a-time\" }`\n- `{ id?, type: \"set_interrupt_mode\", mode: \"immediate\" | \"wait\" }`\n\n### Compaction\n\n- `{ id?, type: \"compact\", customInstructions?: string }`\n- `{ id?, type: \"set_auto_compaction\", enabled: boolean }`\n\n### Retry\n\n- `{ id?, type: \"set_auto_retry\", enabled: boolean }`\n- `{ id?, type: \"abort_retry\" }`\n\n### Bash\n\n- `{ id?, type: \"bash\", command: string }`\n- `{ id?, type: \"abort_bash\" }`\n\n### Session\n\n- `{ id?, type: \"get_session_stats\" }`\n- `{ id?, type: \"export_html\", outputPath?: string }`\n- `{ id?, type: \"switch_session\", sessionPath: string }`\n- `{ id?, type: \"branch\", entryId: string }`\n- `{ id?, type: \"get_branch_messages\" }`\n- `{ id?, type: \"get_last_assistant_text\" }`\n- `{ id?, type: \"set_session_name\", name: string }`\n\n### Messages\n\n- `{ id?, type: \"get_messages\" }`\n\n## Response Schema\n\nAll command results use `RpcResponse`:\n\n- Success: `{ id?, type: \"response\", command: <command>, success: true, data?: ... }`\n- Failure: `{ id?, type: \"response\", command: string, success: false, error: string | object }`; typed control-plane failures use object-valued errors such as `{ \"code\": \"scope_denied\", ... }`.\n\nData payloads are command-specific and defined in `rpc-types.ts`.\n\n\nBy default, `get_state` omits large static fields. Request `include: [\"tools\"]` to include `dumpTools`, `include: [\"systemPrompt\"]` to include `systemPrompt`, or both when a host needs a one-shot full session dump.\n### `get_state` payload\n\n```json\n{\n \"model\": { \"provider\": \"...\", \"id\": \"...\" },\n \"thinkingLevel\": \"off|minimal|low|medium|high|xhigh\",\n \"isStreaming\": false,\n \"isCompacting\": false,\n \"steeringMode\": \"all|one-at-a-time\",\n \"followUpMode\": \"all|one-at-a-time\",\n \"interruptMode\": \"immediate|wait\",\n \"sessionFile\": \"...\",\n \"sessionId\": \"...\",\n \"sessionName\": \"...\",\n \"autoCompactionEnabled\": true,\n \"messageCount\": 0,\n \"queuedMessageCount\": 0,\n \"todoPhases\": [\n {\n \"id\": \"phase-1\",\n \"name\": \"Todos\",\n \"tasks\": [\n {\n \"id\": \"task-1\",\n \"content\": \"Map the tool surface\",\n \"status\": \"in_progress\"\n }\n ]\n }\n ],\n \"contextUsage\": {\n \"tokens\": 0,\n \"contextWindow\": 200000,\n \"percent\": 0\n }\n // Optional with include: [\"systemPrompt\"]:\n // \"systemPrompt\": [\"...\"],\n // Optional with include: [\"tools\"] (or [\"dumpTools\"]):\n // \"dumpTools\": [\n // { \"name\": \"read\", \"description\": \"Read files and URLs\", \"parameters\": {} }\n // ]\n}\n```\n\n### `set_todos` payload\n\nReplaces the in-memory todo state for the current session and returns the normalized phase list:\n\n```json\n{\n \"id\": \"req_2\",\n \"type\": \"set_todos\",\n \"phases\": [\n {\n \"id\": \"phase-1\",\n \"name\": \"Evaluation\",\n \"tasks\": [\n {\n \"id\": \"task-1\",\n \"content\": \"Map the read tool surface\",\n \"status\": \"in_progress\"\n },\n {\n \"id\": \"task-2\",\n \"content\": \"Exercise edit operations\",\n \"status\": \"pending\"\n }\n ]\n }\n ]\n}\n```\n\nThis is useful for hosts that want to pre-seed a plan before the first prompt.\n\n### `set_host_tools` payload\n\nReplaces the current set of host-owned tools that the RPC server may call back\ninto over stdio:\n\n```json\n{\n \"id\": \"req_3\",\n \"type\": \"set_host_tools\",\n \"tools\": [\n {\n \"name\": \"echo_host\",\n \"label\": \"Echo Host\",\n \"description\": \"Echo a value from the embedding host\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"message\": { \"type\": \"string\" }\n },\n \"required\": [\"message\"],\n \"additionalProperties\": false\n }\n }\n ]\n}\n```\n\nThe response payload is:\n\n```json\n{\n \"toolNames\": [\"echo_host\"]\n}\n```\n\nThese tools are added to the active session tool registry before the next model\ncall. Re-sending `set_host_tools` replaces the previous host-owned set.\n\n### `set_host_uri_schemes` payload\n\nReplaces the current set of host-owned URL schemes the RPC server should\ndispatch reads/writes through:\n\n```json\n{\n \"id\": \"req_4\",\n \"type\": \"set_host_uri_schemes\",\n \"schemes\": [\n {\n \"scheme\": \"db\",\n \"description\": \"Virtual db row files\",\n \"writable\": true,\n \"immutable\": false\n }\n ]\n}\n```\n\nThe response payload is:\n\n```json\n{\n \"schemes\": [\"db\"]\n}\n```\n\nSchemes are case-insensitive on the wire and normalized to lowercase before\nthe response is sent. Re-sending `set_host_uri_schemes` replaces the entire\nprevious set — schemes missing from the new list are unregistered.\n\n## Event Stream Schema\n\nRPC mode forwards `AgentSessionEvent` objects from `AgentSession.subscribe(...)` as canonical `event` frames:\n\n```json\n{\n \"type\": \"event\",\n \"protocol_version\": 2,\n \"session_id\": \"...\",\n \"seq\": 1,\n \"frame_id\": \"...\",\n \"payload\": {\n \"event_type\": \"agent_start\",\n \"event\": { \"type\": \"agent_start\" }\n }\n}\n```\n\n`seq` is monotonic per session starting at `1`. `payload.event_type` duplicates the inner event `type` for routing, and `payload.event` contains the original `AgentSessionEvent` fields.\n\nCommon inner event types:\n\n- `agent_start`, `agent_end`\n- `turn_start`, `turn_end`\n- `message_start`, `message_update`, `message_end`\n- `tool_execution_start`, `tool_execution_update`, `tool_execution_end`\n- `auto_compaction_start`, `auto_compaction_end`\n- `auto_retry_start`, `auto_retry_end`\n- `ttsr_triggered`\n- `todo_reminder`\n- `todo_auto_clear`\n\nNon-event stdout categories remain flat: `ready`, `response`, `workflow_gate`, `extension_ui_request`, `extension_error`, `host_tool_call`, `host_tool_cancel`, `host_uri_request`, and `host_uri_cancel`.\n\n`message_update` includes streaming deltas in the inner event's `assistantMessageEvent` (text/thinking/toolcall deltas).\n\nExtension runner errors are emitted separately as flat frames:\n\n```json\n{\n \"type\": \"extension_error\",\n \"extensionPath\": \"...\",\n \"event\": \"...\",\n \"error\": \"...\"\n}\n```\n\n## Prompt/Queue Concurrency and Ordering\n\nThis is the most important operational behavior.\n\n### Immediate ack vs completion\n\n`prompt` and `abort_and_prompt` are **acknowledged immediately**:\n\n```json\n{ \"id\": \"req_1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true }\n```\n\nThat means:\n\n- command acceptance != run completion\n- final completion is observed via `agent_end`\n\n### While streaming\n\n`AgentSession.prompt()` requires `streamingBehavior` during active streaming:\n\n- `\"steer\"` => queued steering message (interrupt path)\n- `\"followUp\"` => queued follow-up message (post-turn path)\n\nIf omitted during streaming, prompt fails.\n\n### Queue defaults\n\nFrom `packages/agent/src/agent.ts` defaults:\n\n- `steeringMode`: `\"one-at-a-time\"`\n- `followUpMode`: `\"one-at-a-time\"`\n- `interruptMode`: `\"immediate\"`\n\n### Mode semantics\n\n- `set_steering_mode` / `set_follow_up_mode`\n - `\"one-at-a-time\"`: dequeue one queued message per turn\n - `\"all\"`: dequeue entire queue at once\n- `set_interrupt_mode`\n - `\"immediate\"`: tool execution checks steering between tool calls; pending steering can abort remaining tool calls in the turn\n - `\"wait\"`: defer steering until turn completion\n\n## Workflow Gate Sub-Protocol\n\nInteractive workflow stages emit a machine-addressable gate frame before the legacy extension UI request:\n\n```json\n{\n \"type\": \"workflow_gate\",\n \"gate_id\": \"wg_4845_ralplan_000001\",\n \"stage\": \"ralplan\",\n \"kind\": \"approval\",\n \"schema\": { \"type\": \"string\", \"enum\": [\"approve\", \"request-changes\", \"reject\"] },\n \"schema_hash\": \"<sha256 of canonical schema>\",\n \"options\": [{ \"value\": \"approve\", \"label\": \"Approve execution\" }],\n \"context\": { \"title\": \"Approve plan?\", \"summary\": \"…\" },\n \"created_at\": \"2026-06-05T05:00:00.000Z\",\n \"required\": true\n}\n```\n\nFields:\n\n- `gate_id`: run-scoped, monotonic, stable id of the form `wg_<run>_<stage>_NNNNNN`.\n- `stage`: one of `\"deep-interview\"`, `\"ralplan\"`, or `\"ultragoal\"` (`team` is reserved and rejected for v1).\n- `kind`: one of `\"question\"`, `\"approval\"`, or `\"execution\"`.\n- `schema`: documented JSON Schema 2020-12 subset for the expected answer; `schema_hash` is the canonical hash of `schema` and equals the server-side validation hash.\n- `options`: optional `RpcWorkflowGateOption[]` (`{ value, label, description? }`), emitted for select-style gates.\n- `context`: `RpcWorkflowGateContext` (`title`, `prompt`, `summary`, `stage_state`, `artifact_refs`, `language`).\n- `created_at`: ISO timestamp the gate was opened; `required` is always `true`.\n\nHosts answer with:\n\n```json\n{ \"id\": \"resp_1\", \"type\": \"workflow_gate_response\", \"gate_id\": \"wg_4845_ralplan_000001\", \"answer\": \"approve\" }\n```\n\nA valid answer resolves the pending gate and returns:\n\n```json\n{ \"id\": \"resp_1\", \"type\": \"response\", \"command\": \"workflow_gate_response\", \"success\": true }\n```\n\nA schema mismatch is **not** a command failure: the response succeeds and the\nresolution data carries `status: \"rejected\"` plus a typed validation `error`\nwith code `invalid_workflow_gate_answer`:\n\n```json\n{\n \"id\": \"resp_1\",\n \"type\": \"response\",\n \"command\": \"workflow_gate_response\",\n \"success\": true,\n \"data\": {\n \"gate_id\": \"wg_1\",\n \"status\": \"rejected\",\n \"answer_hash\": \"…\",\n \"resolved_at\": \"…\",\n \"error\": {\n \"code\": \"invalid_workflow_gate_answer\",\n \"gate_id\": \"wg_1\",\n \"schema_hash\": \"…\",\n \"errors\": [{ \"path\": \"/answer\", \"keyword\": \"type\", \"message\": \"must be boolean\" }]\n }\n }\n}\n```\n\nAnswering a gate that does not exist is a recoverable command failure carrying\nthe broker error code `unknown_gate` (other broker codes are `already_resolved`,\n`idempotency_conflict`, and `invalid_workflow_stage`).\n## Extension UI Sub-Protocol\n\nExtensions in RPC mode use request/response UI frames.\n\n### Outbound request\n\n`RpcExtensionUIRequest` (`type: \"extension_ui_request\"`) methods:\n\n- `select`, `confirm`, `input`, `editor`, `cancel`\n- `notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`\n\nRuntime note:\n\n- Automatic session title generation is disabled in RPC mode, and `setTitle` UI\n requests are also suppressed by default because most hosts do not have a\n meaningful terminal-title surface. Set `SKC_RPC_EMIT_TITLE=1` to opt back in to\n the UI event only.\n\nExample:\n\n```json\n{\n \"type\": \"extension_ui_request\",\n \"id\": \"123\",\n \"method\": \"confirm\",\n \"title\": \"Confirm\",\n \"message\": \"Continue?\",\n \"timeout\": 30000\n}\n```\n\n### Inbound response\n\n`RpcExtensionUIResponse` (`type: \"extension_ui_response\"`):\n\n- `{ type: \"extension_ui_response\", id: string, value: string }`\n- `{ type: \"extension_ui_response\", id: string, confirmed: boolean }`\n- `{ type: \"extension_ui_response\", id: string, cancelled: true, timedOut?: boolean }`\n\nIf a dialog has a timeout, RPC mode resolves to a default value when timeout/abort fires.\n\n## Host Tool Sub-Protocol\n\nRPC hosts can expose custom tools to the agent by sending `set_host_tools`, then\nserving execution requests over the same transport.\n\n### Outbound request\n\nWhen the agent wants the host to execute one of those tools, RPC mode emits:\n\n```json\n{\n \"type\": \"host_tool_call\",\n \"id\": \"host_1\",\n \"toolCallId\": \"toolu_123\",\n \"toolName\": \"echo_host\",\n \"arguments\": { \"message\": \"hello\" }\n}\n```\n\nIf the tool execution is later aborted, RPC mode emits:\n\n```json\n{\n \"type\": \"host_tool_cancel\",\n \"id\": \"host_cancel_1\",\n \"targetId\": \"host_1\"\n}\n```\n\n### Inbound updates and completion\n\nHosts can optionally stream progress:\n\n```json\n{\n \"type\": \"host_tool_update\",\n \"id\": \"host_1\",\n \"partialResult\": {\n \"content\": [{ \"type\": \"text\", \"text\": \"working\" }]\n }\n}\n```\n\nCompletion uses:\n\n```json\n{\n \"type\": \"host_tool_result\",\n \"id\": \"host_1\",\n \"result\": {\n \"content\": [{ \"type\": \"text\", \"text\": \"done\" }]\n }\n}\n```\n\nSet top-level `isError: true` on `host_tool_result` to reject the pending host tool call and surface the returned text content as a tool error.\n\n## Host URI Sub-Protocol\n\nRPC hosts can also own custom URL schemes (virtual files). After\n`set_host_uri_schemes`, every read of `<scheme>://…` and write of\n`<scheme>://…` (when registered as `writable`) is bounced back to the host\nover the same transport.\n\n### Outbound request\n\nWhen a session tool resolves a host-owned URL, RPC mode emits:\n\n```json\n{\n \"type\": \"host_uri_request\",\n \"id\": \"uri_1\",\n \"operation\": \"read\",\n \"url\": \"db://users/42\"\n}\n```\n\nWrites look the same with `\"operation\": \"write\"` and an additional\n`\"content\": \"...\"` field carrying the full replacement bytes.\n\nIf the request is later aborted (caller cancels, session ends), RPC mode\nemits:\n\n```json\n{\n \"type\": \"host_uri_cancel\",\n \"id\": \"uri_cancel_1\",\n \"targetId\": \"uri_1\"\n}\n```\n\n### Inbound result\n\nFor successful reads:\n\n```json\n{\n \"type\": \"host_uri_result\",\n \"id\": \"uri_1\",\n \"content\": \"id=42\\nname=Alice\\n\",\n \"contentType\": \"text/plain\",\n \"notes\": [\"fresh from cache\"],\n \"immutable\": false\n}\n```\n\nFor successful writes, omit content:\n\n```json\n{ \"type\": \"host_uri_result\", \"id\": \"uri_1\" }\n```\n\nTo reject the request, set `isError: true` and either populate `error` with\na message or fall back to `content` for textual error surfacing:\n\n```json\n{\n \"type\": \"host_uri_result\",\n \"id\": \"uri_1\",\n \"isError\": true,\n \"error\": \"row 42 not found\"\n}\n```\n\n### Constraints\n\n- The agent's `edit` tool does not target host URIs. Hosts that want to\n mutate virtual files expose `write` and let the model use the `write` tool\n with replacement content.\n- Schemes are global to the process; `set_host_uri_schemes` replaces the\n previous set, unregistering anything not in the new list.\n- Schemes are normalized to lowercase before registration.\n\n## Error Model and Recoverability\n\n### Command-level failures\n\nFailures are `success: false` with string `error`.\n\n```json\n{\n \"id\": \"req_2\",\n \"type\": \"response\",\n \"command\": \"set_model\",\n \"success\": false,\n \"error\": \"Model not found: provider/model\"\n}\n```\n\n### Recoverability expectations\n\n- Most command failures are recoverable; process remains alive.\n- Malformed JSONL / parse-loop exceptions emit a `parse` error response and continue reading subsequent lines.\n- Empty `set_session_name` is rejected (`Session name cannot be empty`).\n- Extension UI responses with unknown `id` are ignored.\n- Process termination conditions are stdin close or explicit extension-triggered shutdown after the current command.\n\n## Compact Command Flows\n\n### 1) Prompt and stream\n\nstdin:\n\n```json\n{ \"id\": \"req_1\", \"type\": \"prompt\", \"message\": \"Summarize this repo\" }\n```\n\nstdout sequence (typical):\n\n```json\n{ \"id\": \"req_1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true }\n{ \"type\": \"agent_start\" }\n{ \"type\": \"message_update\", \"assistantMessageEvent\": { \"type\": \"text_delta\", \"delta\": \"...\" }, \"message\": { \"role\": \"assistant\", \"content\": [] } }\n{ \"type\": \"agent_end\", \"messages\": [] }\n```\n\n### 2) Prompt during streaming with explicit queue policy\n\nstdin:\n\n```json\n{\n \"id\": \"req_2\",\n \"type\": \"prompt\",\n \"message\": \"Also include risks\",\n \"streamingBehavior\": \"followUp\"\n}\n```\n\n### 3) Inspect and tune queue behavior\n\nstdin:\n\n```json\n{ \"id\": \"q1\", \"type\": \"get_state\" }\n{ \"id\": \"q2\", \"type\": \"set_steering_mode\", \"mode\": \"all\" }\n{ \"id\": \"q3\", \"type\": \"set_interrupt_mode\", \"mode\": \"wait\" }\n```\n\n### 4) Extension UI round trip\n\nstdout:\n\n```json\n{\n \"type\": \"extension_ui_request\",\n \"id\": \"ui_7\",\n \"method\": \"input\",\n \"title\": \"Branch name\",\n \"placeholder\": \"feature/...\"\n}\n```\n\nstdin:\n\n```json\n{ \"type\": \"extension_ui_response\", \"id\": \"ui_7\", \"value\": \"feature/rpc-host\" }\n```\n\n## OpenClaw / Hermes host integrations\n\nFor OpenClaw- or Hermes-style hosts, keep MCP servers and skills on the host side and expose the selected capabilities through RPC host tools. Do not import SKC runtime MCP internals directly; those package paths are intentionally quarantined. See [OpenClaw / Hermes RPC integration notes](./openclaw-hermes-rpc-integration.md).\n\n## Notes on `RpcClient` helper\n\n`src/modes/rpc/rpc-client.ts` is a convenience wrapper, not the protocol definition.\n\nCurrent helper characteristics:\n\n- Spawns `bun <cliPath> --mode rpc`\n- Correlates responses by generated `req_<n>` ids\n- Dispatches recognized `AgentEvent` types to event listeners\n- Dispatches top-level `workflow_gate` frames to `onWorkflowGate()` listeners\n- Supports host-owned custom tools via `setCustomTools()` and automatic handling of `host_tool_call` / `host_tool_cancel`\n- Exposes `respondGate()` for `workflow_gate_response` and waits for the accepted/rejected resolution envelope\n- Does **not** expose helper methods for every protocol command (for example, `set_interrupt_mode` and `set_session_name` are in protocol types but not wrapped as dedicated methods)\n\nUse raw protocol frames if you need complete surface coverage.\n\n## Workflow gates (agent-driven lifecycle)\n\nThe workflow-gate contract makes every human-gated lifecycle moment\n(deep-interview questions, ralplan approval, ultragoal execution sign-off)\nmachine-addressable so an external agent can answer it over RPC without\nscreen-scraping.\n\n### Outbound event: `workflow_gate`\n\n```json\n{\n \"type\": \"workflow_gate\",\n \"gate_id\": \"wg_4845_ralplan_000001\",\n \"stage\": \"ralplan\",\n \"kind\": \"approval\",\n \"schema\": { \"type\": \"string\", \"enum\": [\"approve\", \"request-changes\", \"reject\"] },\n \"schema_hash\": \"<sha256 of canonical schema>\",\n \"options\": [{ \"value\": \"approve\", \"label\": \"Approve execution\" }],\n \"context\": { \"title\": \"Approve plan?\", \"summary\": \"…\" },\n \"created_at\": \"2026-06-05T05:00:00.000Z\",\n \"required\": true\n}\n```\n\n- `gate_id` is **run-scoped and monotonic**: `wg_<run>_<stage>_<NNNNNN>`.\n- `stage` is one of `deep-interview` | `ralplan` | `ultragoal`. `team` is\n reserved and rejected for v1 (single-agent only).\n- `kind` is `question` | `approval` | `execution`.\n- `schema` is a **documented subset of JSON Schema 2020-12**. Supported keywords:\n `type`, `enum`, `const`, `properties`, `required`, `additionalProperties`,\n `items`, `minLength`, `maxLength`, `minimum`, `maximum`, `title`,\n `description`, `oneOf`, `anyOf`. Any other keyword is rejected at gate\n construction (`invalid_workflow_gate_schema`) so the server never advertises a\n schema it will not validate. `schema_hash` equals the server-side validation\n hash for that gate.\n\n### Inbound command: `workflow_gate_response`\n\n```json\n{ \"type\": \"workflow_gate_response\", \"gate_id\": \"wg_4845_ralplan_000001\", \"answer\": \"approve\", \"idempotency_key\": \"k1\" }\n```\n\nThe answer is validated against the advertised schema **before acceptance**:\n\n- Valid → resolution persisted before the workflow advances; response:\n `{ \"type\": \"response\", \"command\": \"workflow_gate_response\", \"success\": true, \"data\": { \"gate_id\": \"…\", \"status\": \"accepted\", \"answer_hash\": \"…\", \"resolved_at\": \"…\" } }`.\n- Invalid → the gate stays **pending** and the resolution carries a typed\n `invalid_workflow_gate_answer` error listing each `{ path, keyword, message, expected? }`.\n- Idempotency: replaying the same `idempotency_key` + identical body returns the\n cached resolution; the same key with a different body is an\n `idempotency_conflict`; answering an already-accepted gate is `already_resolved`.\n- Client helpers wait for this accepted/rejected resolution envelope; they must not treat the write of `workflow_gate_response` itself as completion.\n\n### Entering unattended mode: `negotiate_unattended`\n\nUnattended (zero-human) operation is **fail-closed**. The external agent must\ndeclare its budget, scopes, and action allowlist up front:\n\n```json\n{\n \"type\": \"negotiate_unattended\",\n \"declaration\": {\n \"actor\": \"openclaw/hermes\",\n \"budget\": { \"max_tokens\": 2000000, \"max_tool_calls\": 5000, \"max_wall_time_ms\": 3600000, \"max_cost_usd\": 20 },\n \"scopes\": [\"prompt\", \"control\", \"bash\"],\n \"action_allowlist\": [\"bash.readonly\", \"file.write\"]\n }\n}\n```\n\nA missing or partial declaration refuses unattended mode. Budget, scope, and\naudit enforcement are layered on this contract by the unattended control plane\n(see issues #318/#319/#320). Attended mode is unaffected: clients that never send\n`negotiate_unattended` keep the existing extension-UI / permission behavior.\n\n\n> **Status (live, #315/#318/#321):** the `workflow_gate` /\n> `workflow_gate_response` / `negotiate_unattended` frames, the answer-schema\n> validator, and the durable gate broker are defined, tested, and wired into\n> live session dispatch. When an unattended control plane is attached to the\n> session, `dispatchRpcCommand` routes `negotiate_unattended` and\n> `workflow_gate_response` through it (see\n> `packages/coding-agent/src/modes/shared/agent-wire/command-dispatch.ts`); a\n> session without that control plane returns a typed \"not available\" error for\n> these frames rather than silently dropping them.\n\n\n### Answering gates from a client (#322)\n\nBoth clients expose typed `workflow_gate` receive + respond helpers so an agent\ncan answer a gate from its own memory via a callback.\n\nFor bridge sessions, gate responses are **not** posted through `/commands`. The\nclient must first own the UI/control plane, then post the answer body to\n`POST /v1/sessions/{session_id}/ui-responses/{gate_id}` with\n`X-SKC-Bridge-Owner-Token: <ownerToken>`. `Idempotency-Key` may be supplied as a\nheader and the same value is also accepted in the JSON body as `idempotency_key`.\n\n`@sayknow-cli/bridge-client` (TypeScript):\n\n```ts\nimport { BridgeClient } from \"@sayknow-cli/bridge-client\";\n\nconst client = new BridgeClient({ baseUrl, token });\n// Headless policy: every received gate is routed to the resolver and answered.\nfor await (const { gate, answer } of client.consumeWorkflowGates(sessionId, ownerToken, gate => {\n if (gate.kind === \"approval\") return { decision: \"approve\" };\n if (gate.kind === \"question\") return { selected: [gate.options?.[0]?.value], other: false };\n return { decision: \"approve\" };\n})) {\n console.log(`answered ${gate.gate_id} (${gate.kind}) with`, answer);\n}\n// Or answer a single gate directly:\nawait client.respondGate(sessionId, gateId, ownerToken, { decision: \"approve\" });\n```\n\n`python/skc-rpc` (Python):\n\n```python\nfrom skc_rpc import RpcClient, WorkflowGate\n\nclient = RpcClient(executable=\"skc\")\n\ndef resolver(gate: WorkflowGate) -> object:\n if gate.kind == \"approval\":\n return {\"decision\": \"approve\"}\n if gate.kind == \"question\":\n return {\"selected\": [gate.options[0].value] if gate.options else [], \"other\": False}\n return {\"decision\": \"approve\"}\n\n# Headless policy: route every received gate to the resolver and respond.\nclient.run_workflow_gate_policy(resolver)\nclient.start()\n# Or answer a single gate directly: client.respond_gate(gate_id, {\"decision\": \"approve\"})\n```\n",
|
|
78
|
+
"rpc.md": "# RPC Protocol Reference\n\nRPC mode runs the coding agent as a newline-delimited JSON protocol over stdio.\n\n- **stdin**: commands (`RpcCommand`), `workflow_gate_response`, extension UI responses, and host-tool updates/results\n- **stdout**: a ready frame, command responses (`RpcResponse`), session/agent events, `workflow_gate`, extension UI requests, host-tool requests/cancellations\n\nPrimary implementation:\n\n- `src/modes/rpc/rpc-mode.ts`\n- `src/modes/rpc/rpc-types.ts`\n- `src/session/agent-session.ts`\n- `packages/agent/src/agent.ts`\n- `packages/agent/src/agent-loop.ts`\n\n## Startup\n\n```bash\nskc --mode rpc [regular CLI options]\n```\n\nBehavior notes:\n\n- `@file` CLI arguments are rejected in RPC mode.\n- RPC mode disables automatic session title generation by default to avoid an extra model call.\n- RPC mode resets workflow-altering `todo.*`, `task.*`, `async.*`, and `bash.autoBackground.*` settings to their built-in defaults instead of inheriting user overrides.\n- The process reads stdin as JSONL (`readJsonl(Bun.stdin.stream())`).\n- At startup it writes `{ \"type\": \"ready\" }` before processing commands.\n- When stdin closes, pending host-tool calls are rejected and the process exits with code `0`.\n- Responses/events are written as one JSON object per line.\n\n## Transport and Framing\n\nEach frame is a single JSON object followed by `\\n`.\n\nAgent session events are wrapped in canonical `event` frames. Ready, response, workflow gate, extension UI/error, host tool, and host URI frames remain flat.\n\n### Outbound frame categories (stdout)\n\n1. Ready frame (`{ type: \"ready\" }`)\n2. `RpcResponse` (`{ type: \"response\", ... }`)\n3. Canonical event frames wrapping `AgentSessionEvent` objects (`{ type: \"event\", ... }`)\n4. `RpcWorkflowGateEvent` (`{ type: \"workflow_gate\", ... }`)\n5. `RpcExtensionUIRequest` (`{ type: \"extension_ui_request\", ... }`)\n6. Host tool requests/cancellations (`host_tool_call`, `host_tool_cancel`)\n7. Host URI requests/cancellations (`host_uri_request`, `host_uri_cancel`)\n8. Extension errors (`{ type: \"extension_error\", extensionPath, event, error }`)\n\n### Inbound frame categories (stdin)\n\n1. `RpcCommand`\n2. `RpcWorkflowGateResponse` (`{ type: \"workflow_gate_response\", gate_id, answer }`)\n3. `RpcExtensionUIResponse` (`{ type: \"extension_ui_response\", ... }`)\n4. Host tool updates/results (`host_tool_update`, `host_tool_result`)\n5. Host URI results (`host_uri_result`)\n\n## Request/Response Correlation\n\nAll commands accept optional `id?: string`.\n\n- If provided, normal command responses echo the same `id`.\n- `RpcClient` relies on this for pending-request resolution.\n\nImportant edge behavior from runtime:\n\n- Unknown command responses are emitted with `id: undefined` (even if the request had an `id`).\n- Parse/handler exceptions in the input loop emit `command: \"parse\"` with `id: undefined`.\n- `prompt` and `abort_and_prompt` return immediate success, then may emit a later error response with the **same** id if async prompt scheduling fails.\n\n## Command Schema (canonical)\n\n`RpcCommand` is defined in `src/modes/rpc/rpc-types.ts`:\n\n### Prompting\n\n- `{ id?, type: \"prompt\", message: string, images?: ImageContent[], streamingBehavior?: \"steer\" | \"followUp\" }`\n- `{ id?, type: \"steer\", message: string, images?: ImageContent[] }`\n- `{ id?, type: \"follow_up\", message: string, images?: ImageContent[] }`\n- `{ id?, type: \"abort\" }`\n- `{ id?, type: \"abort_and_prompt\", message: string, images?: ImageContent[] }`\n- `{ id?, type: \"new_session\", parentSession?: string }`\n\n### State\n\n- `{ id?, type: \"get_state\", include?: (\"tools\" | \"dumpTools\" | \"systemPrompt\")[] }` (`dumpTools` is accepted as an alias for the older response field name.)\n- `{ id?, type: \"set_todos\", phases: TodoPhase[] }`\n- `{ id?, type: \"set_host_tools\", tools: RpcHostToolDefinition[] }`\n- `{ id?, type: \"set_host_uri_schemes\", schemes: RpcHostUriSchemeDefinition[] }`\n- `{ id?, type: \"workflow_gate_response\", gate_id: string, answer: unknown }`\n\n### Model\n\n- `{ id?, type: \"set_model\", provider: string, modelId: string }`\n- `{ id?, type: \"set_default_model_selection\", provider: string, modelId: string, thinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" }`\n- `{ id?, type: \"cycle_model\" }`\n- `{ id?, type: \"get_available_models\" }`\n\n### Thinking\n\n- `{ id?, type: \"set_thinking_level\", level: ThinkingLevel }`\n- `{ id?, type: \"cycle_thinking_level\" }`\n\n### Queue modes\n\n- `{ id?, type: \"set_steering_mode\", mode: \"all\" | \"one-at-a-time\" }`\n- `{ id?, type: \"set_follow_up_mode\", mode: \"all\" | \"one-at-a-time\" }`\n- `{ id?, type: \"set_interrupt_mode\", mode: \"immediate\" | \"wait\" }`\n\n### Compaction\n\n- `{ id?, type: \"compact\", customInstructions?: string }`\n- `{ id?, type: \"set_auto_compaction\", enabled: boolean }`\n\n### Retry\n\n- `{ id?, type: \"set_auto_retry\", enabled: boolean }`\n- `{ id?, type: \"abort_retry\" }`\n\n### Bash\n\n- `{ id?, type: \"bash\", command: string }`\n- `{ id?, type: \"abort_bash\" }`\n\n### Session\n\n- `{ id?, type: \"get_session_stats\" }`\n- `{ id?, type: \"export_html\", outputPath?: string }`\n- `{ id?, type: \"switch_session\", sessionPath: string }`\n- `{ id?, type: \"branch\", entryId: string }`\n- `{ id?, type: \"get_branch_messages\" }`\n- `{ id?, type: \"get_last_assistant_text\" }`\n- `{ id?, type: \"set_session_name\", name: string }`\n\n### Messages\n\n- `{ id?, type: \"get_messages\" }`\n\n## Response Schema\n\nAll command results use `RpcResponse`:\n\n- Success: `{ id?, type: \"response\", command: <command>, success: true, data?: ... }`\n- Failure: `{ id?, type: \"response\", command: string, success: false, error: string | object }`; typed control-plane failures use object-valued errors such as `{ \"code\": \"scope_denied\", ... }`.\n\nData payloads are command-specific and defined in `rpc-types.ts`.\n\n### `set_default_model_selection` contract\n\nRequest:\n\n```json\n{ \"id\": \"model-default-1\", \"type\": \"set_default_model_selection\", \"provider\": \"openai\", \"modelId\": \"gpt-5.6\", \"thinkingLevel\": \"high\" }\n```\n\nCorrelated success response:\n\n```json\n{ \"id\": \"model-default-1\", \"type\": \"response\", \"command\": \"set_default_model_selection\", \"success\": true, \"data\": { \"provider\": \"openai\", \"modelId\": \"gpt-5.6\", \"thinkingLevel\": \"high\" } }\n```\n\n`thinkingLevel` may be omitted or set to a concrete level. `\"inherit\"` is rejected. A requested reasoning level is clamped to the selected model's supported range. A non-reasoning model always resolves to `off`. The response always reports the effective concrete level that was persisted and applied.\n\nThis is an ordered mutation. It waits behind earlier mutations and, if the agent is streaming, waits for an active stream to become idle. It acknowledges success only after the machine-global selector is durably written and the selected model and effective level are recorded as the active session default. The selection therefore applies to the next message; it does not change the model of a response already being streamed.\n\nThe durable setting is a fallback, not an unconditional override. Project-level model-role policy overrides the machine-global selector, and a resumed session's recorded default model overrides it. Fresh sessions without either higher-precedence source consume the machine-global selector, including its effective thinking-level suffix.\n\n`set_default_model_selection` belongs to the shared agent-wire command surface. If a Bridge command endpoint is enabled by an embedding, the command requires the `model` scope; this catalog mapping does not enable the default-disabled Bridge command endpoint.\n\nBy default, `get_state` omits large static fields. Request `include: [\"tools\"]` to include `dumpTools`, `include: [\"systemPrompt\"]` to include `systemPrompt`, or both when a host needs a one-shot full session dump.\n### `get_state` payload\n\n```json\n{\n \"model\": { \"provider\": \"...\", \"id\": \"...\" },\n \"thinkingLevel\": \"off|minimal|low|medium|high|xhigh\",\n \"isStreaming\": false,\n \"isCompacting\": false,\n \"steeringMode\": \"all|one-at-a-time\",\n \"followUpMode\": \"all|one-at-a-time\",\n \"interruptMode\": \"immediate|wait\",\n \"sessionFile\": \"...\",\n \"sessionId\": \"...\",\n \"sessionName\": \"...\",\n \"autoCompactionEnabled\": true,\n \"messageCount\": 0,\n \"queuedMessageCount\": 0,\n \"todoPhases\": [\n {\n \"id\": \"phase-1\",\n \"name\": \"Todos\",\n \"tasks\": [\n {\n \"id\": \"task-1\",\n \"content\": \"Map the tool surface\",\n \"status\": \"in_progress\"\n }\n ]\n }\n ],\n \"contextUsage\": {\n \"tokens\": 0,\n \"contextWindow\": 200000,\n \"percent\": 0,\n \"source\": \"heuristic\"\n }\n // Optional with include: [\"systemPrompt\"]:\n // \"systemPrompt\": [\"...\"],\n // Optional with include: [\"tools\"] (or [\"dumpTools\"]):\n // \"dumpTools\": [\n // { \"name\": \"read\", \"description\": \"Read files and URLs\", \"parameters\": {} }\n // ]\n}\n```\n\n| Field | Contract |\n| --- | --- |\n| `tokens` | `number \\| null`. `null` when the count is unknown, such as immediately after compaction. |\n| `contextWindow` | `number`. |\n| `percent` | `number \\| null`. `null` when the count is unknown. |\n| `source` | `\"provider_anchor\" \\| \"heuristic\" \\| \"unknown\"`. `provider_anchor` uses a provider-reported total for already-sent context plus a heuristic estimate of trailing unsent messages; `heuristic` has no provider usage anchor and estimates the full context, including fixed system/tool context; `unknown` is post-compaction, when an exact count is unavailable until the next provider response. |\n\n`contextUsage` may be absent when no model or context window is configured. `source` is additive, so older clients can ignore it.\n\n### `set_todos` payload\n\nReplaces the in-memory todo state for the current session and returns the normalized phase list:\n\n```json\n{\n \"id\": \"req_2\",\n \"type\": \"set_todos\",\n \"phases\": [\n {\n \"id\": \"phase-1\",\n \"name\": \"Evaluation\",\n \"tasks\": [\n {\n \"id\": \"task-1\",\n \"content\": \"Map the read tool surface\",\n \"status\": \"in_progress\"\n },\n {\n \"id\": \"task-2\",\n \"content\": \"Exercise edit operations\",\n \"status\": \"pending\"\n }\n ]\n }\n ]\n}\n```\n\nThis is useful for hosts that want to pre-seed a plan before the first prompt.\n\n### `set_host_tools` payload\n\nReplaces the current set of host-owned tools that the RPC server may call back\ninto over stdio:\n\n```json\n{\n \"id\": \"req_3\",\n \"type\": \"set_host_tools\",\n \"tools\": [\n {\n \"name\": \"echo_host\",\n \"label\": \"Echo Host\",\n \"description\": \"Echo a value from the embedding host\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"message\": { \"type\": \"string\" }\n },\n \"required\": [\"message\"],\n \"additionalProperties\": false\n }\n }\n ]\n}\n```\n\nThe response payload is:\n\n```json\n{\n \"toolNames\": [\"echo_host\"]\n}\n```\n\nThese tools are added to the active session tool registry before the next model\ncall. Re-sending `set_host_tools` replaces the previous host-owned set.\n\n### `set_host_uri_schemes` payload\n\nReplaces the current set of host-owned URL schemes the RPC server should\ndispatch reads/writes through:\n\n```json\n{\n \"id\": \"req_4\",\n \"type\": \"set_host_uri_schemes\",\n \"schemes\": [\n {\n \"scheme\": \"db\",\n \"description\": \"Virtual db row files\",\n \"writable\": true,\n \"immutable\": false\n }\n ]\n}\n```\n\nThe response payload is:\n\n```json\n{\n \"schemes\": [\"db\"]\n}\n```\n\nSchemes are case-insensitive on the wire and normalized to lowercase before\nthe response is sent. Re-sending `set_host_uri_schemes` replaces the entire\nprevious set — schemes missing from the new list are unregistered.\n\n## Event Stream Schema\n\nRPC mode forwards `AgentSessionEvent` objects from `AgentSession.subscribe(...)` as canonical `event` frames:\n\n```json\n{\n \"type\": \"event\",\n \"protocol_version\": 2,\n \"session_id\": \"...\",\n \"seq\": 1,\n \"frame_id\": \"...\",\n \"payload\": {\n \"event_type\": \"agent_start\",\n \"event\": { \"type\": \"agent_start\" }\n }\n}\n```\n\n`seq` is monotonic per session starting at `1`. `payload.event_type` duplicates the inner event `type` for routing, and `payload.event` contains the original `AgentSessionEvent` fields.\n\nCommon inner event types:\n\n- `agent_start`, `agent_end`\n- `turn_start`, `turn_end`\n- `message_start`, `message_update`, `message_end`\n- `tool_execution_start`, `tool_execution_update`, `tool_execution_end`\n- `auto_compaction_start`, `auto_compaction_end`\n- `auto_retry_start`, `auto_retry_end`\n- `ttsr_triggered`\n- `todo_reminder`\n- `todo_auto_clear`\n\nNon-event stdout categories remain flat: `ready`, `response`, `workflow_gate`, `extension_ui_request`, `extension_error`, `host_tool_call`, `host_tool_cancel`, `host_uri_request`, and `host_uri_cancel`.\n\n`message_update` includes streaming deltas in the inner event's `assistantMessageEvent` (text/thinking/toolcall deltas).\n\nExtension runner errors are emitted separately as flat frames:\n\n```json\n{\n \"type\": \"extension_error\",\n \"extensionPath\": \"...\",\n \"event\": \"...\",\n \"error\": \"...\"\n}\n```\n\n## Prompt/Queue Concurrency and Ordering\n\nThis is the most important operational behavior.\n\n### Immediate ack vs completion\n\n`prompt` and `abort_and_prompt` are **acknowledged immediately**:\n\n```json\n{ \"id\": \"req_1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true }\n```\n\nThat means:\n\n- command acceptance != run completion\n- final completion is observed via `agent_end`\n\n### While streaming\n\n`AgentSession.prompt()` requires `streamingBehavior` during active streaming:\n\n- `\"steer\"` => queued steering message (interrupt path)\n- `\"followUp\"` => queued follow-up message (post-turn path)\n\nIf omitted during streaming, prompt fails.\n\n### Queue defaults\n\nFrom `packages/agent/src/agent.ts` defaults:\n\n- `steeringMode`: `\"one-at-a-time\"`\n- `followUpMode`: `\"one-at-a-time\"`\n- `interruptMode`: `\"immediate\"`\n\n### Mode semantics\n\n- `set_steering_mode` / `set_follow_up_mode`\n - `\"one-at-a-time\"`: dequeue one queued message per turn\n - `\"all\"`: dequeue entire queue at once\n- `set_interrupt_mode`\n - `\"immediate\"`: tool execution checks steering between tool calls; pending steering can abort remaining tool calls in the turn\n - `\"wait\"`: defer steering until turn completion\n\n## Workflow Gate Sub-Protocol\n\nInteractive workflow stages emit a machine-addressable gate frame before the legacy extension UI request:\n\n```json\n{\n \"type\": \"workflow_gate\",\n \"gate_id\": \"wg_4845_ralplan_000001\",\n \"stage\": \"ralplan\",\n \"kind\": \"approval\",\n \"schema\": { \"type\": \"string\", \"enum\": [\"approve\", \"request-changes\", \"reject\"] },\n \"schema_hash\": \"<sha256 of canonical schema>\",\n \"options\": [{ \"value\": \"approve\", \"label\": \"Approve execution\" }],\n \"context\": { \"title\": \"Approve plan?\", \"summary\": \"…\" },\n \"created_at\": \"2026-06-05T05:00:00.000Z\",\n \"required\": true\n}\n```\n\nFields:\n\n- `gate_id`: run-scoped, monotonic, stable id of the form `wg_<run>_<stage>_NNNNNN`.\n- `stage`: one of `\"deep-interview\"`, `\"ralplan\"`, or `\"ultragoal\"` (`team` is reserved and rejected for v1).\n- `kind`: one of `\"question\"`, `\"approval\"`, or `\"execution\"`.\n- `schema`: documented JSON Schema 2020-12 subset for the expected answer; `schema_hash` is the canonical hash of `schema` and equals the server-side validation hash.\n- `options`: optional `RpcWorkflowGateOption[]` (`{ value, label, description? }`), emitted for select-style gates.\n- `context`: `RpcWorkflowGateContext` (`title`, `prompt`, `summary`, `stage_state`, `artifact_refs`, `language`).\n- `created_at`: ISO timestamp the gate was opened; `required` is always `true`.\n\nHosts answer with:\n\n```json\n{ \"id\": \"resp_1\", \"type\": \"workflow_gate_response\", \"gate_id\": \"wg_4845_ralplan_000001\", \"answer\": \"approve\" }\n```\n\nA valid answer resolves the pending gate and returns:\n\n```json\n{ \"id\": \"resp_1\", \"type\": \"response\", \"command\": \"workflow_gate_response\", \"success\": true }\n```\n\nA schema mismatch is **not** a command failure: the response succeeds and the\nresolution data carries `status: \"rejected\"` plus a typed validation `error`\nwith code `invalid_workflow_gate_answer`:\n\n```json\n{\n \"id\": \"resp_1\",\n \"type\": \"response\",\n \"command\": \"workflow_gate_response\",\n \"success\": true,\n \"data\": {\n \"gate_id\": \"wg_1\",\n \"status\": \"rejected\",\n \"answer_hash\": \"…\",\n \"resolved_at\": \"…\",\n \"error\": {\n \"code\": \"invalid_workflow_gate_answer\",\n \"gate_id\": \"wg_1\",\n \"schema_hash\": \"…\",\n \"errors\": [{ \"path\": \"/answer\", \"keyword\": \"type\", \"message\": \"must be boolean\" }]\n }\n }\n}\n```\n\nAnswering a gate that does not exist is a recoverable command failure carrying\nthe broker error code `unknown_gate` (other broker codes are `already_resolved`,\n`idempotency_conflict`, and `invalid_workflow_stage`).\n## Extension UI Sub-Protocol\n\nExtensions in RPC mode use request/response UI frames.\n\n### Outbound request\n\n`RpcExtensionUIRequest` (`type: \"extension_ui_request\"`) methods:\n\n- `select`, `confirm`, `input`, `editor`, `cancel`\n- `notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`\n\nRuntime note:\n\n- Automatic session title generation is disabled in RPC mode, and `setTitle` UI\n requests are also suppressed by default because most hosts do not have a\n meaningful terminal-title surface. Set `SKC_RPC_EMIT_TITLE=1` to opt back in to\n the UI event only.\n\nExample:\n\n```json\n{\n \"type\": \"extension_ui_request\",\n \"id\": \"123\",\n \"method\": \"confirm\",\n \"title\": \"Confirm\",\n \"message\": \"Continue?\",\n \"timeout\": 30000\n}\n```\n\n### Inbound response\n\n`RpcExtensionUIResponse` (`type: \"extension_ui_response\"`):\n\n- `{ type: \"extension_ui_response\", id: string, value: string }`\n- `{ type: \"extension_ui_response\", id: string, confirmed: boolean }`\n- `{ type: \"extension_ui_response\", id: string, cancelled: true, timedOut?: boolean }`\n\nIf a dialog has a timeout, RPC mode resolves to a default value when timeout/abort fires.\n\n## Host Tool Sub-Protocol\n\nRPC hosts can expose custom tools to the agent by sending `set_host_tools`, then\nserving execution requests over the same transport.\n\n### Outbound request\n\nWhen the agent wants the host to execute one of those tools, RPC mode emits:\n\n```json\n{\n \"type\": \"host_tool_call\",\n \"id\": \"host_1\",\n \"toolCallId\": \"toolu_123\",\n \"toolName\": \"echo_host\",\n \"arguments\": { \"message\": \"hello\" }\n}\n```\n\nIf the tool execution is later aborted, RPC mode emits:\n\n```json\n{\n \"type\": \"host_tool_cancel\",\n \"id\": \"host_cancel_1\",\n \"targetId\": \"host_1\"\n}\n```\n\n### Inbound updates and completion\n\nHosts can optionally stream progress:\n\n```json\n{\n \"type\": \"host_tool_update\",\n \"id\": \"host_1\",\n \"partialResult\": {\n \"content\": [{ \"type\": \"text\", \"text\": \"working\" }]\n }\n}\n```\n\nCompletion uses:\n\n```json\n{\n \"type\": \"host_tool_result\",\n \"id\": \"host_1\",\n \"result\": {\n \"content\": [{ \"type\": \"text\", \"text\": \"done\" }]\n }\n}\n```\n\nSet top-level `isError: true` on `host_tool_result` to reject the pending host tool call and surface the returned text content as a tool error.\n\n## Host URI Sub-Protocol\n\nRPC hosts can also own custom URL schemes (virtual files). After\n`set_host_uri_schemes`, every read of `<scheme>://…` and write of\n`<scheme>://…` (when registered as `writable`) is bounced back to the host\nover the same transport.\n\n### Outbound request\n\nWhen a session tool resolves a host-owned URL, RPC mode emits:\n\n```json\n{\n \"type\": \"host_uri_request\",\n \"id\": \"uri_1\",\n \"operation\": \"read\",\n \"url\": \"db://users/42\"\n}\n```\n\nWrites look the same with `\"operation\": \"write\"` and an additional\n`\"content\": \"...\"` field carrying the full replacement bytes.\n\nIf the request is later aborted (caller cancels, session ends), RPC mode\nemits:\n\n```json\n{\n \"type\": \"host_uri_cancel\",\n \"id\": \"uri_cancel_1\",\n \"targetId\": \"uri_1\"\n}\n```\n\n### Inbound result\n\nFor successful reads:\n\n```json\n{\n \"type\": \"host_uri_result\",\n \"id\": \"uri_1\",\n \"content\": \"id=42\\nname=Alice\\n\",\n \"contentType\": \"text/plain\",\n \"notes\": [\"fresh from cache\"],\n \"immutable\": false\n}\n```\n\nFor successful writes, omit content:\n\n```json\n{ \"type\": \"host_uri_result\", \"id\": \"uri_1\" }\n```\n\nTo reject the request, set `isError: true` and either populate `error` with\na message or fall back to `content` for textual error surfacing:\n\n```json\n{\n \"type\": \"host_uri_result\",\n \"id\": \"uri_1\",\n \"isError\": true,\n \"error\": \"row 42 not found\"\n}\n```\n\n### Constraints\n\n- The agent's `edit` tool does not target host URIs. Hosts that want to\n mutate virtual files expose `write` and let the model use the `write` tool\n with replacement content.\n- Schemes are global to the process; `set_host_uri_schemes` replaces the\n previous set, unregistering anything not in the new list.\n- Schemes are normalized to lowercase before registration.\n\n## Error Model and Recoverability\n\n### Command-level failures\n\nFailures are `success: false` with string `error`.\n\n```json\n{\n \"id\": \"req_2\",\n \"type\": \"response\",\n \"command\": \"set_model\",\n \"success\": false,\n \"error\": \"Model not found: provider/model\"\n}\n```\n\n### Recoverability expectations\n\n- Most command failures are recoverable; process remains alive.\n- Malformed JSONL / parse-loop exceptions emit a `parse` error response and continue reading subsequent lines.\n- Empty `set_session_name` is rejected (`Session name cannot be empty`).\n- Extension UI responses with unknown `id` are ignored.\n- Process termination conditions are stdin close or explicit extension-triggered shutdown after the current command.\n\n## Compact Command Flows\n\n### 1) Prompt and stream\n\nstdin:\n\n```json\n{ \"id\": \"req_1\", \"type\": \"prompt\", \"message\": \"Summarize this repo\" }\n```\n\nstdout sequence (typical):\n\n```json\n{ \"id\": \"req_1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true }\n{ \"type\": \"agent_start\" }\n{ \"type\": \"message_update\", \"assistantMessageEvent\": { \"type\": \"text_delta\", \"delta\": \"...\" }, \"message\": { \"role\": \"assistant\", \"content\": [] } }\n{ \"type\": \"agent_end\", \"messages\": [] }\n```\n\n### 2) Prompt during streaming with explicit queue policy\n\nstdin:\n\n```json\n{\n \"id\": \"req_2\",\n \"type\": \"prompt\",\n \"message\": \"Also include risks\",\n \"streamingBehavior\": \"followUp\"\n}\n```\n\n### 3) Inspect and tune queue behavior\n\nstdin:\n\n```json\n{ \"id\": \"q1\", \"type\": \"get_state\" }\n{ \"id\": \"q2\", \"type\": \"set_steering_mode\", \"mode\": \"all\" }\n{ \"id\": \"q3\", \"type\": \"set_interrupt_mode\", \"mode\": \"wait\" }\n```\n\n### 4) Extension UI round trip\n\nstdout:\n\n```json\n{\n \"type\": \"extension_ui_request\",\n \"id\": \"ui_7\",\n \"method\": \"input\",\n \"title\": \"Branch name\",\n \"placeholder\": \"feature/...\"\n}\n```\n\nstdin:\n\n```json\n{ \"type\": \"extension_ui_response\", \"id\": \"ui_7\", \"value\": \"feature/rpc-host\" }\n```\n\n## OpenClaw / Hermes host integrations\n\nFor OpenClaw- or Hermes-style hosts, keep MCP servers and skills on the host side and expose the selected capabilities through RPC host tools. Do not import SKC runtime MCP internals directly; those package paths are intentionally quarantined. See [OpenClaw / Hermes RPC integration notes](./openclaw-hermes-rpc-integration.md).\n\n## Notes on `RpcClient` helper\n\n`src/modes/rpc/rpc-client.ts` is a convenience wrapper, not the protocol definition.\n\nCurrent helper characteristics:\n\n- Spawns `bun <cliPath> --mode rpc`\n- Correlates responses by generated `req_<n>` ids\n- Dispatches recognized `AgentEvent` types to event listeners\n- Dispatches top-level `workflow_gate` frames to `onWorkflowGate()` listeners\n- Supports host-owned custom tools via `setCustomTools()` and automatic handling of `host_tool_call` / `host_tool_cancel`\n- Exposes `respondGate()` for `workflow_gate_response` and waits for the accepted/rejected resolution envelope\n- Does **not** expose helper methods for every protocol command (for example, `set_interrupt_mode` and `set_session_name` are in protocol types but not wrapped as dedicated methods)\n\nUse raw protocol frames if you need complete surface coverage.\n\n## Workflow gates (agent-driven lifecycle)\n\nThe workflow-gate contract makes every human-gated lifecycle moment\n(deep-interview questions, ralplan approval, ultragoal execution sign-off)\nmachine-addressable so an external agent can answer it over RPC without\nscreen-scraping.\n\n### Outbound event: `workflow_gate`\n\n```json\n{\n \"type\": \"workflow_gate\",\n \"gate_id\": \"wg_4845_ralplan_000001\",\n \"stage\": \"ralplan\",\n \"kind\": \"approval\",\n \"schema\": { \"type\": \"string\", \"enum\": [\"approve\", \"request-changes\", \"reject\"] },\n \"schema_hash\": \"<sha256 of canonical schema>\",\n \"options\": [{ \"value\": \"approve\", \"label\": \"Approve execution\" }],\n \"context\": { \"title\": \"Approve plan?\", \"summary\": \"…\" },\n \"created_at\": \"2026-06-05T05:00:00.000Z\",\n \"required\": true\n}\n```\n\n- `gate_id` is **run-scoped and monotonic**: `wg_<run>_<stage>_<NNNNNN>`.\n- `stage` is one of `deep-interview` | `ralplan` | `ultragoal`. `team` is\n reserved and rejected for v1 (single-agent only).\n- `kind` is `question` | `approval` | `execution`.\n- `schema` is a **documented subset of JSON Schema 2020-12**. Supported keywords:\n `type`, `enum`, `const`, `properties`, `required`, `additionalProperties`,\n `items`, `minLength`, `maxLength`, `minimum`, `maximum`, `title`,\n `description`, `oneOf`, `anyOf`. Any other keyword is rejected at gate\n construction (`invalid_workflow_gate_schema`) so the server never advertises a\n schema it will not validate. `schema_hash` equals the server-side validation\n hash for that gate.\n\n### Inbound command: `workflow_gate_response`\n\n```json\n{ \"type\": \"workflow_gate_response\", \"gate_id\": \"wg_4845_ralplan_000001\", \"answer\": \"approve\", \"idempotency_key\": \"k1\" }\n```\n\nThe answer is validated against the advertised schema **before acceptance**:\n\n- Valid → resolution persisted before the workflow advances; response:\n `{ \"type\": \"response\", \"command\": \"workflow_gate_response\", \"success\": true, \"data\": { \"gate_id\": \"…\", \"status\": \"accepted\", \"answer_hash\": \"…\", \"resolved_at\": \"…\" } }`.\n- Invalid → the gate stays **pending** and the resolution carries a typed\n `invalid_workflow_gate_answer` error listing each `{ path, keyword, message, expected? }`.\n- Idempotency: replaying the same `idempotency_key` + identical body returns the\n cached resolution; the same key with a different body is an\n `idempotency_conflict`; answering an already-accepted gate is `already_resolved`.\n- Client helpers wait for this accepted/rejected resolution envelope; they must not treat the write of `workflow_gate_response` itself as completion.\n\n### Entering unattended mode: `negotiate_unattended`\n\nUnattended (zero-human) operation is **fail-closed**. The external agent must\ndeclare its budget, scopes, and action allowlist up front:\n\n```json\n{\n \"type\": \"negotiate_unattended\",\n \"declaration\": {\n \"actor\": \"openclaw/hermes\",\n \"budget\": { \"max_tokens\": 2000000, \"max_tool_calls\": 5000, \"max_wall_time_ms\": 3600000, \"max_cost_usd\": 20 },\n \"scopes\": [\"prompt\", \"control\", \"bash\"],\n \"action_allowlist\": [\"bash.readonly\", \"file.write\"]\n }\n}\n```\n\nA missing or partial declaration refuses unattended mode. Budget, scope, and\naudit enforcement are layered on this contract by the unattended control plane\n(see issues #318/#319/#320). Attended mode is unaffected: clients that never send\n`negotiate_unattended` keep the existing extension-UI / permission behavior.\n\n\n> **Status (live, #315/#318/#321):** the `workflow_gate` /\n> `workflow_gate_response` / `negotiate_unattended` frames, the answer-schema\n> validator, and the durable gate broker are defined, tested, and wired into\n> live session dispatch. When an unattended control plane is attached to the\n> session, `dispatchRpcCommand` routes `negotiate_unattended` and\n> `workflow_gate_response` through it (see\n> `packages/coding-agent/src/modes/shared/agent-wire/command-dispatch.ts`); a\n> session without that control plane returns a typed \"not available\" error for\n> these frames rather than silently dropping them.\n\n\n### Answering gates from a client (#322)\n\nBoth clients expose typed `workflow_gate` receive + respond helpers so an agent\ncan answer a gate from its own memory via a callback.\n\nFor bridge sessions, gate responses are **not** posted through `/commands`. The\nclient must first own the UI/control plane, then post the answer body to\n`POST /v1/sessions/{session_id}/ui-responses/{gate_id}` with\n`X-SKC-Bridge-Owner-Token: <ownerToken>`. `Idempotency-Key` may be supplied as a\nheader and the same value is also accepted in the JSON body as `idempotency_key`.\n\n`@sayknow-cli/bridge-client` (TypeScript):\n\n```ts\nimport { BridgeClient } from \"@sayknow-cli/bridge-client\";\n\nconst client = new BridgeClient({ baseUrl, token });\n// Headless policy: every received gate is routed to the resolver and answered.\nfor await (const { gate, answer } of client.consumeWorkflowGates(sessionId, ownerToken, gate => {\n if (gate.kind === \"approval\") return { decision: \"approve\" };\n if (gate.kind === \"question\") return { selected: [gate.options?.[0]?.value], other: false };\n return { decision: \"approve\" };\n})) {\n console.log(`answered ${gate.gate_id} (${gate.kind}) with`, answer);\n}\n// Or answer a single gate directly:\nawait client.respondGate(sessionId, gateId, ownerToken, { decision: \"approve\" });\n```\n\n`python/skc-rpc` (Python):\n\n```python\nfrom skc_rpc import RpcClient, WorkflowGate\n\nclient = RpcClient(executable=\"skc\")\n\ndef resolver(gate: WorkflowGate) -> object:\n if gate.kind == \"approval\":\n return {\"decision\": \"approve\"}\n if gate.kind == \"question\":\n return {\"selected\": [gate.options[0].value] if gate.options else [], \"other\": False}\n return {\"decision\": \"approve\"}\n\n# Headless policy: route every received gate to the resolver and respond.\nclient.run_workflow_gate_policy(resolver)\nclient.start()\n# Or answer a single gate directly: client.respond_gate(gate_id, {\"decision\": \"approve\"})\n```\n",
|
|
78
79
|
"rulebook-matching-pipeline.md": "# Rulebook Matching Pipeline\n\nThis document describes how coding-agent discovers rules from supported config formats, normalizes them into a single `Rule` shape, resolves precedence conflicts, and splits the result into:\n\n- **Rulebook rules** (available to the model via system prompt + `rule://` URLs)\n- **TTSR rules** (time-travel stream interruption rules)\n\nIt reflects the current implementation, including partial semantics and metadata that is parsed but not enforced.\n\n## Implementation files\n\n- [`../src/capability/rule.ts`](../packages/coding-agent/src/capability/rule.ts)\n- [`../src/capability/index.ts`](../packages/coding-agent/src/capability/index.ts)\n- [`../src/discovery/index.ts`](../packages/coding-agent/src/discovery/index.ts)\n- [`../src/discovery/helpers.ts`](../packages/coding-agent/src/discovery/helpers.ts)\n- [`../src/discovery/builtin.ts`](../packages/coding-agent/src/discovery/builtin.ts)\n- [`../src/discovery/cursor.ts`](../packages/coding-agent/src/discovery/cursor.ts)\n- [`../src/discovery/windsurf.ts`](../packages/coding-agent/src/discovery/windsurf.ts)\n- [`../src/discovery/cline.ts`](../packages/coding-agent/src/discovery/cline.ts)\n- [`../src/sdk.ts`](../packages/coding-agent/src/sdk.ts)\n- [`../src/system-prompt.ts`](../packages/coding-agent/src/system-prompt.ts)\n- [`../src/internal-urls/rule-protocol.ts`](../packages/coding-agent/src/internal-urls/rule-protocol.ts)\n- [`../src/utils/frontmatter.ts`](../packages/coding-agent/src/utils/frontmatter.ts)\n\n## 1. Canonical rule shape\n\nAll providers normalize source files into `Rule`:\n\n```ts\ninterface Rule {\n name: string;\n path: string;\n content: string;\n globs?: string[];\n alwaysApply?: boolean;\n description?: string;\n condition?: string[];\n scope?: string[];\n interruptMode?: \"never\" | \"prose-only\" | \"tool-only\" | \"always\";\n _source: SourceMeta;\n}\n```\n\nCapability identity is `rule.name` (`ruleCapability.key = rule => rule.name`).\n\nConsequence: precedence and deduplication are **name-based only**. Two different files with the same `name` are considered the same logical rule.\n\n## 2. Discovery sources and normalization\n\n`src/discovery/index.ts` auto-registers providers. For `rules`, current providers are:\n\n- `native` (priority `100`)\n- `cursor` (priority `50`)\n- `windsurf` (priority `50`)\n- `cline` (priority `40`)\n\n### Native provider (`builtin.ts`)\n\nLoads `.skc` rules from:\n\n- project: `<cwd>/.skc/rules/*.{md,mdc}`\n- user: `~/.skc/agent/rules/*.{md,mdc}`\n\nNormalization:\n\n- `name` = filename without `.md`/`.mdc`\n- frontmatter parsed via `parseFrontmatter`\n- `content` = body (frontmatter stripped)\n- `globs`, `alwaysApply`, `description`, `condition`/legacy `ttsr_trigger`, `scope`, and `interruptMode` are parsed by `buildRuleFromMarkdown`\n\nImportant caveat: `condition` values that look like file globs are converted into `tool:edit(...)` / `tool:write(...)` scope shorthands with catch-all condition `.*`.\n\n### Cursor provider (`cursor.ts`)\n\nLoads from:\n\n- user: `~/.cursor/rules/*.{mdc,md}`\n- project: `<cwd>/.cursor/rules/*.{mdc,md}`\n\nNormalization (`transformMDCRule`):\n\n- `description`: kept only if string\n- `alwaysApply`: only `true` is preserved (`false` becomes `undefined`)\n- `globs`: accepts array (string elements only) or single string\n- `condition`/legacy `ttsr_trigger`, `scope`, and `interruptMode` are parsed by shared rule helpers\n- `name` from filename without extension\n\n### Windsurf provider (`windsurf.ts`)\n\nLoads from:\n\n- user: `~/.codeium/windsurf/memories/global_rules.md` (fixed rule name `global_rules`)\n- project: `<cwd>/.windsurf/rules/*.md`\n\nNormalization:\n\n- `globs`: array-of-string or single string\n- `alwaysApply`, `description`, `condition`/legacy `ttsr_trigger`, `scope`, and `interruptMode` parsed by shared rule helpers\n- `name` is fixed to `global_rules` for the user global file and derived from filename for project rules\n\n### Cline provider (`cline.ts`)\n\nSearches upward from `cwd` for nearest `.clinerules`:\n\n- if directory: loads `*.md` inside it\n- if file: loads single file as rule named `clinerules`\n\nNormalization:\n\n- `globs`: array-of-string or single string\n- `alwaysApply`, `description`, `condition`/legacy `ttsr_trigger`, `scope`, and `interruptMode` parsed by shared rule helpers\n- `name` is fixed to `clinerules` for a `.clinerules` file and derived from filename for `.clinerules/*.md`\n\n## 3. Frontmatter parsing behavior and ambiguity\n\nAll providers use `parseFrontmatter` (`utils/frontmatter.ts`) with these semantics:\n\n1. Frontmatter is parsed only when content starts with `---` and has a closing `\\n---`.\n2. Body is trimmed after frontmatter extraction.\n3. If YAML parse fails:\n - warning is logged,\n - parser falls back to simple `key: value` line parsing (`^(\\w+):\\s*(.*)$`).\n\nAmbiguity consequences:\n\n- Fallback parser does not support arrays, nested objects, quoting rules, or hyphenated keys.\n- Fallback values become strings (for example `alwaysApply: true` becomes string `\"true\"`), so providers requiring boolean/string types may drop metadata.\n- `ttsr_trigger` works in fallback (underscore key); keys like `thinking-level` would not.\n- Files without valid frontmatter still load as rules with empty metadata and full content body.\n\n## 4. Provider precedence and deduplication\n\n`loadCapability(\"rules\")` (`capability/index.ts`) merges provider outputs and then deduplicates by `rule.name`.\n\n### Precedence model\n\n- Providers are ordered by priority descending.\n- Equal priority keeps registration order (`cursor` before `windsurf` from `discovery/index.ts`).\n- Dedup is first-wins: first encountered rule name is kept; later same-name items are marked `_shadowed` in `all` and excluded from `items`.\n\nEffective rule provider order is currently:\n\n1. `native` (100)\n2. `cursor` (50)\n3. `windsurf` (50)\n4. `cline` (40)\n\n### Intra-provider ordering caveat\n\nWithin a provider, item order comes from `loadFilesFromDir` glob result ordering plus explicit push order. This is deterministic enough for normal use but not explicitly sorted in code.\n\nNotable source-order differences:\n\n- `native` appends project then user config dirs.\n- `cursor` appends user then project results.\n- `windsurf` appends user `global_rules` first, then project rules.\n- `cline` loads only nearest `.clinerules` source.\n\n## 5. Split into Rulebook, Always-Apply, and TTSR buckets\n\nAfter rule discovery in `createAgentSession` (`sdk.ts`):\n\n1. All discovered rules are scanned.\n2. Rules with `condition` entries are registered into `TtsrManager`; legacy `ttsr_trigger` / `ttsrTrigger` are accepted during rule parsing as condition fallbacks.\n3. A separate `rulebookRules` list is built with this predicate:\n\n```ts\n!isTtsrRule && rule.alwaysApply !== true && !!rule.description;\n```\n\n4. An `alwaysApplyRules` list is built:\n\n```ts\n!isTtsrRule && rule.alwaysApply === true;\n```\n\n### Bucket behavior\n\n- **TTSR bucket**: any rule with a non-empty parsed `condition` that `TtsrManager.addRule(...)` accepts. Takes priority over other buckets.\n- **Always-apply bucket**: `alwaysApply === true`, not TTSR. Full content injected into system prompt. Resolvable via `rule://`.\n- **Rulebook bucket**: must have description, must not be TTSR, must not be `alwaysApply`. Listed in system prompt by name+description; content read on demand via `rule://`.\n- A rule with both `condition` and `alwaysApply` goes to TTSR only (TTSR takes priority).\n- A rule with both `alwaysApply` and `description` goes to always-apply only (not rulebook).\n\n## 6. How metadata affects runtime surfaces\n\n### `description`\n\n- Required for inclusion in rulebook.\n- Rendered in system prompt `<rules>` block.\n- Missing description means rule is not available via `rule://` and not listed in system prompt rules.\n\n### `globs`\n\n- Carried through on `Rule`.\n- Rendered as `<glob>...</glob>` entries in the system prompt rules block.\n- Exposed in rules UI state (`extensions` mode list).\n- **Not enforced for automatic matching in this pipeline.** There is no runtime glob matcher selecting rules by current file/tool target.\n\n### `alwaysApply`\n\n- Parsed and preserved by providers.\n- Used in UI display (`\"always\"` trigger label in extensions state manager).\n- Used as an exclusion condition from `rulebookRules`.\n- **Full rule content is auto-injected into the system prompt** (before the rulebook rules section).\n- Rule is also addressable via `rule://<name>` for re-reading.\n\n### `condition`, `scope`, and `interruptMode`\n\n- `condition` is the current TTSR trigger field; legacy `ttsr_trigger` / `ttsrTrigger` are accepted as fallback inputs during parsing.\n- `scope` narrows TTSR matching scope. A condition token that looks like a file glob becomes `tool:edit(<glob>)` and `tool:write(<glob>)` scope entries plus catch-all condition `.*`.\n- `interruptMode` can override the global TTSR interrupt mode for the rule.\n\n## 7. System prompt inclusion path\n\n`buildSystemPromptInternal` receives both `rules` (rulebook) and `alwaysApplyRules`.\n\nAlways-apply rules are rendered first, injecting their raw content directly into the prompt.\n\nRulebook rules are rendered in a `# Rules` section with:\n\n- `Read rule://<name> when working in matching domain`\n- Each rule's `name`, `description`, and optional `<glob>` list\n\nThis is advisory/contextual: prompt text asks the model to read applicable rules, but code does not enforce glob applicability.\n\n## 8. `rule://` internal URL behavior\n\n`RuleProtocolHandler` is registered with:\n\n```ts\nnew RuleProtocolHandler({\n getRules: () => [...rulebookRules, ...alwaysApplyRules],\n});\n```\n\nImplications:\n\n- `rule://<name>` resolves against both **rulebookRules** and **alwaysApplyRules**.\n- TTSR-only rules and rules with no description and no `alwaysApply` are not addressable via `rule://`.\n- Resolution is exact name match.\n- Unknown names return error listing available rule names.\n- Returned content is raw `rule.content` (frontmatter stripped), content type `text/markdown`.\n\n## 9. Known partial / non-enforced semantics\n\n1. Provider descriptions mention legacy files (`.cursorrules`, `.windsurfrules`), but current loader code paths do not actually read those files.\n2. `globs` metadata is surfaced to prompt/UI but not enforced by rule selection logic.\n3. Rule selection for `rule://` includes rulebook and always-apply rules, but not TTSR-only rules.\n4. Discovery warnings (`loadCapability(\"rules\").warnings`) are produced but `createAgentSession` does not currently surface/log them in this path.\n",
|
|
79
80
|
"sayknow-remote.md": "# Sayknow Remote — v0 Design (thin phone steering wheel)\n\nStatus: **design / pending approval** · Tracks issue #565 · Scope: **v0 only**\n\nSayknow Remote is a thin remote *steering wheel* for an already-running PC-side\n`skc` session. It is intentionally **not** a universal phone shell, a remote\nfilesystem editor, or a remote-desktop replacement. The PC stays the authority\nboundary for file edits, shell execution, approval gates, and sensitive output.\n\nThis document fixes the two things the issue calls out as blockers/gates before\nany code lands: the **authority boundary contract** and the **transmitted-data\ncontract**. It then maps v0 onto existing surfaces and splits the work into\nPR-sized steps.\n\n## TL;DR architecture decision\n\nv0 is a thin **read + one-line-submit** surface layered on subsystems that\nalready exist. It introduces **no new remote-control protocol** — that would\nrequire ADR-level rationale per [`docs/bridge.md`](bridge.md).\n\n| Concern | Reused existing surface |\n| --- | --- |\n| Network transport, TLS, bearer auth, fail-closed posture | Bridge mode (`skc --mode bridge`), [`docs/bridge.md`](bridge.md) |\n| Client SDK / framing | `@sayknow-cli/bridge-client` (`BridgeClient`, `events()`) |\n| Session state storage, liveness, bounded status, submit gating | Harness control plane (`skc harness`), `packages/coding-agent/src/harness-control-plane/` |\n| Bounded observation (never a raw transcript dump) | `Observation` / `SessionStateView` in `harness-control-plane/types.ts` |\n| Web client precedent (local server + SPA) | `packages/stats` (`server.ts` + `src/client/`) |\n\nThe only genuinely new piece is a thin **Sayknow Remote gateway**: one PC-side\nprocess that enumerates harness control-plane sessions and proxies a strict,\nbounded subset of read/submit operations to each session's owner. The phone\ntalks to exactly one gateway endpoint.\n\n## Why a gateway (and not one bridge per session)\n\nBridge mode serves **exactly one live `AgentSession` per process** and is\nfail-closed by default (see the endpoint matrix in\n`packages/coding-agent/src/modes/bridge/bridge-mode.ts`). The v0 requirement\n\"list active PC-side sessions\" needs cross-session enumeration, which a single\nbridge process does not provide.\n\nThe harness control plane already centralizes exactly this: per-session\n`state.json`, lease + heartbeat liveness, a single-writer severity event log,\nand a **bounded** observation vocabulary. So the gateway is a thin read/submit\nproxy over the control plane, wearing the bridge security model (TLS + bearer +\nfail-closed). This keeps bridge single-session semantics untouched and avoids a\nproliferation of per-session ports and pairing surfaces.\n\n```\n phone (mobile web)\n │ HTTPS + bearer (scoped: remote:view + remote:submit)\n ▼\n Sayknow Remote gateway ── enumerates ─▶ harness control-plane session-state dir\n (one PC process) ── observe ───▶ owner process (RuntimeOwner, lease holder)\n ── submit ────▶ owner submit path (readyForSubmit gating)\n │\n └─ never: file edits · shell · gate answers · raw transcript · secrets\n```\n\n## Authority boundary contract\n\nThe PC-side runtime is the sole authority. The gateway and the phone are\n**observers + one-line submitters**, nothing more.\n\nThe gateway MUST NOT, in v0:\n\n- edit files, run shell, or invoke any mutating tool;\n- answer workflow-gate / permission / approval prompts (those stay on the PC);\n- expose bridge/RPC command scopes (`message:read`, `session`, `model`, `bash`,\n `host_tools`, `host_uri`, `export`, `admin`, or `control`) to the phone;\n- stream raw pane output, transcripts, tool arguments/results, diffs, file\n contents, environment, or secrets;\n- bypass the owner's `readyForSubmit` gating or submit while a session is busy.\n\nThe phone MAY, in v0:\n\n- list active sessions (bounded metadata);\n- open one session and read its **bounded** status/observation;\n- submit a single one-line instruction through the owner's normal submit path;\n- see idle / working / blocked status and a human-readable reason when blocked.\n\nA one-line submit travels the same path as a local `skc harness submit`, so it\ninherits the owner's submission gating (`Observation.readyForSubmit` /\n`submitUnavailableReason`). The phone cannot force a submit the local runtime\nwould itself refuse.\n\n## Transmitted-data contract (allowlist)\n\nThe contract is an **allowlist**: only the fields below leave the PC. Anything\nnot listed is withheld by default. This is enforced in code as a typed\nprojection from the control plane's already-bounded `Observation` /\n`SessionStateView` — never a passthrough of internal state.\n\n### Session list entry → phone (`RemoteSessionSummary`)\n\n| Field | Source | Notes |\n| --- | --- | --- |\n| `sessionId` | `SessionState.sessionId` | opaque id |\n| `name` | derived from handle metadata (`issueOrPr`, repo, branch, or session id fallback) | sanitized, length-capped |\n| `harness` | `SessionState.harness` | `sayknow-cli` in v1 |\n| `status` | derived (see state mapping) | `idle` \\| `working` \\| `blocked` \\| `offline` |\n| `lastActivityAt` | `Observation.lastActivityAt` | ISO timestamp |\n| `branch` | `Observation.branch` | branch name only |\n\n### Open-session view → phone (`RemoteSessionView`)\n\n| Field | Source | Notes |\n| --- | --- | --- |\n| `sessionId`, `name`, `harness`, `status` | as above | |\n| `lifecycle` | `SessionStateView.lifecycle` | bounded enum |\n| `ownerLive` | `SessionStateView.ownerLive` | liveness |\n| `blockers` | `SessionStateView.blockers` | reason strings, sanitized |\n| `observedSignals` | `Observation.observedSignals` | bounded vocab only (`tool-call`, `test-running`, `streaming`, `idle`, …) |\n| `gitDelta` | `Observation.gitDelta` | enum: `clean`/`dirty`/`zero-delta`/`unknown` |\n| `risk` | `Observation.risk` | enum |\n| `readyForSubmit` | `Observation.readyForSubmit` | submit affordance |\n| `submitUnavailableReason` | `Observation.submitUnavailableReason` | when not ready |\n| `lastActivityAt`, `branch` | as above | |\n\n### Phone → PC (`RemoteSubmitRequest`)\n\n| Field | Notes |\n| --- | --- |\n| `sessionId` | target session |\n| `text` | single one-line instruction; length-capped, control-chars stripped |\n| `idempotencyKey` | optional; dedupes retries (mirrors bridge idempotency) |\n\n### Never transmitted by default\n\nRaw pane/terminal output, full transcript / message bodies, tool call arguments\nor results, file contents, diffs, system prompt, environment variables, tokens\nor secrets, and absolute paths beyond the session `cwd`/`branch` metadata. When\ncontent is intentionally held back, the phone shows a neutral *\"withheld on PC\"*\nmarker rather than a redacted blob.\n\n## Session-state model (idle / working / blocked)\n\n`status` is derived from harness lifecycle + liveness + bounded signals:\n\n- **offline** — `ownerLive == false`, lease dead, or gateway cannot reach the\n owner. (Distinct from blocked; the PC is gone, not waiting.)\n- **blocked** — `lifecycle == \"blocked\"`, OR a workflow-gate / permission prompt\n is pending on the PC, OR `readyForSubmit == false` with a\n `submitUnavailableReason`. Phone shows the reason; it does **not** resolve it.\n- **working** — owner live and `lifecycle` in\n `{started, submitted, observing, recovering, validating, finalizing}` with\n recent activity signals (`streaming` / `tool-call` / `test-running`).\n- **idle** — owner live, stable lifecycle, last signal `idle`/`completed`, and\n `readyForSubmit == true`.\n\n## Failure states (must be understandable)\n\n| Condition | Detection | Phone UX |\n| --- | --- | --- |\n| Disconnected PC | `ownerLive`/lease dead or gateway unreachable | `offline`; submit disabled; \"PC is offline\" |\n| Expired pairing | bearer/pairing token expired or revoked | \"Pairing expired — re-pair on PC\" |\n| Session busy | `readyForSubmit == false` (+ reason) | submit disabled with reason; optionally queue |\n| Submit rejected | typed object error (e.g. `{ code: \"scope_denied\" }`, see [`docs/rpc.md`](rpc.md)) | inline rejection reason |\n| Sensitive output withheld | bounded observation by design | neutral \"withheld on PC\" marker |\n\n## Pairing and auth (minimum that is not security soup)\n\nv0 = **local pairing only**. Reuse the bridge security model for transport and authentication posture, but expose a gateway-specific authorization surface:\n\n- **TLS mandatory for every bind, including loopback** (no plaintext fallback;\n matches `docs/bridge.md`).\n- **Bearer token mandatory** for every endpoint except health/help.\n- Pairing flow: the PC prints/serves a short-lived **pairing code**; the phone\n submits host + code and receives a **scoped bearer** capped to\n gateway-only scopes: `remote:view` + `remote:submit` only. These scopes are\n not aliases for bridge/RPC `message:read` or `prompt`. Phone bearers MUST NOT\n authorize bridge command-catalog calls such as `get_messages`,\n `get_last_assistant_text`, `get_state` with `include: [\"systemPrompt\",\n \"tools\"]`, `new_session`, `switch_session`, `branch`, `set_model`,\n `bash`, `host_*`, `control`, or `admin`.\n- Tokens expire; re-pairing is the recovery path. The gateway is fail-closed:\n unknown/expired tokens, non-gateway scopes, bridge command-catalog methods,\n and out-of-scope commands are rejected before dispatch.\n\nHosted relay is **deferred to v1** and gated behind a separate ADR (it changes\nthe trust model and is where \"security soup\" risk concentrates).\n\n## Open questions from the issue — v0 decisions\n\n| Question | v0 decision | Deferred |\n| --- | --- | --- |\n| Hosted relay vs local pairing vs both | Local pairing only | Hosted relay → v1 (ADR) |\n| Minimum pairing/auth | Pairing code → gateway-scoped bearer, TLS mandatory | Identity/relay accounts → v1 |\n| Which session states are public | Bounded: `idle`/`working`/`blocked`/`offline` + bounded observation vocab | Richer telemetry → v1 |\n| Web vs native first | Mobile web first (reuse stats SPA build pattern) | Native app / PWA polish → v1 |\n| Notifications / pause / resume | Out of scope for v0 | Staged in v1 |\n\n## Implementation plan (PR-sized steps)\n\nEach step is independently shippable; later steps stay fail-closed until wired.\n\n1. **PR 1 — this doc.** `docs/sayknow-remote.md` + README cross-link. Resolves the\n authority + transmitted-data gates. No code.\n2. **PR 2 — typed contract + schema.** `RemoteSessionSummary`,\n `RemoteSessionView`, `RemoteSubmitRequest`, `RemoteSubmitResult`,\n `RemoteErrorCode`, plus a projection `Observation`/`SessionStateView →\n RemoteSessionView` and a JSON schema. Tests assert the allowlist (no\n forbidden field can leak). Types only; no runtime wiring.\n3. **PR 3 — gateway read path.** Enumerate harness sessions (list) and serve the\n per-session bounded view, behind an explicit opt-in flag, fail-closed\n otherwise, reusing bridge TLS + bearer. Tests for liveness derivation and the\n redaction projection.\n4. **PR 4 — gateway submit path.** One-line submit through the owner's submit\n gating; typed rejections for busy/denied; idempotency. Tests for\n busy/rejected paths.\n5. **PR 5 — pairing/auth.** Pairing code → gateway-scoped bearer\n (`remote:view` + `remote:submit`) with expiry. Tests prove phone bearers\n cannot call bridge/RPC `message:read`, `prompt`, session/model, shell, host,\n control, or admin surfaces.\n6. **PR 6 — mobile web client.** Minimal SPA (list → open → status → submit)\n using `@sayknow-cli/bridge-client` and the `packages/stats` build pattern.\n7. **PR 7 — failure-state UX + hardening.** Failure-state surfaces, redaction\n hardening, CHANGELOG, docs finalize.\n\n## Non-goals (v0)\n\n- No arbitrary phone-side shell.\n- No raw secret/log dumping to mobile by default.\n- No direct filesystem editor from the phone.\n- No bypass around PC-side approval/confirmation gates.\n- No remote-desktop replacement.\n- No second authenticated remote-control protocol (reuse bridge; relay needs an\n ADR).\n\n## Key source references\n\n- Bridge transport / security: `packages/coding-agent/src/modes/bridge/`, [`docs/bridge.md`](bridge.md)\n- RPC command/response contract: `packages/coding-agent/src/modes/rpc/`, [`docs/rpc.md`](rpc.md)\n- Client SDK: `packages/bridge-client/src/`\n- Control plane (sessions, leases, bounded observation, submit): `packages/coding-agent/src/harness-control-plane/`, `packages/coding-agent/src/commands/harness.ts`\n- Web client precedent: `packages/stats/src/server.ts`, `packages/stats/src/client/`\n\n—\n*[repo owner's gaebal-sayknow (clawdbot) 🦞]*\n",
|
|
80
81
|
"sdk.md": "# SDK\n\nThe SDK is the in-process integration surface for `@sayknow-cli/coding-agent`.\nUse it when you want direct access to agent state, event streaming, tool wiring, and session control from your own Bun/Node process.\n\nIf you need cross-language/process isolation, use RPC mode instead.\n\n## Installation\n\n```bash\nbun add @sayknow-cli/coding-agent\n```\n\n## Entry points\n\n`@sayknow-cli/coding-agent` exports the SDK APIs from the package root (and also via `@sayknow-cli/coding-agent/sdk`).\n\nCore exports for embedders:\n\n- `createAgentSession`\n- `SessionManager`\n- `Settings`\n- `AuthStorage`\n- `ModelRegistry`\n- `discoverAuthStorage`\n- Discovery helpers for retained context/prompt surfaces (`discoverContextFiles`, `discoverPromptTemplates`)\n- Tool factory surface (`createTools`, `BUILTIN_TOOLS`, tool classes)\n\n## Quick start (auto-discovery defaults)\n\n```ts\nimport { createAgentSession } from \"@sayknow-cli/coding-agent\";\n\nconst { session, modelFallbackMessage } = await createAgentSession();\n\nif (modelFallbackMessage) {\n process.stderr.write(`${modelFallbackMessage}\\n`);\n}\n\nconst unsubscribe = session.subscribe((event) => {\n if (\n event.type === \"message_update\" &&\n event.assistantMessageEvent.type === \"text_delta\"\n ) {\n process.stdout.write(event.assistantMessageEvent.delta);\n }\n});\n\nawait session.prompt(\"Summarize this repository in 3 bullets.\");\nunsubscribe();\nawait session.dispose();\n```\n\n## What `createAgentSession()` discovers by default\n\n`createAgentSession()` follows “provide to override, omit to discover”.\n\nIf omitted, it resolves:\n\n- `cwd`: `getProjectDir()`\n- `agentDir`: `~/.skc/agent` (via `getAgentDir()`)\n- `authStorage`: `discoverAuthStorage(agentDir)`\n- `modelRegistry`: `new ModelRegistry(authStorage)` + background `refreshInBackground()` when the registry is not provided\n- `settings`: `await Settings.init({ cwd, agentDir })`\n- `sessionManager`: `SessionManager.create(cwd)` (file-backed)\n- context files and prompt templates\n- built-in tools via `createTools(...)`\n- LSP integration (enabled by default)\n- `eventBus`: new `EventBus()` unless supplied\n\n### Required vs optional inputs\n\nTypically you must provide only what you want to control:\n\n- **Must provide**: nothing for a minimal session\n- **Usually provide explicitly** in embedders:\n - `sessionManager` (if you need in-memory or custom location)\n - `authStorage` + `modelRegistry` (if you own credential/model lifecycle)\n - `model` or `modelPattern` (if deterministic model selection matters)\n - `settings` (if you need isolated/test config)\n\n## Session manager behavior (persistent vs in-memory)\n\n`AgentSession` always uses a `SessionManager`; behavior depends on which factory you use.\n\n### File-backed (default)\n\n```ts\nimport { createAgentSession, SessionManager } from \"@sayknow-cli/coding-agent\";\n\nconst { session } = await createAgentSession({\n sessionManager: SessionManager.create(process.cwd()),\n});\n\nconsole.log(session.sessionFile); // absolute .jsonl path\n```\n\n- Persists conversation/messages/state deltas to session files.\n- Supports resume/open/list/fork workflows.\n- `session.sessionFile` is defined.\n\n### In-memory\n\n```ts\nimport { createAgentSession, SessionManager } from \"@sayknow-cli/coding-agent\";\n\nconst { session } = await createAgentSession({\n sessionManager: SessionManager.inMemory(),\n});\n\nconsole.log(session.sessionFile); // undefined\n```\n\n- No filesystem persistence.\n- Useful for tests, ephemeral workers, request-scoped agents.\n- Session methods still work, but persistence-specific behaviors (file resume/fork paths) are naturally limited.\n\n### Resume/open/list helpers\n\n```ts\nimport { SessionManager } from \"@sayknow-cli/coding-agent\";\n\nconst recent = await SessionManager.continueRecent(process.cwd());\nconst listed = await SessionManager.list(process.cwd());\nconst opened = listed[0] ? await SessionManager.open(listed[0].path) : null;\n```\n\n## Model and auth wiring\n\n`createAgentSession()` uses `ModelRegistry` + `AuthStorage` for model selection and API key resolution.\n\n### Explicit wiring\n\n```ts\nimport {\n createAgentSession,\n discoverAuthStorage,\n ModelRegistry,\n SessionManager,\n} from \"@sayknow-cli/coding-agent\";\n\nconst authStorage = await discoverAuthStorage();\nconst modelRegistry = new ModelRegistry(authStorage);\nawait modelRegistry.refresh();\n\nconst available = modelRegistry.getAvailable();\nif (available.length === 0)\n throw new Error(\"No authenticated models available\");\n\nconst { session } = await createAgentSession({\n authStorage,\n modelRegistry,\n model: available[0],\n thinkingLevel: \"medium\",\n sessionManager: SessionManager.inMemory(),\n});\n```\n\n### Selection order when `model` is omitted\n\nWhen no explicit `model`/`modelPattern` is provided:\n\n1. restore model from existing session (if restorable + key available)\n2. settings default model role (`default`)\n3. first available model with valid auth\n\nIf restore fails, `modelFallbackMessage` explains fallback.\n\n### Auth priority\n\n`AuthStorage.getApiKey(...)` resolves in this order:\n\n1. runtime override (`setRuntimeApiKey`)\n2. stored credentials in `agent.db`\n3. provider environment variables\n4. custom-provider resolver fallback (if configured)\n\n## Event subscription model\n\nSubscribe with `session.subscribe(listener)`; it returns an unsubscribe function.\n\n```ts\nconst unsubscribe = session.subscribe((event) => {\n switch (event.type) {\n case \"agent_start\":\n case \"turn_start\":\n case \"tool_execution_start\":\n break;\n case \"message_update\":\n if (event.assistantMessageEvent.type === \"text_delta\") {\n process.stdout.write(event.assistantMessageEvent.delta);\n }\n break;\n }\n});\n```\n\n`AgentSessionEvent` includes core `AgentEvent` plus session-level events:\n\n- `auto_compaction_start` / `auto_compaction_end`\n- `auto_retry_start` / `auto_retry_end`\n- `retry_fallback_applied` / `retry_fallback_succeeded`\n- `ttsr_triggered`\n- `todo_reminder` / `todo_auto_clear`\n- `irc_message`\n\n## Prompt lifecycle\n\n`session.prompt(text, options?)` is the primary entry point.\n\nBehavior:\n\n1. optional command/template expansion (`/` commands, custom commands, file slash commands, prompt templates)\n2. if currently streaming:\n - requires `streamingBehavior: \"steer\" | \"followUp\"`\n - queues instead of throwing work away\n3. if idle:\n - validates model + API key\n - appends user message\n - starts agent turn\n\nRelated APIs:\n\n- `sendUserMessage(content, { deliverAs? })`\n- `steer(text, images?)`\n- `followUp(text, images?)`\n- `sendCustomMessage({ customType, content, ... }, { deliverAs?, triggerTurn? })`\n- `abort()`\n\n## Tools integration\n\n### Built-ins and filtering\n\n- Built-ins come from `createTools(...)` and `BUILTIN_TOOLS`.\n- `toolNames` acts as an allowlist for built-ins.\n- Hidden tools (for example `yield`) are opt-in unless required by options.\n\n```ts\nconst { session } = await createAgentSession({\n toolNames: [\"read\", \"grep\", \"find\", \"write\"],\n requireYieldTool: true,\n});\n```\n\n### Runtime tool set changes\n\n`AgentSession` supports runtime activation updates:\n\n- `getActiveToolNames()`\n- `getAllToolNames()`\n- `setActiveToolsByName(names)`\n\nSystem prompt is rebuilt to reflect active tool changes.\n\n## Discovery helpers\n\nUse these when you want partial control without recreating internal discovery logic:\n\n- `discoverAuthStorage(agentDir?)`\n- `discoverContextFiles(cwd?, _agentDir?)`\n- `discoverPromptTemplates(cwd?, agentDir?)`\n- `buildSystemPrompt(options?)`\n\n## Subagent-oriented options\n\nFor SDK consumers building orchestrators (similar to task executor flow):\n\n- `outputSchema`: passes structured output expectation into tool context\n- `requireYieldTool`: forces `yield` tool inclusion\n- `taskDepth`: recursion-depth context for nested task sessions\n- `parentTaskPrefix`: artifact naming prefix for nested task outputs\n\nThese are optional for normal single-agent embedding.\n\n## `createAgentSession()` return value\n\n```ts\ntype CreateAgentSessionResult = {\n session: AgentSession;\n setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void;\n modelFallbackMessage?: string;\n lspServers?: Array<{\n name: string;\n status: \"ready\" | \"error\";\n fileTypes: string[];\n error?: string;\n }>;\n eventBus: EventBus;\n};\n```\n\nUse `setToolUIContext(...)` only if your embedder provides UI capabilities that tools should call into.\n\n## Startup performance\n\n`createAgentSession()` runs two background optimizations to overlap I/O with the rest of session setup:\n\n- **Model-host preconnect.** As soon as the model is resolved, the SDK fires a best-effort `fetch.preconnect(model.baseUrl)` so DNS + TCP + TLS + HTTP/2 to the provider's host happens in parallel with tool registry build, and system-prompt assembly. The first real `fetch(...)` then reuses the warm connection, saving 100–300 ms on transcontinental hops (e.g. residential IP → `api.anthropic.com`). Implementation lives in `preconnectModelHost()` in `packages/coding-agent/src/sdk.ts`. If `fetch.preconnect` is unavailable (non-Bun runtime) or the call throws, the optimization is silently skipped — never a hard dependency. Applies to every mode (interactive, print, RPC, ACP).\n- **Conditional LSP warmup.** Startup LSP servers (those returned by `discoverStartupLspServers(cwd)`) are only warmed when **all** of these hold:\n - `enableLsp !== false` on the session options, **and**\n - `options.hasUI === true` (interactive TUI), **and**\n - the `lsp.diagnosticsOnWrite` setting is enabled.\n\n Print / script / RPC / ACP invocations (`hasUI=false`) skip the warmup entirely: they don't render the warmup status indicator and typically finish before the language servers would stabilize, so warming them just spends CPU parsing big `initialize` responses concurrently with the LLM stream consumer and jitters perceived latency. Tools that actually need an LSP server still spin one up on demand through `getOrCreateClient()` — only the *startup* warmup is skipped. The returned `lspServers` field in `CreateAgentSessionResult` is therefore `undefined` (not an empty array) whenever the warmup branch was bypassed.\n\n## Minimal controlled embed example\n\n```ts\nimport {\n createAgentSession,\n discoverAuthStorage,\n ModelRegistry,\n SessionManager,\n Settings,\n} from \"@sayknow-cli/coding-agent\";\n\nconst authStorage = await discoverAuthStorage();\nconst modelRegistry = new ModelRegistry(authStorage);\nawait modelRegistry.refresh();\n\nconst settings = Settings.isolated({\n \"compaction.enabled\": true,\n \"retry.enabled\": true,\n});\n\nconst { session } = await createAgentSession({\n authStorage,\n modelRegistry,\n settings,\n sessionManager: SessionManager.inMemory(),\n toolNames: [\"read\", \"grep\", \"find\", \"edit\", \"write\"],\n enableLsp: true,\n});\n\nsession.subscribe((event) => {\n if (\n event.type === \"message_update\" &&\n event.assistantMessageEvent.type === \"text_delta\"\n ) {\n process.stdout.write(event.assistantMessageEvent.delta);\n }\n});\n\nawait session.prompt(\"Find all TODO comments in this repo and propose fixes.\");\nawait session.dispose();\n```\n",
|
|
@@ -87,7 +88,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
87
88
|
"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",
|
|
88
89
|
"skc-session-clawhip-routing.md": "# Clawhip-routed SKC sessions\n\nThis guide documents the visible tmux session pattern used by operator bots such as Clawhip, Hermes, and OpenClaw when repository work must stay observable in a routed channel.\n\nUse this pattern when a human or chatops router needs to watch the session, receive stale-session alerts, and send follow-up prompts into the same visible SKC pane.\n\nFor pure machine control, prefer the Coordinator MCP tools in [`docs/hermes-mcp-bridge.md`](./hermes-mcp-bridge.md). For a single embedded worker process, prefer [`docs/rpc.md`](./rpc.md). This visible-session pattern is the operator-facing fallback/interop lane.\n\n## Contract\n\n1. Create or verify a dedicated git worktree for the issue or PR.\n2. Register a named tmux session with the host router before launching SKC.\n3. Start interactive `skc` inside the worktree.\n4. Wait until the SKC TUI is ready.\n5. Inject the real task prompt separately.\n6. Verify acceptance from actual work evidence, not from a visible pasted prompt.\n\nDo not launch visible routed work in the canonical repo checkout. Use a worktree so branch changes, generated files, tests, and cleanup stay scoped to the task.\n\n## Session naming\n\nUse stable names that include the project and artifact id:\n\n```text\nsayknow-cli-issue-905-ctrl-shift-enter-newline\nsayknow-cli-pr-911-ctrl-shift-enter-review\nclawhip-issue-269-lightweight-zero-receipt\n```\n\nAvoid ambiguous names such as `fix-tui`, `review`, or `issue-905` when multiple repositories route into the same chat surface.\n\n## Portable script shape\n\nThe exact router command is host-owned. A Clawhip-style wrapper usually has three small scripts:\n\n```sh\n# create.sh\n# create/register a routed tmux session and start interactive skc in the worktree\nscripts/skc-session/create.sh <session-name> <worktree-path> [channel-id] [mention]\n\n# prompt.sh\n# inject the real task after the TUI is ready\nscripts/skc-session/prompt.sh <session-name> @/path/to/task.md\n\n# tail.sh\n# inspect bounded pane output before/after prompt delivery\nscripts/skc-session/tail.sh <session-name> [lines]\n```\n\nThis repository includes a portable implementation in `scripts/skc-session/`. It keeps private routing values outside the script body: channel ids and mentions are runtime arguments, the router binary is optional, and credentials are never embedded. Host deployments can still override router behavior with environment variables instead of editing the scripts.\n\n\n## Included helper scripts\n\nThe `scripts/skc-session/` directory contains the public version of the operator helpers:\n\n- `create.sh` validates a dedicated git worktree, starts interactive `skc` in tmux, preserves the pane after exit, prints and writes the session-specific durable state path, writes `metadata.json`, mirrors pane output to `pane.log`, records lifecycle events in `events.log`, bridges the inner `skc` process to a public-safe `runtime-state.json`, writes normal-exit `final.json`, and optionally registers a Clawhip-style `tmux watch`.\n- `prompt.sh` sends a text or `@file` prompt only after the pane looks like a ready SKC TUI; if the tmux session vanished, it refuses injection and prints the durable metadata/log/final/events recovery paths plus the last pane-log excerpt.\n- `tail.sh` captures bounded pane output for readiness and acceptance checks, with durable metadata, pane-log, event-log, and final-status fallback when tmux vanished.\n- `harness-tmux-owner-start.sh` starts the SKC harness control plane with the RuntimeOwner resident inside tmux for dogfood/debug cases that need visible owner liveness.\n\nConfiguration is runtime-only:\n\n```sh\nexport SKC_BIN=/path/to/skc # optional; defaults to command -v skc\nexport SKC_SESSION_FLAGS=\"--model provider/model\" # optional interactive skc flags\nexport SKC_SESSION_ROUTER=clawhip # optional router binary\nexport SKC_SESSION_SKIP_ROUTER=1 # skip router registration\nexport SKC_SESSION_STATE_DIR=/tmp/skc-session-state # optional durable metadata/log root\nexport SKC_SESSION_LOG_SEARCH_ROOT=$HOME/Workspace # optional tail/prompt fallback search root\nexport SKC_SESSION_STALE_MINUTES=60 # router stale window\nexport SKC_SESSION_KEYWORDS=\"/skill:ralplan,Question\"\n```\n\nNo token, channel id, mention, workspace root, or private host path is hard-coded. Pass channel/mention values at invocation time when your router needs them.\n\n## Example flow\n\n```sh\n# 1. Prepare a dedicated worktree.\ngit -C /repo/sayknow-cli fetch origin dev\ngit -C /repo/sayknow-cli worktree add \\\n /repo/worktrees/sayknow-cli-issue-905-ctrl-shift-enter-newline \\\n -b issue-905-ctrl-shift-enter-newline origin/dev\n\n# 2. Start the routed visible session.\n./scripts/skc-session/create.sh \\\n sayknow-cli-issue-905-ctrl-shift-enter-newline \\\n /repo/worktrees/sayknow-cli-issue-905-ctrl-shift-enter-newline \\\n \"$CHANNEL_ID\" \\\n \"$MENTION\"\n\n# 3. Confirm TUI readiness.\n./scripts/skc-session/tail.sh sayknow-cli-issue-905-ctrl-shift-enter-newline 80\n\n# 4. Inject the task prompt.\n./scripts/skc-session/prompt.sh \\\n sayknow-cli-issue-905-ctrl-shift-enter-newline \\\n @/tmp/issue-905-task.md\n\n# 5. Confirm real work started.\n./scripts/skc-session/tail.sh sayknow-cli-issue-905-ctrl-shift-enter-newline 160\n```\n\n## Prompt shape\n\nImplementation prompt:\n\n```text\n/skill:ralplan\n\nskc ultragoal fix issue #905 missed Ctrl+Shift+Enter newline case.\n\nRepo: jaybeyond/Sayknow_CLI\nWorktree: /repo/worktrees/sayknow-cli-issue-905-ctrl-shift-enter-newline\nBranch: issue-905-ctrl-shift-enter-newline\nBase: dev\n\nScope:\n- inspect parser/key matching and packages/tui/src/components/editor.ts\n- add explicit ctrl+shift+enter newline handling\n- add focused tests for the reported terminal sequences\n- run targeted verification\n- commit, push, and open a PR to dev\n\nNon-goals:\n- no unrelated tmux/session/process changes\n- no synchronous filesystem, process, tmux, network, or durable writes in keystroke paths\n```\n\nReview prompt:\n\n```text\n/skill:ralplan\n\nReview PR #911 as a red-team-only merge gate.\nInspect origin/dev...HEAD, changed files, CI, and contract risks.\nLook for blockers, regressions, test gaps, and hidden user-facing drift.\nPost MERGE_READY or REQUEST_CHANGES with evidence. Do not merge.\n```\n\n## Acceptance checks\n\nAfter prompt delivery, require one of these before reporting that the session is working:\n\n- a tool call or file read in the pane,\n- an explicit plan or todo update,\n- a diff or test command,\n- a GitHub comment/review/PR URL,\n- a terminal verdict such as `MERGE_READY` or `REQUEST_CHANGES`.\n\nA prompt being visible in tmux scrollback is not acceptance by itself. If tmux disappears before terminal verdict, inspect the state path printed by `create.sh`: `metadata.json` identifies the worktree/session and links `runtime-state.json`; `runtime-state.json` contains public-safe SKC lifecycle state (`completed` / `errored`, timestamps, cwd/workdir, branch, session file, exit code/signal/error when available) without pane logs, prompts, transcripts, tokens, config, environment dumps, or raw tool output; `pane.log` contains the private mirrored transcript; `events.log` records launch/exit milestones; `final.json` is present when the wrapper observed `skc` exit; and `vanished.json` is present when the external monitor observed the tmux session disappear. Use `tail.sh <session-name> [lines]` to surface these artifacts without a live tmux server.\n\n## Anti-patterns\n\n- Starting `skc -p` for long-running visible repo work.\n- Launching from the canonical repo checkout instead of a task worktree.\n- Running a long SKC/tmux session under a short shell timeout that can SIGKILL the owner process.\n- Treating tmux process existence as proof that the prompt was accepted.\n- Restarting a vanished session without first checking its durable metadata, pane log, event log, and final status.\n- Hard-coding private channel ids, bot mentions, or router tokens into public SKC docs.\n- Using this visible-session pattern when Coordinator MCP turn state is available and sufficient.\n",
|
|
89
90
|
"standalone-mcp.md": "# Standalone SKC MCP support\n\nThis page answers the common user question: “Does normal `skc` inherit my Claude Code/Codex MCP servers, or can I configure MCP servers directly for the standalone TUI?”\n\n## Short answer\n\nNormal standalone SKC (`skc`, `skc --tmux`, and print-mode prompts) does **not** inherit MCP servers from Claude Code, Codex, Cursor, Gemini, Windsurf, or other tools as a public startup contract.\n\nStandalone SKC also has a narrow direct-registration command for explicit user-provided server definitions:\n\n```bash\nskc mcp add context7 npx -y @upstash/context7-mcp\nskc mcp add docs --type http --url https://example.test/mcp --header Authorization=\"Bearer $TOKEN\"\nskc mcp list\nskc mcp remove context7\n```\n\n`skc mcp add` writes only the definition supplied on that invocation to SKC's own MCP config (`~/.skc/agent/mcp.json` by default, or `./.skc/mcp.json` with `--project`). It does not read Claude Code, Codex, OpenCode, Cursor, Gemini, Windsurf, or other live configs. `skc mcp list` and `skc mcp remove` print redacted definitions so env/header/auth/OAuth credential values are not exposed in public output. These registrations are storage-only today: normal standalone `skc`, `skc --tmux`, and print-mode sessions do not load them as runtime tools.\n\n## What is supported today\n\n| Need | Use | Notes |\n| --- | --- | --- |\n| External bot or multi-session controller wants to drive SKC | [Coordinator MCP](./hermes-mcp-bridge.md) via `skc mcp-serve coordinator` | SKC exposes an **outward** MCP server with SKC coordinator tools. This is not a way to import arbitrary MCP tools into the standalone TUI. |\n| Editor/ACP client owns MCP servers and wants SKC as the agent backend | [ACP mode](./external-control-readiness.md#acp-mode) via `skc --mode acp` or `skc acp` | The ACP client supplies and owns MCP servers. SKC keeps those client-owned MCP tools isolated from standalone on-disk discovery. |\n| Host application already manages MCP servers and policies | [RPC host tools](./rpc.md#host-tool-sub-protocol) via `skc --mode rpc` | Convert the selected MCP capabilities into host-owned RPC tools. The host executes the MCP call and returns `host_tool_result`. |\n| OpenClaw/Hermes-style host wants to map its own MCP/skills into SKC | [OpenClaw / Hermes RPC integration notes](./openclaw-hermes-rpc-integration.md) | Treat MCP as a host implementation detail and expose only policy-approved capabilities as RPC host tools. |\n| Codex / Claude Code want a one-step install to delegate planning/execution to SKC | [Canonical sayknow-cli plugin](./hermes-mcp-bridge.md) under `plugins/` via `skc setup claude` / `skc setup codex` | Installs the Coordinator MCP server plus `skc_delegate_plan/execute/team` commands. Fail-closed: workdir-scoped roots, mutations off until opt-in. Install with `codex plugin marketplace add ./plugins` (verified on Codex CLI 0.139.0) or `/plugin marketplace add ./plugins` for Claude Code. |\n\n## What standalone SKC does not do\n\nStandalone SKC does **not** currently promise any of these behaviors:\n\n- reading Claude Code's global MCP server list and automatically enabling it;\n- reading Codex MCP server config as an inherited runtime contract;\n- merging multiple tools' MCP configs into the normal TUI at startup;\n- making `.mcp.json`, `mcp.json`, `.codex/config.toml`, or other discovered files a stable public standalone-TUI config API;\n- exposing Coordinator MCP tools as ordinary in-session model tools.\n\nThis boundary is intentional: MCP servers often carry credentials, local filesystem reach, browser/session state, approval semantics, and tool names that belong to the host that configured them. Blind inheritance would mix policies between products and make it unclear which process owns credentials, approvals, sandboxing, and lifecycle.\n\n## Recommended workaround for a specific MCP server\n\nIf you need a context engine, internal search server, browser MCP, database MCP, or another custom MCP inside SKC:\n\n1. Keep the MCP server configured in the host that owns its credentials and policy.\n2. Start SKC through RPC (`skc --mode rpc`) from that host.\n3. Register a narrow host-owned tool with `set_host_tools` / `RpcClient#setCustomTools()`.\n4. Have the host tool call the real MCP server and return the result to SKC as `host_tool_result`.\n\nThat shape keeps the MCP server's auth, approvals, filesystem access, and process lifetime with the host while still letting the SKC model request the capability when needed.\n\nFor multi-session orchestration, prefer Coordinator MCP instead. Coordinator MCP lets an external controller start/register sessions, send turns, answer questions, read artifacts, and write durable status reports; it does not import arbitrary MCP servers into a standalone TUI session.\n\n## Related docs\n\n- [Coordinator MCP bridge](./hermes-mcp-bridge.md)\n- [External control surface readiness](./external-control-readiness.md)\n- [RPC Protocol Reference](./rpc.md)\n- [OpenClaw / Hermes RPC integration notes](./openclaw-hermes-rpc-integration.md)\n- [Clawhip-routed SKC sessions](./skc-session-clawhip-routing.md)\n",
|
|
90
|
-
"telegram-onboarding.md": "# Telegram notification onboarding\n\nThis guide documents the current bundled Telegram notification setup path from\nSayknow-CLI source. It is for the managed reference client used by\n`skc notify setup`, not a separate remote-control product.\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/notifications/<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. Run the interactive setup wizard\n\nFrom any terminal where `skc` is installed:\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\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. What setup writes\n\n`skc notify setup` writes these settings through the SKC Settings layer:\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\nAt runtime, notifications are considered globally configured only when all of\nthese are present:\n\n- `notifications.enabled`\n- `notifications.telegram.botToken`\n- `notifications.telegram.chatId`\n\nEnvironment/session precedence from `packages/coding-agent/src/notifications/config.ts`:\n\n1. `SKC_NOTIFICATIONS=0` is a hard opt-out.\n2. Local `/notify off` disables only the current session.\n3. `SKC_NOTIFICATIONS=1` or `SKC_NOTIFICATIONS_TOKEN` enables the legacy explicit path.\n4. A complete global setup enables notifications automatically.\n5. Otherwise notifications stay off.\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. When the notification extension is\nregistered, the session writes its endpoint discovery file and ensures the\nTelegram daemon is running.\n\nThe daemon is a singleton per bot token/chat pair. Telegram allows only one\nactive `getUpdates` long-poll owner for a bot token, so SKC keeps a local daemon\nlock/state file and makes later sessions attach to the fresh owner instead of\nstarting a second poller. This avoids Telegram `409 Conflict` failures.\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.\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\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`\n - `/lean`\n - `/verbosity <lean|verbose>`\n - `/redact <on|off>`\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\n## 8. Local `/notify` inside a session\n\nInside a running SKC session:\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 global setup is complete and\n `SKC_NOTIFICATIONS=0` is not forcing opt-out.\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/notifications/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### Telegram 409 conflict\n\nOnly one `getUpdates` poller can own a bot token. Stop any old manual bridge or\nexternal bot process using the same token, then let SKC's managed daemon own it.\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/notifications/<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",
|
|
91
|
+
"telegram-onboarding.md": "# Telegram notification onboarding\n\nThis guide documents the current bundled Telegram notification setup path from\nSayknow-CLI source. It is for the managed reference client used by\n`skc notify setup`, not a separate remote-control product.\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/notifications/<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. Run the interactive setup wizard\n\nFrom any terminal where `skc` is installed:\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\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. What setup writes\n\n`skc notify setup` writes these settings through the SKC Settings layer:\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\nAt runtime, notifications are considered globally configured only when all of\nthese are present:\n\n- `notifications.enabled`\n- `notifications.telegram.botToken`\n- `notifications.telegram.chatId`\n\nEnvironment/session precedence from `packages/coding-agent/src/notifications/config.ts`:\n\n1. `SKC_NOTIFICATIONS=0` is a hard opt-out.\n2. Local `/notify off` disables only the current session.\n3. `SKC_NOTIFICATIONS=1` or `SKC_NOTIFICATIONS_TOKEN` enables the legacy explicit path.\n4. A complete global setup enables notifications automatically.\n5. Otherwise notifications stay off.\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. When the notification extension is\nregistered, the session writes its endpoint discovery file and ensures the\nTelegram daemon is running.\n\nThe daemon is a singleton per bot token/chat pair. Telegram allows only one\nactive `getUpdates` long-poll owner for a bot token, so SKC keeps a local daemon\nlock/state file and makes later sessions attach to the fresh owner instead of\nstarting a second poller. This avoids Telegram `409 Conflict` failures.\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/notifications/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/notifications/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\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`\n - `/lean`\n - `/verbosity <lean|verbose>`\n - `/redact <on|off>`\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\n## 8. Local `/notify` inside a session\n\nInside a running SKC session:\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 global setup is complete and\n `SKC_NOTIFICATIONS=0` is not forcing opt-out.\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/notifications/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. Stop any old manual bridge or\nexternal bot process using the same token, then let SKC's managed daemon own it.\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/notifications/<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",
|
|
91
92
|
"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",
|
|
92
93
|
"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",
|
|
93
94
|
"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",
|
|
@@ -105,7 +106,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
105
106
|
"tools/eval.md": "# eval\n\n> Execute Python or JavaScript code in persistent cell-based runtimes.\n\n> **Notice:** Do not shell out to `python -c`/`python -e`, `bun -e`, or `node -e` via the `bash` tool for ad-hoc code execution. Use this tool instead — it gives you persistent state across cells, structured `display()` output, image/JSON capture, and proper cancellation/timeout handling that one-shot `-e`/`-c` invocations cannot provide.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/eval.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/eval.md`\n- Key collaborators:\n - `packages/coding-agent/src/eval/backend.ts` — backend execution contract\n - `packages/coding-agent/src/eval/js/index.ts` — JS backend adapter\n - `packages/coding-agent/src/eval/js/executor.ts` — JS execution + output sink\n - `packages/coding-agent/src/eval/js/context-manager.ts` — persistent VM contexts, prelude, tool bridge\n - `packages/coding-agent/src/eval/js/prelude.txt` — JS global helpers\n - `packages/coding-agent/src/eval/py/index.ts` — Python backend adapter\n - `packages/coding-agent/src/eval/py/executor.ts` — kernel session retention, reset, cleanup\n - `packages/coding-agent/src/eval/py/kernel.ts` — Jupyter gateway/kernel protocol, display capture\n - `packages/coding-agent/src/eval/py/prelude.py` — Python helper functions and status events\n - `packages/coding-agent/src/session/streaming-output.ts` — truncation, artifacts, streamed chunks\n - `docs/python-repl.md` — Python kernel/gateway internals\n\n## Inputs\n\nTool parameters are a JSON object with a single `cells` field — an ordered array of cell objects. Each cell is a structured record; there is no `*** Cell` header parsing, no language sniffing, and no implicit single-cell fallback. Cells run in array order; state persists within each language across cells and across tool calls.\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `cells` | `EvalCellInput[]` | Yes | Cells executed in order. At least one cell is required (`.min(1)`). |\n\nEach `EvalCellInput` (from `evalCellSchema` in `packages/coding-agent/src/tools/eval.ts`):\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `language` | `\"py\" \\| \"js\"` | Yes | Backend selector. `\"py\"` maps to the IPython/Jupyter kernel (`python` backend); `\"js\"` maps to the persistent JavaScript VM. |\n| `code` | `string` | Yes | Cell body, verbatim. JSON-encoded — embed newlines, quotes, and indentation directly; no fences, no headers. |\n| `title` | `string` | No | Short label rendered in the transcript (e.g. `\"imports\"`, `\"load config\"`). |\n| `timeout` | `integer` | No | Per-cell timeout in seconds, clamped to `1..600`. Defaults to 30 when omitted. |\n| `reset` | `boolean` | No | Wipe this cell's language kernel before running. Reset is per-language: a `py` cell's reset does not touch the JS VM and vice versa. Defaults to `false`. |\n\nMinimal example matching the live schema:\n\n```json\n{\n \"cells\": [\n { \"language\": \"py\", \"title\": \"imports\", \"timeout\": 10, \"code\": \"import json\\nfrom pathlib import Path\" },\n { \"language\": \"py\", \"title\": \"load config\", \"code\": \"data = json.loads(read('package.json'))\\ndisplay(data)\" },\n { \"language\": \"js\", \"title\": \"summary\", \"reset\": true, \"code\": \"const data = JSON.parse(await read('package.json'));\\ndisplay(data);\\nreturn data.name;\" }\n ]\n}\n```\n\n## Outputs\n\nFinal result from `EvalTool.execute()` is single-shot, but `onUpdate` streams partial text and `details` while cells run.\n\nReturned shape:\n\n- `content`: one text block containing combined cell output, or `(no text output)` / `(no output)` when only rich outputs exist.\n- `details` (`EvalToolDetails` from `packages/coding-agent/src/eval/types.ts`):\n - `cells`: per-cell code, status (`pending`/`running`/`complete`/`error`), output, duration, exit code, status events, markdown flag\n - `language`: first backend used\n - `languages`: distinct backends used, in first-use order\n - `jsonOutputs`: structured values emitted via `display(...)`\n - `images`: image payloads emitted by Python rich display or JS `display({ type: \"image\", ... })`\n - `statusEvents`: aggregated helper/tool status events\n - `notice`: backend fallback notice (currently unused; reserved for future per-cell notices)\n - `meta`: truncation metadata\n - `isError`: set on cell failure or cancellation\n\nRenderer behavior in `packages/coding-agent/src/tools/eval.ts`:\n\n- call preview renders each cell's `code` with syntax highlighting based on its declared `language`\n- result view renders each cell separately, including status, duration, and output\n- markdown outputs are rendered with the Markdown component instead of plain text\n- `jsonOutputs` render as a tree, collapsed or expanded depending on UI state\n- timeout / truncation notices render as dim metadata lines\n- images are carried in `details.images`; generic tool UI image handling renders them outside the text block\n\nSide-channel artifacts:\n\n- `session.allocateOutputArtifact?.(\"eval\")` may allocate an `artifact://...` backing store for spilled output.\n- Truncated output metadata points at that artifact when available.\n\n## Flow\n\n1. `EvalTool.execute()` in `packages/coding-agent/src/tools/eval.ts` receives `params.cells` already validated by the Zod schema — no string parsing step.\n2. For each cell, `execute()` maps `cell.language` to an `EvalLanguage` (`\"py\"` → `\"python\"`, `\"js\"` → `\"js\"`) and calls `resolveBackend(session, language)`:\n - `python` is gated on `eval.py !== false` and `pythonBackend.isAvailable(session)`.\n - `js` is gated on `eval.js !== false`.\n - A disabled or unavailable requested backend throws `ToolError`; there is no auto-fallback or sniffing.\n3. The tool allocates an `OutputSink`, a `TailBuffer`, per-cell result objects, and a `sessionAbortController`. `session.trackEvalExecution?.(...)` can wrap the whole run for external cancellation tracking.\n4. Cells execute sequentially. For each cell, `execute()`:\n - clamps `(cell.timeout ?? 30) * 1000` ms through `clampTimeout(\"eval\", ...)`\n - builds a combined abort signal from the tool signal, the timeout, and the session abort controller\n - marks the cell `running` and emits an update\n - calls the backend’s `execute()` with `cwd`, `sessionId`, `sessionFile`, `kernelOwnerId`, `deadlineMs`, `reset` (defaults to `false`), artifact info, and chunk callback\n5. JS cells dispatch through `packages/coding-agent/src/eval/js/index.ts` into `executeJs()`; Python cells dispatch through `packages/coding-agent/src/eval/py/index.ts` into `executePython()`.\n6. Backend text chunks stream into the shared `OutputSink`; rich outputs are accumulated separately as JSON, images, markdown markers, and status events.\n7. After each cell:\n - text output is trimmed and stored on that cell result\n - multi-cell runs prefix text with `[i/n]` and the optional title\n - cancellations return early with `isError: true` and a cell-specific abort message\n - non-zero exit codes return early with `isError: true` and a message naming the failed cell\n - later cells are skipped after the first error, but earlier cell state persists in the underlying runtime\n8. On success, the tool joins all cell outputs, synthesizes `(no text output)` or `(no output)` when needed, and attaches truncation metadata from `summarizeFinal()`.\n9. The renderer uses `details.cells`, `details.jsonOutputs`, and `details.statusEvents` to build notebook-style output. `mergeCallAndResult = true` and `inline = true`, so call and result render together in the transcript.\n\n## Modes / Variants\n\n### Backend selection\n\nBackend choice is **explicit per cell** — there is no auto-detection.\n\n- `language: \"py\"` → Python (IPython/Jupyter) backend\n- `language: \"js\"` → JavaScript VM backend\n\nIf the requested backend is disabled or unavailable, the tool throws `ToolError` for that cell. The caller chooses; the tool does not silently substitute.\n\n### JavaScript runtime\n\nImplemented in `packages/coding-agent/src/eval/js/context-manager.ts` and `packages/coding-agent/src/eval/js/prelude.txt`.\n\n- Persistent `vm.Context` instances keyed by `js:${sessionId}` in `vmContexts`\n- `reset: true` calls `resetVmContext(sessionKey)` before the cell executes\n- Top-level `await` and bare `return` are supported by wrapping code in an async IIFE when `wrapCode()` sees `await` or `return`\n- Top-level static `import ... from ...` and dynamic `import(...)` calls are routed through `rewriteImports()`, which sends them via `__skc_import__` so the specifier resolves against the session cwd\n- Module cache is busted for **local** imports between cells so edits to source files are picked up without restarting the runtime. `__skc_import__` deletes `require.cache[absPath]` before re-importing whenever the original specifier is a filesystem path: relative (`./x`, `../x`, `.`, `..`), POSIX-absolute (`/...`), home-prefixed (`~/...`), or Windows drive-letter (`C:\\...` / `C:/...`). Bare specifiers (`react`, `lodash/x`) and URL/scheme specifiers (`node:fs`, `file://...`, `https://...`) are left in cache so package identity stays stable across cells. The cache-bust only fires when the resolved target is an absolute path — unresolved bare-package fallbacks (`resolveImportSpecifier()` returning the original specifier) skip it.\n- The prelude installs globals:\n - `display`, `print`\n - `read`, `write`, `append`, `sort`, `uniq`, `counter`, `diff`, `tree`, `env`, `output`\n - `tool.<name>(args)` proxy for arbitrary session tool calls\n- JS helpers are async because they cross the VM/tool boundary\n- `display(value)` behavior:\n - plain objects/arrays become JSON outputs\n - `{ type: \"image\", data, mimeType }` becomes an image output\n - scalars become text\n- The VM exposes a restricted `process` subset plus `Buffer`, `fetch`, `Blob`, `File`, `Headers`, `Request`, `Response`, `fs`, `require`, and browser-style globals\n- Per-session VM runs are serialized with `runQueued()`\n\n### Python runtime\n\nImplemented in `packages/coding-agent/src/eval/py/executor.ts`, `packages/coding-agent/src/eval/py/kernel.ts`, and `packages/coding-agent/src/eval/py/prelude.py`. See `docs/python-repl.md` for gateway and kernel details.\n\n- Default mode is retained `session` kernels keyed by `python:${sessionId}`\n- Optional `python.kernelMode = \"per-call\"` creates a fresh kernel for each cell and shuts it down afterward\n- `reset: true` disposes the retained kernel for that session before the cell runs; later Python cells in the same tool call reuse the fresh kernel\n- Startup path:\n - availability check\n - create/connect kernel\n - initialize cwd / env / `sys.path`\n - execute `PYTHON_PRELUDE`\n- Python cells run inside IPython/Jupyter, so top-level `await` works; the prompt warns not to use `asyncio.run(...)`\n- The Python prelude defines synchronous helpers with the same surface as JS (except `tool.<name>` exists only in JS)\n- `display(value)` wraps dict/list/tuple values in `IPython.display.JSON`; rich display MIME bundles are preserved\n- Kernel `display_data` / `execute_result` messages map to:\n - `application/x-skc-status` → status event\n - `image/png` → image output\n - `application/json` → JSON output\n - `text/markdown` → markdown output\n - `text/plain` → text output\n - `text/html` → HTML converted to markdown with `htmlToBasicMarkdown()`\n- Interactive stdin is rejected: `input_request` sends an empty reply, marks `stdinRequested`, and the executor returns exit code `1`\n\n### Multi-language call behavior\n\nA single tool call can mix Python and JS cells. Persistence is per language runtime:\n\n- `reset: true` on a Python cell does not touch JS state\n- `reset: true` on a JS cell does not touch Python state\n- each backend keeps its own retained session keyed from the same session-derived ID\n\n## Side Effects\n\n- Filesystem\n - JS/Python prelude helpers can read, write, append, diff, and traverse files under the session cwd or absolute paths.\n - Output may spill to an artifact file via `OutputSink`.\n- Network\n - Python backend speaks NDJSON to a local `python3` subprocess over stdin/stdout (no network).\n - JS runtime exposes `fetch` and `tool.<name>()`; those tools may perform additional network I/O.\n- Subprocesses / native bindings\n - Python availability check runs `<python> -c ...`.\n - Python backend spawns one `python -u runner.py` subprocess per kernel; cancellation sends `SIGINT`. Details in `docs/python-repl.md`.\n- Session state\n - `session.assertEvalExecutionAllowed?.()` can block execution.\n - `session.trackEvalExecution?.(...)` can register cancellable eval work.\n - `session.getSessionFile?.()` and `session.getEvalKernelOwnerId?.()` influence kernel reuse and artifact lookup.\n - JS VM contexts persist in `vmContexts` across eval calls until reset/disposal.\n - Python retained kernels persist in `kernelSessions` until reset, eviction, idle cleanup, or owner cleanup.\n- User-visible prompts / interactive UI\n - none; stdin requests are rejected programmatically\n- Background work / cancellation\n - Python retained kernels have heartbeat and idle cleanup timers.\n - Cancellation interrupts a running Python kernel and aborts JS promise waits.\n\n## Limits & Caps\n\n- Per-cell timeout default: 30s (applied when `timeout` is omitted in `EvalTool.execute()`; clamped through `TOOL_TIMEOUTS.eval.default` in `packages/coding-agent/src/tools/tool-timeouts.ts`)\n- Schema-level `timeout` range: integer `1..600` seconds (enforced by Zod on the cell schema)\n- Timeout clamp at runtime: 1s minimum, 600s maximum (`TOOL_TIMEOUTS.eval` in `packages/coding-agent/src/tools/tool-timeouts.ts`)\n- Transcript code/output preview: 10 lines by default (`EVAL_DEFAULT_PREVIEW_LINES` in `packages/coding-agent/src/tools/eval.ts`)\n- Output truncation window: 50KB default (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`)\n- Output line cap inside truncation helpers: 3000 lines (`DEFAULT_MAX_LINES` in `packages/coding-agent/src/session/streaming-output.ts`)\n- Streaming tail buffer for live updates: `DEFAULT_MAX_BYTES * 2` = 100KB (`packages/coding-agent/src/tools/eval.ts`)\n- Python retained kernel idle timeout: 5 minutes (`IDLE_TIMEOUT_MS` in `packages/coding-agent/src/eval/py/executor.ts`)\n- Python retained kernel cap: 4 sessions (`MAX_KERNEL_SESSIONS` in `packages/coding-agent/src/eval/py/executor.ts`)\n- Python retained kernel cleanup sweep: every 30s (`CLEANUP_INTERVAL_MS` in `packages/coding-agent/src/eval/py/executor.ts`)\n- Python owner-cleanup shutdown wait: 2000ms (`OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS` in `packages/coding-agent/src/eval/py/executor.ts`)\n- Python heartbeat interval: 5s (`ensureKernelHeartbeat()` in `packages/coding-agent/src/eval/py/executor.ts`)\n- Python external gateway availability check timeout: 5s (`AbortSignal.timeout(5000)` in `packages/coding-agent/src/eval/py/kernel.ts`)\n- Python auto-restart budget: one restart per retained session before hard failure (`restartCount > 1` in `packages/coding-agent/src/eval/py/executor.ts`)\n\n## Errors\n\n- Zod validation rejects malformed `cells` arrays before `execute()` runs (missing `language`/`code`, out-of-range `timeout`, empty `cells`).\n- Missing session without proxy executor throws `ToolError(\"Eval tool requires a session when not using proxy executor\")`.\n- Disabled/unavailable backends throw `ToolError` from `resolveBackend()`:\n - `eval.py = false` and a `py` cell is requested\n - `eval.js = false` and a `js` cell is requested\n - Python kernel unavailable and a `py` cell is requested\n- JS runtime exceptions are converted into text output plus `exitCode: 1`; cancellations return `cancelled: true` and may append `Command timed out`.\n- Python execution errors from the kernel become text output and `exitCode: 1`; later cells are skipped.\n- Python stdin requests are treated as errors with the message `Kernel requested stdin; interactive input is not supported.`\n- Cancellation is returned, not thrown, once backend execution has started. The tool formats it as a cell failure and sets `details.isError = true`.\n- If output truncates, the tool still succeeds; truncation is surfaced through `details.meta` and artifact-backed full output when available.\n\n## Notes\n\n- Backend selection is now strictly explicit per cell: `language` must be `\"py\"` or `\"js\"`. The previous `*** Cell` header parser, the `eval.lark` constrained grammar, and the sniffer-based fallback have all been removed.\n- `EvalTool.customFormat` no longer exists. Tool calls flow through the standard JSON schema; there is no Lark-constrained sampling path.\n- `tool.<name>()` exists only in JS. Python prelude helpers do not call back into the full tool registry.\n- JS helper paths reject protocol URIs (`://`) in `resolvePath()`; the JS prelude is filesystem-only unless the code calls `tool.read(...)` or another tool explicitly.\n- Python helper `output(...)` depends on `SKC_SESSION_FILE`; it fails outside a session-backed run.\n- `display()` can produce text and structured outputs from the same value; the renderer prefers markdown over `text/plain` when both exist.\n- JS static imports are rewritten only at top level. Nested imports stay invalid and surface normal JS syntax/runtime errors.\n- `EvalTool` is `concurrency = \"exclusive\"`, so eval calls do not overlap within a session.\n- The tool description shown to the model is templated by backend availability (`getEvalToolDescription()`); if Python is unavailable, the prompt omits Python-specific instructions.\n",
|
|
106
107
|
"tools/find.md": "# find\n\n> Find filesystem paths by glob; use `search` when you need content matches instead of path matches.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/find.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/find.md`\n- Key collaborators:\n - `packages/coding-agent/src/tools/path-utils.ts` — normalize inputs; split base path vs glob.\n - `packages/coding-agent/src/tools/list-limit.ts` — apply result-count caps.\n - `packages/coding-agent/src/session/streaming-output.ts` — truncate text output at byte cap.\n - `packages/coding-agent/src/tools/tool-result.ts` — build `content` and `details.meta`.\n - `packages/coding-agent/src/tools/output-meta.ts` — encode limit / truncation metadata.\n - `packages/coding-agent/src/tools/tool-errors.ts` — map user-facing tool errors.\n - `packages/coding-agent/src/tools/index.ts` — register the built-in local implementation.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `paths` | `string[]` | Yes | One or more globs, files, or directories. Empty strings are rejected. Multiple entries may be merged into one brace-union search when their base paths can be resolved together. |\n| `hidden` | `boolean` | No | Whether hidden files are included. Defaults to `true` (`hidden ?? true`). |\n| `limit` | `number` | No | Max returned paths. Defaults to `1000`. Must be a finite positive number; non-integers are floored. |\n\n## Outputs\nThe tool returns a single text block plus structured `details`.\n\n- Success text: newline-delimited paths, one per line, relative to the session cwd when possible; absolute when outside cwd. Exact file inputs return that file path as one line.\n- Empty result text: `No files found matching pattern`.\n- Multi-path partial miss: appends `Skipped missing paths: ...` after the result block, or after the empty-result line.\n- `details` may include:\n - `scopePath`: display form of the searched root or merged roots.\n - `fileCount`: number of paths returned after result limiting.\n - `files`: returned paths as an array.\n - `truncated`: whether result count or byte truncation occurred.\n - `resultLimitReached`: reached result limit.\n - `missingPaths`: skipped missing inputs in multi-path calls.\n - `truncation` / `meta.limits`: structured truncation and limit metadata for renderers.\n- Streaming: when the runtime supplies `onUpdate`, the local implementation emits incremental newline-delimited text snapshots during globbing, throttled to 200 ms.\n\n## Flow\n1. `FindTool.execute()` normalizes each `paths` entry with `normalizePathLikeInput()` and `/\\\\/g -> \"/\"` (`packages/coding-agent/src/tools/find.ts`). Empty normalized entries fail with `` `paths` must contain non-empty globs or paths ``.\n2. For multi-path local calls, `partitionExistingPaths(..., parseFindPattern)` (`packages/coding-agent/src/tools/path-utils.ts`) stats each base path. Missing entries are skipped; if all are missing, the tool throws `Path not found: ...`. Single missing paths still hard-fail.\n3. The tool tries `resolveExplicitFindPatterns()` to merge multiple inputs into one search rooted at a common base path. If that does not apply, it parses one input with `parseFindPattern()`.\n4. `parseFindPattern()` determines `(basePath, globPattern, hasGlob)`:\n - no glob chars (`*`, `?`, `[`, `{`) => search that path with implicit `**/*`.\n - glob in the first segment => search from `.` and, unless the pattern already starts with `**/`, prefix it with `**/`.\n - glob later in the path => split at the first glob-bearing segment.\n5. `resolveToCwd()` converts the base path to an absolute path under the session cwd. A resolved `/` is rejected with `Searching from root directory '/' is not allowed`.\n6. `limit` is defaulted to `DEFAULT_LIMIT` (`1000`) and validated as a positive finite integer. `hidden` defaults to `true`. The tool also creates a 5 s timeout via `AbortSignal.timeout(GLOB_TIMEOUT_MS)`.\n7. Execution then branches:\n - **Custom operations branch**: if `FindToolOptions.operations.glob` exists, the tool checks existence with `operations.exists()`, short-circuits exact-file inputs via `operations.stat()` when available, then calls `operations.glob(globPattern, searchPath, { ignore: [\"**/node_modules/**\", \"**/.git/**\"], limit })`.\n - **Built-in local branch**: the tool stats `searchPath`. Exact-file inputs return immediately. Directory inputs call `natives.glob()` with `fileType: File`, `hidden`, `maxResults: limit`, `sortByMtime: true`, `gitignore: true`, and the combined abort signal.\n8. In the local branch, optional `onMatch` callbacks convert each match to a cwd-relative display path and emit throttled progress updates.\n9. After native glob returns, JS sorts `result.matches` by `mtime` descending (`(b.mtime ?? 0) - (a.mtime ?? 0)`) before formatting paths.\n10. `buildResult()` applies `applyListLimit()` to cap the array again at `limit`, joins paths with `\\n`, then runs `truncateHead()` with `maxLines: Number.MAX_SAFE_INTEGER`. In practice this leaves the 50 KB byte cap in place while disabling the default 3000-line cap.\n11. `toolResult()` packages text plus `details`, and records result-limit / truncation metadata for renderers.\n\n## Modes / Variants\n- **Exact file path**: if the parsed input has no glob and the resolved path stats as a file, output is that one path.\n- **Directory path**: if the parsed input has no glob and stats as a directory, the tool searches it with implicit `**/*`.\n- **Single glob path**: one input parsed by `parseFindPattern()`.\n- **Merged multi-path search**: multiple inputs resolved by `resolveExplicitFindPatterns()` into one brace-union glob rooted at a common base path.\n- **Partial multi-path search with missing inputs**: local multi-path calls skip missing base paths and surface them as `missingPaths` / `Skipped missing paths: ...`.\n- **Custom delegated search**: uses injected `FindOperations` instead of local fs + native glob.\n\n## Side Effects\n- Filesystem\n - Stats the resolved base path, and in local multi-path mode stats every candidate base path up front.\n - Does not write files.\n- Subprocesses / native bindings\n - Built-in local mode calls the native `@sayknow-cli/natives` glob implementation.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Emits structured progress updates when `onUpdate` is provided.\n - Adds truncation / limit metadata to the tool result.\n- Background work / cancellation\n - Local globbing is cancellable through the caller abort signal plus an internal 5 s timeout.\n\n## Limits & Caps\n- Default result limit: `1000` (`DEFAULT_LIMIT` in `packages/coding-agent/src/tools/find.ts`).\n- Local glob timeout: `5000` ms (`GLOB_TIMEOUT_MS` in `packages/coding-agent/src/tools/find.ts`).\n- Output byte cap: `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`).\n- Default generic line cap in `truncateHead()` is `3000`, but `find` overrides `maxLines` to `Number.MAX_SAFE_INTEGER`, so byte size — not line count — is the practical output truncation cap.\n- Streaming update throttle: `200` ms between `onUpdate` emissions.\n- Sort order: most recent `mtime` first in the built-in local branch and promised in the prompt. The tool re-sorts in JS even though native glob receives `sortByMtime: true` so native code can still stop early at `maxResults`.\n\n## Errors\n- User-facing `ToolError`s from `FindTool.execute()` include:\n - `` `paths` must contain non-empty globs or paths ``\n - `Path not found: ...`\n - `Searching from root directory '/' is not allowed`\n - `Limit must be a positive number`\n - `Path is not a directory: ...`\n - `find timed out after 5s`\n- If the caller aborts, the local branch converts `AbortError` into `ToolAbortError`.\n- Non-`ENOENT` stat failures and other unexpected errors are rethrown.\n- Empty matches are not errors; they return the no-files text result.\n\n## Notes\n- Reach for `find` for filename / path discovery. Reach for `search` when the selection criterion is file contents or regex matches; `search` takes a `pattern` and returns anchored content matches, while `find` only returns matching paths (`packages/coding-agent/src/prompts/tools/find.md`, `packages/coding-agent/src/prompts/tools/search.md`).\n- Bare top-level globs are made recursive. `*.ts` is parsed as base `.` plus glob `**/*.ts`; `src/*.ts` stays rooted at `src` with a non-recursive `*.ts` segment; `src/**/*.ts` preserves explicit recursion.\n- `.gitignore` is always enabled in the built-in local branch (`gitignore: true`). There is no model-facing flag to disable it.\n- `hidden` defaults to `true`; hidden-file exclusion is opt-out, not opt-in.\n- Multi-path missing-input tolerance only applies in the built-in local branch. The custom-operations branch hard-fails the first missing `searchPath` it checks.\n- The custom `FindOperations.glob()` hook receives `ignore` and `limit`, but not the `hidden` flag or an explicit `.gitignore` toggle. A remote delegate must account for that itself if it wants parity with the local branch.\n- Built-in local globbing asks the native layer for `fileType: File`, so recursive directory searches yield files, not directories. Directory outputs are only possible through exact-path passthrough or custom delegates that return them.\n",
|
|
107
108
|
"tools/github.md": "# github\n\n> Dispatch GitHub CLI operations for repositories, issues, pull requests, search, and Actions run watching.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/gh.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/github.md`\n- Key collaborators:\n - `packages/coding-agent/src/tools/gh-format.ts` — shorten commit SHAs for summaries.\n - `packages/coding-agent/src/tools/gh-renderer.ts` — TUI rendering, especially `run_watch` live/result views.\n - `packages/coding-agent/src/utils/git.ts` — `gh`/`git` process wrappers, repo locking, branch config writes.\n - `packages/utils/src/dirs.ts` — base directory for dedicated PR worktrees.\n - `packages/coding-agent/src/sdk.ts` — session artifact allocation hook.\n - `packages/coding-agent/src/session/artifacts.ts` — artifact filename format `<id>.<toolType>.log`.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"repo_view\" \\| \"pr_create\" \\| \"pr_checkout\" \\| \"pr_push\" \\| \"search_issues\" \\| \"search_prs\" \\| \"search_code\" \\| \"search_commits\" \\| \"search_repos\" \\| \"run_watch\"` | Yes | Dispatch selector. `GithubTool.execute()` switches only on this field. |\n| `repo` | `string` | No | `owner/repo` override. Ignored when the identifier argument is already a full GitHub URL. For `search_issues`/`search_prs`/`search_code`/`search_commits`, defaults to the current checkout's `owner/repo` when omitted (skipped when the query already contains a `repo:`/`org:`/`user:`/`owner:` qualifier or when current-repo resolution fails). Required in practice when `gh` cannot infer repo context from the current checkout. |\n| `branch` | `string` | No | Used by `repo_view`, `pr_push`, and `run_watch`. `run_watch` falls back to current git branch when `run` is omitted; `pr_push` falls back to current branch. |\n| `pr` | `string \\| string[]` | No | Used by `pr_checkout`. Each item may be a PR number, branch name, or GitHub PR URL. Array form enables batching. Omitted means current branch PR. |\n| `force` | `boolean` | No | Used only by `pr_checkout`. Defaults to `false`; allows resetting an existing `pr-<number>` local branch to the PR head commit. |\n| `forceWithLease` | `boolean` | No | Used only by `pr_push`; passed through to git push. |\n| `title` | `string` | No | Used only by `pr_create`. Required unless `fill` is `true`. |\n| `body` | `string` | No | Used only by `pr_create`. Mutually exclusive with `fill`. Empty/omitted body becomes `--body \"\"` to suppress the interactive editor. Non-empty body is written to a temp file and passed as `--body-file`. |\n| `base` | `string` | No | Used only by `pr_create`; passed as `--base`. |\n| `head` | `string` | No | Used only by `pr_create`; passed as `--head`. |\n| `draft` | `boolean` | No | Used only by `pr_create`. Defaults to `false`. |\n| `fill` | `boolean` | No | Used only by `pr_create`. Defaults to `false`. Mutually exclusive with `title` and `body`. |\n| `reviewer` | `string[]` | No | Used only by `pr_create`; each entry becomes `--reviewer`. |\n| `assignee` | `string[]` | No | Used only by `pr_create`; each entry becomes `--assignee`. |\n| `label` | `string[]` | No | Used only by `pr_create`; each entry becomes `--label`. |\n| `query` | `string` | No | Used by all `search_*` ops. Required there. |\n| `limit` | `number` | No | Used by all `search_*` ops. Defaults to `10`, floored, clamped to `50`, and must be `> 0`. |\n| `run` | `string` | No | Used only by `run_watch`. Must be a numeric run ID or full GitHub Actions run URL. |\n| `tail` | `number` | No | Used only by `run_watch`. Defaults to `15`, floored, clamped to `200`, and must be `> 0`. |\n\n## Outputs\nThe tool returns a single text result built by `buildTextResult()` in `packages/coding-agent/src/tools/gh.ts`.\n\n- `content`: one text block. Multi-item ops join sections with blank lines and `---` separators.\n- `sourceUrl`: set for single repo/PR/run results when a canonical URL is known.\n- `details`: optional structured metadata used by the TUI renderer.\n - Common fields: `artifactId`, `repo`, `branch`, `worktreePath`, `remote`, `remoteBranch`, `headSha`, `runId`, `runIds`, `status`, `conclusion`, `failedJobs`.\n - `pr_checkout` adds `checkouts: GhPrCheckoutSummary[]`.\n - `run_watch` adds `watch: GhRunWatchViewDetails`, which drives the custom live/result renderer in `packages/coding-agent/src/tools/gh-renderer.ts`.\n- Artifact trailer: when `artifactId` is present, the text body gets an appended line like `Full failed-job logs: artifact://<id>`.\n - `run_watch` allocates artifacts with `session.allocateOutputArtifact(\"github\")`; persistent sessions therefore save failed-log bodies as `<artifact-dir>/<id>.github.log`.\n\n`run_watch` is the only streaming op. It emits `onUpdate` snapshots while polling, then returns one final text result.\n\n## Flow\n1. `GithubTool.createIf()` exposes the tool only when `git.github.available()` finds `gh` on `PATH`.\n2. `GithubTool.execute()` wraps dispatch in `untilAborted()` and switches on `params.op`.\n3. Each op normalizes optional strings, arrays, booleans, and numeric caps locally in `packages/coding-agent/src/tools/gh.ts`.\n4. CLI execution goes through `git.github.run/json/text()` in `packages/coding-agent/src/utils/git.ts`:\n - spawns `gh ...` with `Bun.spawn()`;\n - trims stdout/stderr unless `trimOutput: false`;\n - maps common auth/repo-context failures into tool-facing `ToolError` messages;\n - `json()` rejects empty or invalid JSON.\n5. Read-style ops (`repo_view`, `search_*`) fetch JSON and format Markdown-like text summaries. Single-issue and single-PR views were moved out of the tool and now resolve through the `issue://` / `pr://` internal URL schemes, which share the same SQLite cache.\n7. PR diffs moved out of the tool. `pr://<N>/diff` lists changed files, `pr://<N>/diff/<i>` slices a single file, and `pr://<N>/diff/all` returns the full unified diff — see `docs/tools/read.md`. All three variants share one `gh pr diff` invocation through the `pr-diff` cache row.\n8. `pr_checkout` resolves PR metadata first, then enters `git.withRepoLock()` before any git mutation so parallel checkout calls for the same primary repo do not race on shared `.git` state.\n9. `pr_push` reads PR head metadata back from git branch config, derives a refspec, then pushes with `git.push()`.\n10. `pr_create` shells out once, then best-effort re-reads the created PR for a richer summary.\n11. `run_watch` chooses either run mode (`run` supplied) or commit mode (`run` omitted), polls GitHub Actions APIs every 3 seconds, emits streaming updates, and may save a full failed-log artifact before returning.\n12. Final text goes through `toolResult().text(...)`; if `session.allocateOutputArtifact()` returns a slot, failed-log text is persisted with `Bun.write()`.\n\n## Modes / Variants\n\n### `repo_view`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `repo`, `branch` |\n| `gh` command | `gh repo view [<repo>] [--branch <branch>] --json <GH_REPO_FIELDS>` |\n| Batching | None |\n| Output | `# <owner/repo>` header, description, URL, default branch, requested branch, visibility, permission, primary language, stars, forks, archive/fork flags, updated timestamp, homepage, topics. `sourceUrl = data.url`. |\n\nIf `repo` is omitted, `gh` repository resolution is used.\n\nSingle-issue and single-PR reads live in the `issue://<N>` / `pr://<N>` URL schemes (see `docs/tools/read.md`). They share `~/.skc/cache/github-cache.db` (override via `SKC_GITHUB_CACHE_DB`) and the `github.cache.softTtlSec` / `github.cache.hardTtlSec` / `github.cache.enabled` settings. The cache retains rendered Markdown plus the raw JSON payload returned by `gh`, including private bodies, comments, reviews, and review comments when comments are enabled; rows are scoped by the local GitHub credential fingerprint. Root and repo-scoped reads (`issue://`, `pr://owner/repo`) issue a live `gh issue list` / `gh pr list` for browsing; query params `state`, `limit`, `author`, `label` pass through to `gh` (`issue://` accepts `state=open|closed|all`; `pr://` also accepts `merged`). PR diffs ride the same cache under `pr://<N>/diff[/…]`: the listing, full diff, and per-file slices all share one `pr-diff` row keyed by repo and PR number.\n\n### `pr_create`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` plus either `fill=true` or `title` |\n| Optional fields | `repo`, `title`, `body`, `base`, `head`, `draft`, `fill`, `reviewer[]`, `assignee[]`, `label[]` |\n| `gh` command | `gh pr create ...` with flags assembled from provided fields |\n| Batching | None |\n| Output | `# Created Pull Request ...` summary with URL, state, draft flag, base/head, author, created time, labels, optional body. `sourceUrl` is the created PR URL. |\n\nBranches:\n- `fill && (title || body !== undefined)` throws.\n- Non-empty `body` is written under a temp dir `gh-pr-body-*` in `os.tmpdir()`, passed as `--body-file`, then removed in `finally`.\n- After creation, the tool parses the returned URL and best-effort runs `gh pr view <number> --repo <repo> --json <GH_PR_FIELDS_NO_COMMENTS>`; failures there are swallowed.\n\n### `pr_checkout`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `repo`, `pr`, `force` |\n| `gh` command | For each requested PR: `gh pr view [<pr>] [--repo <repo>] --json <GH_PR_CHECKOUT_FIELDS>`; cross-repo PRs may also call `gh repo view <headRepository> --json <GH_REPO_CLONE_FIELDS>`. |\n| Batching | Yes. `pr` may be `string[]`; each PR is resolved in parallel, but git mutations are serialized per primary repo by `git.withRepoLock()`. |\n| Output | Single PR: checkout/worktree summary plus `details.repo`, `details.branch`, `details.worktreePath`, `details.remote`, `details.remoteBranch`, `details.checkouts`. Batched: `# <n> Pull Request Worktrees (...)` plus one section per PR and aggregated `details.checkouts`. |\n\nWorktree and metadata behavior:\n- Local branch name is always `pr-<number>`.\n- Worktree path is `path.join(getWorktreesDir(), encodeRepoPathForFilesystem(primaryRepoRoot), localBranch)`, where `getWorktreesDir()` is `~/.skc/wt`; effective path is `~/.skc/wt/<encoded-primary-repo-root>/pr-<number>`.\n- Existing worktree detection is by branch ref `refs/heads/pr-<number>` from `git.worktree.list()`.\n- New worktree creation calls `git.worktree.add(repoRoot, finalWorktreePath, localBranch, { signal })` after verifying the path is neither already registered nor already present on disk.\n- For same-repo PRs, remote is `origin`. For cross-repo PRs, the tool resolves a clone URL for the head repo, reuses an existing remote with the same URL when possible, or creates `fork-<owner>` / `fork-<owner>-<n>`.\n- The branch push metadata is persisted with `git config` under the repository's shared `.git/config` as:\n - `branch.pr-<number>.remote`\n - `branch.pr-<number>.merge`\n - `branch.pr-<number>.pushRemote`\n - `branch.pr-<number>.skcPrHeadRef`\n - `branch.pr-<number>.skcPrUrl`\n - `branch.pr-<number>.skcPrIsCrossRepository`\n - `branch.pr-<number>.skcPrMaintainerCanModify`\n- If `refs/heads/pr-<number>` already exists at a different commit, checkout fails unless `force=true`, in which case `git branch --force` resets it to the fetched PR head.\n- If a matching worktree already exists, the tool reuses it and reports `reused: true`.\n\n### `pr_push`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `branch`, `forceWithLease` |\n| `gh` command | None. This path uses git, not `gh`. |\n| Batching | None |\n| Output | `# Pushed Pull Request Branch` summary with local branch, remote, remote branch, remote URL, PR URL, and force-with-lease flag. `sourceUrl = prUrl` when known. |\n\nPush target resolution reads the `branch.<name>.skcPrHeadRef`, `pushRemote`/`remote`, `skcPrUrl`, `skcPrMaintainerCanModify`, and `skcPrIsCrossRepository` git-config keys written by `pr_checkout`. If the current checked-out branch matches the target branch, the source ref is `HEAD`; otherwise it pushes `refs/heads/<branch>`. The refspec is `HEAD:refs/heads/<headRef>` or `refs/heads/<branch>:refs/heads/<headRef>`.\n\n### `search_issues`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/issues -f q=\"<query> [repo:<repo>] is:issue\" -F per_page=<limit>` |\n| Batching | None |\n| Output | `# GitHub issues search`, echoed query, optional repo, result count, then one bullet per issue with repo/state/author/labels/timestamps/URL. |\n\n`repo` defaults to the current checkout's `owner/repo` via `resolveSearchRepoScope()` when omitted. The default is suppressed when the query already contains a leading `repo:`/`org:`/`user:`/`owner:` qualifier or when `gh repo view` fails to resolve the current checkout (e.g. outside a github remote).\n\n### `search_prs`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/issues -f q=\"<query> [repo:<repo>] is:pr\" -F per_page=<limit>` |\n| Batching | None |\n| Output | Same shape as `search_issues`, labeled as pull requests. |\n\n`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.\n\n### `search_code`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/code -f q=\"<query> [repo:<repo>]\" -F per_page=<limit> -H \"Accept: application/vnd.github.text-match+json\"` |\n| Batching | None |\n| Output | `# GitHub code search`, result count, then one bullet per match with path, repo, short commit SHA, URL, and first normalized text-match fragment line when present. |\n\n`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.\n\n### `search_commits`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `repo`, `limit` |\n| `gh` command | `gh api -X GET /search/commits -f q=\"<query> [repo:<repo>]\" -F per_page=<limit>` |\n| Batching | None |\n| Output | `# GitHub commits search`, result count, then one bullet per commit: short SHA + first commit-message line, repo, author, date, URL. |\n\n`repo` defaults to the current checkout's `owner/repo` as in `search_issues`.\n\n### `search_repos`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op`, `query` |\n| Optional fields | `limit` |\n| `gh` command | `gh api -X GET /search/repositories -f q=\"<query>\" -F per_page=<limit>` |\n| Batching | None |\n| Output | `# GitHub repositories search`, result count, then one bullet per repo with first description line, language, stars, forks, open issues, visibility, archive/fork flags, updated time, URL. |\n\n`repo` is intentionally not used for this op.\n\n### `run_watch`\n\n| Aspect | Value |\n| --- | --- |\n| Required fields | `op` |\n| Optional fields | `repo`, `branch`, `run`, `tail` |\n| `gh` command | Repo resolution: `gh repo view --json nameWithOwner -q .nameWithOwner` when `repo` and run URL repo are both absent. Single-run mode uses `gh api --method GET /repos/<repo>/actions/runs/<runId>` and `gh api --method GET /repos/<repo>/actions/runs/<runId>/jobs`. Commit mode uses `gh api --method GET /repos/<repo>/branches/<branch>`, `gh api --method GET /repos/<repo>/actions/runs`, `gh api --method GET /repos/<repo>/actions/runs/<runId>/jobs`, and `gh api /repos/<repo>/actions/jobs/<jobId>/logs` for failed jobs. |\n| Batching | Implicit batching only in commit mode: all workflow runs for one commit are tracked together. |\n| Output | Streaming watch snapshots via `onUpdate`, then a final text report. On failure, appends `Full failed-job logs: artifact://<id>` and sets `details.artifactId`. |\n\nWatch flow:\n- `run` parsing accepts either a decimal run ID or a full run URL. URL repo must match explicit `repo` when both are given.\n- Poll interval is fixed at 3 seconds (`RUN_WATCH_INTERVAL_DEFAULT`).\n- Failure grace period is fixed at 5 seconds (`RUN_WATCH_GRACE_DEFAULT`). When any failed job appears before completion, the tool emits a note, waits once, re-fetches state, then collects logs so concurrent failures are included.\n- Failed-job logs are fetched with `gh api /repos/<repo>/actions/jobs/<jobId>/logs` via `git.github.run()`, not `json()`. Non-zero exit leaves `available: false` instead of failing the whole watch.\n- Inline result includes only the last `tail` lines per failed job. The saved artifact contains full logs (`mode: \"full\"`).\n- In commit mode, success is intentionally double-checked: once all known runs are successful, the tool waits one more poll interval and succeeds only if the set of run IDs is unchanged. This avoids returning before late workflow runs appear for the same commit.\n- `details.watch` drives a specialized renderer in `packages/coding-agent/src/tools/gh-renderer.ts`; non-watch results fall back to generic text rendering.\n\n## Side Effects\n- Filesystem\n - `pr_create` may create a temp dir under `os.tmpdir()` named `gh-pr-body-*`, write `body.md`, then remove the dir in `finally`.\n - `pr_checkout` may create directories under `~/.skc/wt/<encoded-primary-repo-root>/` and add git worktrees there.\n - `run_watch` may write a session artifact with full failed-job logs.\n- Network\n - Every op shells out to `gh`, which then talks to GitHub APIs except `pr_push`.\n - `pr_push` uses git network transport to the configured remote.\n- Subprocesses / native bindings\n - All `gh` calls use `Bun.spawn([\"gh\", ...args])`.\n - `pr_checkout` and `pr_push` also invoke git helpers from `packages/coding-agent/src/utils/git.ts`.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - `run_watch` consumes `session.allocateOutputArtifact()` when failed-job logs are persisted.\n - Returned `details` objects carry run/checkouts metadata for the renderer/UI.\n- User-visible prompts / interactive UI\n - `gh` interactive editor fallback is suppressed for `pr_create` by forcing either `--body-file` or `--body \"\"`.\n - `gh-renderer` provides compact headers for all ops and a custom live watch view for `run_watch`.\n- Background work / cancellation\n - `run_watch` loops until success/failure and uses `scheduler.wait()` between polls.\n - `GithubTool.execute()` is wrapped in `untilAborted()`; `git.github.run()` forwards the abort signal into `Bun.spawn()`.\n\n## Limits & Caps\n- Search result default: `10` (`SEARCH_LIMIT_DEFAULT` in `packages/coding-agent/src/tools/gh.ts`).\n- Search result max: `50` (`SEARCH_LIMIT_MAX`).\n- PR file preview inside the `pr://` view: first `50` files only (`FILE_PREVIEW_LIMIT` in `gh.ts`).\n- Run-watch poll interval: `3s` (`RUN_WATCH_INTERVAL_DEFAULT`).\n- Run-watch failure grace period: `5s` (`RUN_WATCH_GRACE_DEFAULT`).\n- Run-watch failed-log tail default: `15` lines (`RUN_WATCH_TAIL_DEFAULT`).\n- Run-watch failed-log tail max: `200` lines (`RUN_WATCH_TAIL_MAX`).\n- PR review comments page size: `100` (`REVIEW_COMMENTS_PAGE_SIZE`).\n- Actions jobs page size: `100` (`RUN_JOBS_PAGE_SIZE`).\n- Search and tail numeric inputs are floored with `Math.floor()`, clamped to the max, and rejected when non-finite or `<= 0`.\n- `pr_checkout` batch fan-out is unbounded in tool code; all requested PRs are launched with `Promise.all()`.\n\n## Errors\n- Tool creation is skipped entirely when `gh` is not installed.\n- `git.github.run()` throws `ToolError(\"GitHub CLI (gh) is not installed...\")` if `gh` is missing at execution time.\n- `git.github.text/json()` map common failures to model-facing messages:\n - not authenticated → `GitHub CLI is not authenticated. Run \\`gh auth login\\`.`\n - missing repo context without explicit `repo` → `GitHub repository context is unavailable. Pass \\`repo\\` explicitly or run the tool inside a GitHub checkout.`\n - otherwise stderr/stdout text, or fallback `GitHub CLI command failed: gh ...`\n- `json()` also throws on empty stdout or invalid JSON.\n- Local validation errors throw `ToolError`, including:\n - missing required per-op fields (`query`, `title unless fill=true`)\n - invalid numeric `limit` / `tail`\n - invalid `run` format\n - `fill` combined with `title` or `body`\n - missing git repo / branch / HEAD context for checkout, push, or watch\n - `pr_push` on a branch without `skcPrHeadRef` metadata\n - conflicting existing worktree path or branch without `force`\n- `run_watch` treats failed-job log fetches specially: missing log content does not fail the watch; it marks that log `available: false` and prints `Log tail unavailable.` / `Full log unavailable.`.\n- `pr_create` swallows only the post-create best-effort `gh pr view` refresh; the create step itself still fails normally.\n\n## Notes\n- `appendRepoFlag()` intentionally skips `--repo` when the identifier argument is already a full GitHub URL; that lets `gh` derive repo/number from the URL.\n- `normalizePrIdentifierList()` accepts `reviewer`, `assignee`, and `label` arrays too; the helper name is broader than its callers.\n- `pr_push` depends on `pr_checkout` having run first for that local branch; there is no alternate metadata source.\n- `pr_checkout` stores push metadata in branch config, not in the worktree directory. Reusing the same `pr-<number>` branch reuses those config keys.\n- Worktree write serialization is keyed by the primary repo root, not the current worktree path, because git worktrees share `.git/config`, `packed-refs`, commit-graph, and worktree metadata files.\n- `search_repos` is the only search op that never forwards `repo`; repository scoping must be expressed in the query itself.\n- `run_watch` success on commit mode means “all observed runs succeeded and no additional runs appeared one poll later”, not merely “latest poll looked green”.\n- The TUI renderer collapses failed log previews unless the result view is expanded; the underlying text result still contains the same tailed lines plus any artifact reference.\n",
|
|
108
|
-
"tools/irc.md": "# irc\n\n> Send short prose messages to other live agents in the current process.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/irc.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/irc.md`\n- Key collaborators:\n - `packages/coding-agent/src/registry/agent-registry.ts` — process-global live agent directory.\n - `packages/coding-agent/src/session/agent-session.ts` — side-channel reply generation and history injection.\n - `packages/coding-agent/src/prompts/system/irc-incoming.md` — no-tools auto-reply prompt.\n - `packages/coding-agent/src/tools/index.ts` — tool availability gating.\n - `packages/coding-agent/src/config/settings-schema.ts` — `irc.enabled` default.\n - `packages/coding-agent/src/modes/controllers/event-controller.ts` — renders IRC events into chat UI.\n - `packages/coding-agent/src/modes/utils/ui-helpers.ts` — formats `[IRC]` transcript lines.\n - `packages/coding-agent/src/task/executor.ts` — carries `irc.enabled` into subagents.\n\n## Inputs\n\n### `op: \"list\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"list\"` | Yes | Lists peers visible to the caller. |\n\n### `op: \"send\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"send\"` | Yes | Sends one message to one peer or to `\"all\"`. |\n| `to` | `string` | Yes | Peer id such as `0-Main`, or `\"all\"` for broadcast. Whitespace is trimmed. |\n| `message` | `string` | Yes | Message body. Whitespace is trimmed; empty-after-trim is rejected. |\n| `awaitReply` | `boolean` | No | Wait for prose replies. Defaults to `true` for direct messages and `false` for `to: \"all\"`. |\n\n## Outputs\n- Single-shot `AgentToolResult`; no streaming updates.\n- `content` is one text block.\n - `list` returns either `No other live agents.` or a bullet list headed by `<n> peer(s):`.\n - `send` returns delivery summary text, then optional `## Replies`, `## Failed`, and `Unknown / unavailable peers:` sections.\n- `details` is structured metadata:\n - `list`: `{ op, from, peers, channels }`\n - `send`: `{ op, from, to, delivered, replies?, failed?, notFound? }`\n- The tool does not return raw IRC frames, message ids, or a transcript object.\n\n## Flow\n1. `IrcTool.createIf` only constructs the tool when `irc.enabled` is on and the session has both an `AgentRegistry` and `getAgentId` (`packages/coding-agent/src/tools/irc.ts`).\n2. Tool discovery adds another gate in `packages/coding-agent/src/tools/index.ts`: if the caller is `0-Main` and `async.enabled` is off, `irc` is hidden because the main agent cannot talk to concurrent peers in sync mode.\n3. `execute` resolves the process-global registry and sender id. Missing either returns a text error result instead of throwing.\n4. `op: \"list\"` calls `registry.listVisibleTo(senderId)`, which exposes every other agent in flat namespace whose status is `running` or `idle` (`packages/coding-agent/src/registry/agent-registry.ts`).\n5. `list` formats human-readable lines and returns `channels` as `['all', ...peerIds]`. These are logical targets only; there is no channel join state.\n6. `op: \"send\"` trims `to` and `message`; missing values produce text errors.\n7. `send` resolves targets:\n - `to === \"all\"`: all visible peers.\n - otherwise: one exact registry id, excluding self and excluding peers not in `running`/`idle`.\n8. `send` chooses `awaitReply = params.awaitReply ?? !isBroadcast`.\n9. Each target is dispatched in parallel via `target.session.respondAsBackground(...)`. One slow or failing peer does not block dispatch to the others.\n10. `respondAsBackground` emits an `irc_message` session event, forwards a display-only relay to the main session UI, and either:\n - queues just the incoming message for later history injection when `awaitReply === false`, or\n - renders `packages/coding-agent/src/prompts/system/irc-incoming.md`, runs `runEphemeralTurn` with `toolChoice: \"none\"`, emits an auto-reply event, then queues both incoming and reply messages for history injection.\n11. Deferred injection waits until the recipient is no longer streaming; `#flushPendingBackgroundExchanges` appends the custom messages through normal `message_start`/`message_end` external events so persistence and listeners see them.\n12. `send` aggregates `delivered`, `replies`, `failed`, and `notFound`, then returns one text summary plus matching `details`.\n\n## Modes / Variants\n- `list`: enumerate visible peers and logical channels.\n- `send` direct message: one exact peer id, default synchronous auto-reply.\n- `send` broadcast: `to: \"all\"`, default fire-and-forget (`awaitReply: false`) to every visible peer.\n- `send` with `awaitReply: false`: recipient records the incoming message but does not generate a reply.\n- `send` with `awaitReply: true`: recipient performs a no-tools ephemeral LLM turn and returns prose.\n\n## Side Effects\n- Session state\n - Reads from the process-global `AgentRegistry`.\n - Emits `irc_message` session events on recipient sessions.\n - Queues IRC custom messages into recipient persisted history after the current stream finishes.\n - For non-main recipients, forwards display-only relay observations into the main session UI; these relays are not persisted to the main agent history.\n - Subagents inherit `irc.enabled` from task executor settings.\n- User-visible prompts / interactive UI\n - IRC events render as `[IRC]` transcript lines in the TUI.\n - Auto-replies are generated from `packages/coding-agent/src/prompts/system/irc-incoming.md` and explicitly forbid tool use.\n- Background work / cancellation\n - `send` starts one background `respondAsBackground` call per target.\n - The caller's `AbortSignal` is forwarded into each background reply turn.\n- Network\n - No IRC server connection.\n - When `awaitReply: true`, the recipient may make model-provider API calls through `runEphemeralTurn`.\n- Filesystem\n - No direct filesystem writes in the tool itself.\n\n## Limits & Caps\n- Availability gates:\n - `irc.enabled` defaults to `true` in `packages/coding-agent/src/config/settings-schema.ts`.\n - Main agent tool discovery suppresses `irc` when `async.enabled` is off (`packages/coding-agent/src/tools/index.ts`).\n- Visibility scope: only peers in status `running` or `idle` are addressable via `listVisibleTo`.\n- Reply execution:\n - No tools are available in auto-reply turns (`toolChoice: \"none\"` in `runEphemeralTurn`).\n - No internal timeout, retry, backoff, rate limit, or reply length cap is defined in `irc.ts`; behavior relies on the underlying model stream and any upstream API limits.\n- Flush scheduling: deferred history injection polls every `50` ms while the recipient is still streaming (`#scheduleBackgroundExchangeFlush` in `packages/coding-agent/src/session/agent-session.ts`).\n\n## Errors\n- The tool returns text errors, not thrown exceptions, for:\n - missing registry: `IRC is unavailable in this session.`\n - missing sender id: `IRC is unavailable: caller has no agent id.`\n - missing `to`: `` `to` is required for op=\"send\". ``\n - missing `message`: `` `message` is required for op=\"send\". ``\n - unknown op: `Unknown irc op.`\n- Unknown, self-addressed, non-running, and non-idle direct targets are reported under `details.notFound` and in the text footer `Unknown / unavailable peers:`.\n- If a target has no attached session, it is treated as not found.\n- Exceptions thrown by `respondAsBackground` or `runEphemeralTurn` are caught per-target and surfaced under `details.failed` as `{ id, error }`; other recipients still complete.\n- If no target succeeds, `send` still returns normally with `No recipients received the message.` and optional `failed`/`notFound` metadata.\n\n## Notes\n- This is IRC-like naming only. There are no servers, sockets, nick registration, auth handshakes, channels beyond `all`, or commands such as join/part/topic.\n- Addressing is by exact agent id from the registry; there is no fuzzy lookup or aliasing.\n- `channels` in `list` is synthetic output: `all` plus visible peer ids. Nothing is persisted across calls as channel membership.\n- Persistence is per recipient history, not per sender history. The sender gets the tool result; the recipient later sees injected custom messages on its next turn.\n- The main UI may show IRC relays for conversations it was not part of, but those relay records are explicitly display-only.\n- Because reply generation snapshots in-flight assistant text, a recipient can answer based on partially streamed context.\n- Direct self-messaging is rejected by resolving the target as unavailable.",
|
|
109
|
+
"tools/irc.md": "# irc\n\n> Send short prose messages to other live agents in the current process.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/irc.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/irc.md`\n- Key collaborators:\n - `packages/coding-agent/src/registry/agent-registry.ts` — process-global live agent directory.\n - `packages/coding-agent/src/session/agent-session.ts` — side-channel reply generation and history injection.\n - `packages/coding-agent/src/prompts/system/irc-incoming.md` — no-tools auto-reply prompt.\n - `packages/coding-agent/src/tools/index.ts` — tool availability gating.\n - `packages/coding-agent/src/config/settings-schema.ts` — `irc.enabled` default.\n - `packages/coding-agent/src/modes/controllers/event-controller.ts` — renders IRC events into chat UI.\n - `packages/coding-agent/src/modes/utils/ui-helpers.ts` — formats `[IRC]` transcript lines.\n - `packages/coding-agent/src/task/executor.ts` — carries `irc.enabled` into subagents.\n\n## Inputs\n\n### `op: \"list\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"list\"` | Yes | Lists peers visible to the caller. |\n\n### `op: \"send\"`\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `op` | `\"send\"` | Yes | Sends one message to one peer or to `\"all\"`. |\n| `to` | `string` | Yes | Peer id such as `0-Main`, or `\"all\"` for broadcast. Whitespace is trimmed. |\n| `message` | `string` | Yes | Message body. Whitespace is trimmed; empty-after-trim is rejected. |\n| `awaitReply` | `boolean` | No | Wait for prose replies. Defaults to `true` for direct messages and `false` for `to: \"all\"`. |\n\n## Outputs\n- Single-shot `AgentToolResult`; no streaming updates.\n- `content` is one text block.\n - `list` returns either `No other live agents.` or a bullet list headed by `<n> peer(s):`.\n - `send` returns delivery summary text, then optional `## Replies`, `## Failed`, and `Unknown / unavailable peers:` sections.\n- `details` is structured metadata:\n - `list`: `{ op, from, peers, channels }`\n - `send`: `{ op, from, to, delivered, replies?, failed?, notFound? }`\n- The tool does not return raw IRC frames, message ids, or a transcript object.\n\n## Flow\n1. `IrcTool.createIf` only constructs the tool when `irc.enabled` is on and the session has both an `AgentRegistry` and `getAgentId` (`packages/coding-agent/src/tools/irc.ts`).\n2. Tool discovery adds another gate in `packages/coding-agent/src/tools/index.ts`: if the caller is `0-Main` and `async.enabled` is off, `irc` is hidden because the main agent cannot talk to concurrent peers in sync mode.\n3. `execute` resolves the process-global registry and sender id. Missing either returns a text error result instead of throwing.\n4. `op: \"list\"` calls `registry.listVisibleTo(senderId)`, which exposes every other agent in flat namespace whose status is `running` or `idle` (`packages/coding-agent/src/registry/agent-registry.ts`).\n5. `list` formats human-readable lines and returns `channels` as `['all', ...peerIds]`. These are logical targets only; there is no channel join state.\n6. `op: \"send\"` trims `to` and `message`; missing values produce text errors.\n7. `send` resolves targets:\n - `to === \"all\"`: all visible peers.\n - otherwise: one exact registry id, excluding self and excluding peers not in `running`/`idle`.\n8. `send` chooses `awaitReply = params.awaitReply ?? !isBroadcast`.\n9. Each target is dispatched in parallel via `target.session.respondAsBackground(...)`. One slow or failing peer does not block dispatch to the others.\n10. `respondAsBackground` accepts each delivery into the recipient's volatile current-session exchange queue before observing it in the recipient or main UI, and before reporting sender delivery success:\n - `awaitReply === false`: accepts/queues the incoming message, then emits its `irc_message` event and forwards the display-only relay to the main session UI.\n - `awaitReply === true`: renders `packages/coding-agent/src/prompts/system/irc-incoming.md` and runs `runEphemeralTurn` with `toolChoice: \"none\"`. After a reply succeeds, it constructs and accepts/queues the ordered incoming + auto-reply pair, commits its IRC roster claim, then emits both `irc_message` events and forwards both display-only relays. A failed or aborted reply turn accepts and surfaces nothing.\n11. Deferred injection waits until the recipient is no longer streaming; `#flushPendingBackgroundExchanges` appends accepted custom messages through normal `message_start`/`message_end` external events so persistence and listeners see them.\n12. `send` aggregates `delivered`, `replies`, `failed`, and `notFound`, then returns one text summary plus matching `details`.\n\n## Modes / Variants\n- `list`: enumerate visible peers and logical channels.\n- `send` direct message: one exact peer id, default synchronous auto-reply.\n- `send` broadcast: `to: \"all\"`, default fire-and-forget (`awaitReply: false`) to every visible peer.\n- `send` with `awaitReply: false`: recipient records the incoming message but does not generate a reply.\n- `send` with `awaitReply: true`: recipient performs a no-tools ephemeral LLM turn and returns prose.\n\n## Side Effects\n- Session state\n - Reads from the process-global `AgentRegistry`.\n - Accepts each IRC delivery into a process-local, volatile recipient exchange queue before recipient/main observations and sender success. This acceptance is not durable delivery.\n - Emits `irc_message` session events on recipient sessions after acceptance.\n - Flushes accepted IRC custom messages into recipient history after the current stream finishes.\n - For non-main recipients, forwards display-only relay observations into the main session UI after acceptance; these relays are not persisted to the main agent history. Observer failures are isolated from accepted delivery.\n - Subagents inherit `irc.enabled` from task executor settings.\n- User-visible prompts / interactive UI\n - IRC events render as `[IRC]` transcript lines in the TUI.\n - Auto-replies are generated from `packages/coding-agent/src/prompts/system/irc-incoming.md` and explicitly forbid tool use.\n- Background work / cancellation\n - `send` starts one background `respondAsBackground` call per target.\n - The caller's `AbortSignal` is forwarded into each background reply turn.\n- Network\n - No IRC server connection.\n - When `awaitReply: true`, the recipient may make model-provider API calls through `runEphemeralTurn`.\n- Filesystem\n - No direct filesystem writes in the tool itself.\n\n## Limits & Caps\n- Availability gates:\n - `irc.enabled` defaults to `true` in `packages/coding-agent/src/config/settings-schema.ts`.\n - Main agent tool discovery suppresses `irc` when `async.enabled` is off (`packages/coding-agent/src/tools/index.ts`).\n- Visibility scope: only peers in status `running` or `idle` are addressable via `listVisibleTo`.\n- Reply execution:\n - No tools are available in auto-reply turns (`toolChoice: \"none\"` in `runEphemeralTurn`).\n - No internal timeout, retry, backoff, rate limit, or reply length cap is defined in `irc.ts`; behavior relies on the underlying model stream and any upstream API limits.\n- Flush scheduling: deferred history injection polls every `50` ms while the recipient is still streaming (`#scheduleBackgroundExchangeFlush` in `packages/coding-agent/src/session/agent-session.ts`).\n\n## Errors\n- The tool returns text errors, not thrown exceptions, for:\n - missing registry: `IRC is unavailable in this session.`\n - missing sender id: `IRC is unavailable: caller has no agent id.`\n - missing `to`: `` `to` is required for op=\"send\". ``\n - missing `message`: `` `message` is required for op=\"send\". ``\n - unknown op: `Unknown irc op.`\n- Unknown, self-addressed, non-running, and non-idle direct targets are reported under `details.notFound` and in the text footer `Unknown / unavailable peers:`.\n- If a target has no attached session, it is treated as not found.\n- Exceptions from reply generation before awaited-exchange acceptance are caught per-target and surfaced under `details.failed` as `{ id, error }`; other recipients still complete. A provider failure or sender abort therefore emits no `irc_message` observations and accepts no recipient exchange. Recipient/main observer failures after acceptance are isolated and do not turn a delivered exchange into a sender failure.\n- If no target succeeds, `send` still returns normally with `No recipients received the message.` and optional `failed`/`notFound` metadata.\n\n## Notes\n- This is IRC-like naming only. There are no servers, sockets, nick registration, auth handshakes, channels beyond `all`, or commands such as join/part/topic.\n- Addressing is by exact agent id from the registry; there is no fuzzy lookup or aliasing.\n- `channels` in `list` is synthetic output: `all` plus visible peer ids. Nothing is persisted across calls as channel membership.\n- Recipient history, not sender history, receives accepted IRC custom messages when the recipient flushes its current-session queue. Acceptance is process-local and volatile: it does not promise durable storage, fsync, recovery, deduplication, or replay across process loss.\n- The main UI may show IRC relays for conversations it was not part of, but those relay records are explicitly display-only.\n- Because reply generation snapshots in-flight assistant text, a recipient can answer based on partially streamed context.\n- Direct self-messaging is rejected by resolving the target as unavailable.\n\n## Sidebar\n\n- `irc.sidebar.enabled` defaults to `true`: the read-only sidebar is available when `irc.enabled` is also enabled, but starts closed. `app.irc.sidebar.toggle` (default `Alt+I`, remappable) opens or hides it.\n- The sidebar retains the active runtime UI session's IRC observations only. It is not written to disk or restored into another session. Each arrival decides its inline lifetime once: arrivals while the panel is visible expire 10 seconds after observation; arrivals while it is closed persist inline. Later toggles do not change that decision.\n- An eligible first live inline arrival while the sidebar is closed shows a one-time hint using the resolved toggle key (for example, `Alt+I opens sidebar`). Rebuilds do not show or consume this hint.\n- When open, the transcript/sidebar split targets 70:30. The sidebar keeps a 30-column minimum only while the transcript can retain at least half the usable width; below that boundary the sidebar yields completely and the transcript renders full width. Retained sidebar messages are Discord-style blocks: `sender → recipient · HH:mm`, followed by the retained body with a two-column indent and one blank row between messages. Sender and recipient display fields are normalized and bounded to 256 UTF-8 bytes at complete grapheme boundaries.\n- The sidebar retains at most 10,000 observations and 16 MiB of UTF-8 message payload per runtime UI session, evicting the oldest observations first. An individual observation larger than the 16 MiB budget is omitted rather than truncated or admitted by evicting the rest of the backlog. Replay suppression tracks at most 100,000 unique observation identities and then fails closed for unseen arrivals until the runtime UI instance ends, preventing forgotten identities from resurrecting across eviction or fork cleanup while keeping memory bounded. Both inline transcript and sidebar body rendering independently materialize at most 2,048 rows from a bounded 64 KiB UTF-8 source projection, preferring recent retained content and showing explicit message/backlog elision markers when necessary. The bounded, runtime-only, read-only backlog does not affect welcome-screen row reservation, which counts transcript rows only.\n- While the sidebar is visible, Kitty terminals keep rendering real images in the transcript (Kitty placements are cursor-neutral and compose safely with the split). Cursor-advancing protocols — iTerm2 inline images and raw SIXEL sequences — are represented by compact text placeholders so they cannot corrupt the split. Hiding the sidebar restores normal rendering for every protocol.\n- A successful `/fork` starts a new logical UI session: it clears the sidebar ledger, hides the panel, and resets roster-delivery state. Failed or cancelled forks preserve the current runtime sidebar state.\n\n## Hidden peer roster reminders\n\nWhen the live peer roster changes, an eligible model turn receives one hidden single-line reminder listing stable agent ids and roster labels. The initial empty roster produces no reminder; a later transition to empty does. Running/idle status changes alone do not count as a roster change.\n\nNormal turns and `/btw` ephemeral turns commit an atomic roster claim on successful completion. Awaited IRC auto-replies defer that commit until their incoming + auto-reply exchange is accepted; failed or aborted reply turns release the claim for a later retry. These reminders are context-only and never appear in the transcript or persisted history.",
|
|
109
110
|
"tools/job.md": "# job\n\n> Wait for or cancel background jobs managed by the session async runtime.\n\n## Source\n- Entry: `packages/coding-agent/src/tools/job.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/job.md`\n- Key collaborators:\n - `packages/coding-agent/src/async/job-manager.ts` — job registry, cancellation, delivery suppression.\n - `packages/coding-agent/src/async/support.ts` — feature gating for background jobs.\n - `packages/coding-agent/src/tools/bash.ts` — explicit async bash and auto-backgrounded bash jobs.\n - `packages/coding-agent/src/task/index.ts` — async task-job scheduling.\n - `packages/coding-agent/src/sdk.ts` — automatic follow-up delivery for unsuppressed completions.\n - `packages/coding-agent/src/config/settings-schema.ts` — `async.pollWaitDuration` options.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `poll` | `string[]` | No | Job ids to watch. Cannot be combined with `list`. If omitted (and `cancel` is also omitted), the tool watches all running jobs. If provided, missing ids are silently filtered out before waiting. |\n| `cancel` | `string[]` | No | Job ids to cancel before any polling. Missing ids are reported as `not_found`; non-running ids as `already_completed`. |\n| `list` | `boolean` | No | Return an immediate snapshot of every job spawned by the calling agent (running + completed within retention) without waiting. Read-only — cannot be combined with `poll` or `cancel`. |\n\n## Outputs\nThe tool returns one text block plus `details`.\n\n- `content[0].text`: markdown-like plain text sections assembled by `#buildResult(...)`:\n - `## Cancelled (N)` for cancel outcomes.\n - `## Completed (N)` for non-running jobs, including stored `resultText` and `errorText`.\n - `## Still Running (N)` for jobs still in `running`.\n- `details.jobs`: array of snapshots:\n - `id: string`\n - `type: \"bash\" | \"task\"`\n - `status: \"running\" | \"completed\" | \"failed\" | \"cancelled\"`\n - `label: string`\n - `durationMs: number`\n - optional `resultText`, `errorText`\n- `details.cancelled` appears only when `cancel` was passed; each item is `{ id, status }` where status is `\"cancelled\" | \"not_found\" | \"already_completed\"`.\n\nStreaming behavior:\n- During a polling wait, `execute(...)` emits `onUpdate(...)` every 500 ms with an empty text block and fresh `details.jobs` snapshots.\n- Final return is single-shot after a completion, timeout, abort, or immediate fast path.\n\nRead-only snapshot path:\n- Calling `job` with `list: true` returns a markdown summary of every job spawned by the calling agent (running + completed within retention) without waiting.\n\n## Flow\n1. `JobTool.createIf(...)` in `packages/coding-agent/src/tools/job.ts` only exposes the tool when `isBackgroundJobSupportEnabled(...)` returns true for either `async.enabled` or `bash.autoBackground.enabled`.\n2. `execute(...)` fetches `session.asyncJobManager`. If absent, it returns `Async execution is disabled; no background jobs are available.`\n3. `cancel` ids are processed first:\n - `manager.getJob(id)` missing → `not_found`.\n - existing job with `status !== \"running\"` → `already_completed`.\n - running job → `manager.cancel(id)`, which sets `job.status = \"cancelled\"`, aborts the controller, and schedules eviction.\n4. Polling mode is chosen with `const shouldPoll = requestedPollIds !== undefined || cancelIds.length === 0`:\n - only `cancel` present → return immediately, no wait.\n - explicit `poll`, or no args at all → proceed to watch jobs.\n5. Watch set resolution:\n - explicit `poll` → map ids through `manager.getJob(...)` and drop missing ones.\n - no `poll` and no `cancel` → `manager.getRunningJobs()`.\n6. Empty watch set returns immediately:\n - if cancellations happened, return snapshots for the cancelled ids that still exist.\n - else return either `No matching jobs found for IDs: ...` or `No running background jobs to wait for.`\n7. If every watched job is already non-running, `#buildResult(...)` returns immediately without waiting.\n8. Otherwise the tool waits on `Promise.race(...)` across:\n - every watched running job's `job.promise`,\n - a timeout promise for `async.pollWaitDuration`,\n - the tool-call abort signal when present.\n9. Before waiting, it calls `manager.watchJobs(watchedJobIds)`. This suppresses automatic completion delivery for those ids while they are being watched.\n10. If `onUpdate` exists, a 500 ms interval sends progress snapshots from `#snapshotJobs(...)`; one snapshot is emitted immediately before entering the race.\n11. In `finally`, the tool always calls `manager.unwatchJobs(...)`, clears the timeout, and stops the progress interval.\n12. `#buildResult(...)` deduplicates jobs, snapshots current manager state, then calls `manager.acknowledgeDeliveries(...)` for every non-running job in the result. That suppresses later automatic follow-up delivery for the same completions and removes queued deliveries for those ids.\n13. The final text groups jobs by non-running vs still-running state. A timeout is not an error path; it simply returns the current snapshot.\n\n## Modes / Variants\n- Poll all running jobs: call with neither `poll` nor `cancel`.\n- Poll explicit ids: call with `poll` only.\n- Cancel only: call with `cancel` only; cancellations happen and the tool returns immediately.\n- Cancel then poll: call with both. Cancellations are applied first, then the tool watches the remaining resolved `poll` ids.\n- Read-only inspection: call with `list: true` for the same snapshot data without waiting on completion.\n\nSpawn paths that produce jobs:\n- `packages/coding-agent/src/tools/bash.ts`\n - `async: true` always registers a `type: \"bash\"` job with `AsyncJobManager.register(...)` and returns a start message.\n - auto-background mode (`bash.autoBackground.enabled`) starts the same managed job path for non-PTY commands, waits up to `min(bash.autoBackground.thresholdMs, timeoutMs - 1000)`, and if the command is still running returns a background-job start result instead of inline command output.\n- `packages/coding-agent/src/task/index.ts`\n - when `async.enabled` is on, the chosen agent is not blocking, and `tasks.length > 0`, each task item is registered as a `type: \"task\"` job.\n\nLifecycle and exact state names:\n- Conceptual scheduling path: `pending` (only task-progress bookkeeping before work starts) → `running` → `completed` / `failed`; cancellation changes a running async job to `cancelled`.\n- Exact `AsyncJob.status` values in `packages/coding-agent/src/async/job-manager.ts`: `\"running\" | \"completed\" | \"failed\" | \"cancelled\"`.\n- Exact per-task progress values in `packages/coding-agent/src/task/types.ts`: `\"pending\" | \"running\" | \"completed\" | \"failed\" | \"aborted\"`.\n\n## Side Effects\n- Filesystem\n - None in `job.ts` itself.\n - Jobs being observed may already have written artifacts/results through their own tool runtimes.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Reads and mutates `session.asyncJobManager` state.\n - `watchJobs(...)` / `unwatchJobs(...)` toggle delivery suppression for the watched ids.\n - `acknowledgeDeliveries(...)` marks completed ids as suppressed and removes queued deliveries for them.\n - `cancel(...)` aborts running jobs through each job's `AbortController`.\n- User-visible prompts / interactive UI\n - Polling emits periodic `onUpdate` snapshots every 500 ms.\n - Automatic job completion follow-ups are generated by `packages/coding-agent/src/sdk.ts` only for unsuppressed deliveries.\n- Background work / cancellation\n - Waiting uses a timeout plus optional tool-call abort signal.\n - Cancelling a job does not synchronously await teardown; it flips state, aborts, and returns control to the manager/job promise.\n\n## Limits & Caps\n- Poll wait duration comes from `async.pollWaitDuration` in `packages/coding-agent/src/config/settings-schema.ts`:\n - allowed values: `5s`, `10s`, `30s`, `1m`, `5m`\n - default: `30s`\n- Progress update cadence while polling: `PROGRESS_INTERVAL_MS = 500` in `packages/coding-agent/src/tools/job.ts`.\n- Async job retention default: `DEFAULT_RETENTION_MS = 5 * 60 * 1000` in `packages/coding-agent/src/async/job-manager.ts`.\n- Manager fallback max-running limit: `DEFAULT_MAX_RUNNING_JOBS = 15` in `packages/coding-agent/src/async/job-manager.ts`.\n- Session wiring clamps `async.maxJobs` to `1..100` before constructing the manager in `packages/coding-agent/src/sdk.ts`; settings default is `100` in `packages/coding-agent/src/config/settings-schema.ts`.\n- Async completion delivery retry backoff in `packages/coding-agent/src/async/job-manager.ts`:\n - base `500` ms\n - max `30_000` ms\n - jitter `< 200` ms\n - exponent capped at 8 doublings\n\n## Errors\n- Tool-disabled path is returned as normal text, not thrown: `Async execution is disabled; no background jobs are available.`\n- Polling a nonexistent id is not an exception:\n - with `poll` only, missing ids are dropped; if none remain the tool returns `No matching jobs found for IDs: ...`.\n - with `cancel`, each missing id is reported as `not_found` in `details.cancelled` and text.\n- Cancelling a non-running job is not an exception; it reports `already_completed` even if the actual status is `completed`, `failed`, or `cancelled`.\n- Tool-call abort during polling stops waiting and returns a final snapshot through `#buildResult(...)`; it does not cancel watched jobs.\n- Failures inside the underlying async work are stored on the job (`status: \"failed\"`, `errorText`) and reported in normal tool output, not rethrown by `job`.\n- Calling `list: true` against an empty manager returns a normal empty-list result rather than throwing; missing ids passed to `poll` are silently filtered.\n\n## Notes\n- `job` waits for the first watched running job to settle, not for all watched jobs. If others remain `running`, they are reported under `## Still Running`; the caller must invoke `job` again to continue waiting.\n- Delivery suppression is the key difference between snapshot and automatic delivery:\n - snapshots (`job` calls with `poll` or `list: true`) read current manager state;\n - follow-up delivery comes from `AsyncJobManager.#enqueueDelivery(...)` and `sdk.ts` `onJobComplete`;\n - watched or acknowledged ids are suppressed via `isDeliverySuppressed(...)`.\n- `manager.cancel(id)` sets `status = \"cancelled\"` before the underlying promise settles. The job function may later populate `resultText` or `errorText`; `job-manager.ts` preserves that text but does not transition the status away from `cancelled`.\n- Retention eviction removes the job record, suppression flags, and watch flag together. After eviction, both `job` calls and `list: true` snapshots behave as if the id never existed.\n",
|
|
110
111
|
"tools/lsp.md": "# lsp\n\n> Query language servers for diagnostics, navigation, symbols, renames, code actions, capabilities, and raw requests.\n\n## Source\n- Entry: `packages/coding-agent/src/lsp/index.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/lsp.md`\n- Key collaborators:\n - `packages/coding-agent/src/lsp/client.ts` — client process lifecycle and JSON-RPC\n - `packages/coding-agent/src/lsp/config.ts` — config loading, auto-detect, server selection\n - `packages/coding-agent/src/lsp/lspmux.ts` — optional `lspmux` command wrapping\n - `packages/coding-agent/src/lsp/edits.ts` — apply `WorkspaceEdit` and text edits\n - `packages/coding-agent/src/lsp/utils.ts` — URI conversion, symbol resolution, formatting, glob expansion\n - `packages/coding-agent/src/lsp/types.ts` — tool schema and protocol types\n - `packages/coding-agent/src/lsp/clients/index.ts` — custom linter client cache/factory\n - `packages/coding-agent/src/lsp/clients/lsp-linter-client.ts` — LSP-backed linter adapter\n - `packages/coding-agent/src/lsp/clients/biome-client.ts` — Biome CLI diagnostics/formatting adapter\n - `packages/coding-agent/src/lsp/clients/swiftlint-client.ts` — SwiftLint CLI diagnostics adapter\n - `packages/coding-agent/src/tools/index.ts` — tool registration and `lsp.enabled` gating\n - `packages/coding-agent/src/tools/tool-timeouts.ts` — timeout defaults and clamping\n - `packages/coding-agent/src/lsp/defaults.json` — built-in server definitions for auto-detect\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `action` | string enum | Yes | One of `diagnostics`, `definition`, `references`, `hover`, `symbols`, `rename`, `rename_file`, `code_actions`, `type_definition`, `implementation`, `status`, `reload`, `capabilities`, `request`. |\n| `file` | string | No | File path; for `diagnostics` also a glob; for workspace forms use `\"*\"`; for `rename_file` this is the source path. |\n| `line` | number | No | 1-indexed line number for position-based actions. Defaults to `1` on the single-file action path. |\n| `symbol` | string | No | Substring used to resolve the column on `line`. Supports `name#N` occurrence selectors; `N` is 1-indexed and defaults to `1`. |\n| `query` | string | No | Workspace symbol query, code-action selector/filter, or LSP method name for `action=request`. |\n| `new_name` | string | No | Required for `rename` and `rename_file`. |\n| `apply` | boolean | No | For `rename`/`rename_file`, apply unless explicitly `false`. For `code_actions`, list unless explicitly `true`. |\n| `timeout` | number | No | Seconds, clamped by `clampTimeout(\"lsp\", ...)` to `5..60`, default `20`. |\n| `payload` | string | No | JSON string for `action=request`; overrides auto-built params. |\n\n## Outputs\n- Single-shot `AgentToolResult`.\n- `content` is always one text block: `[{ type: \"text\", text: string }]`.\n- `details` is `LspToolDetails`: `action`, `success`, optional `serverName`, optional original `request`.\n- No streaming updates.\n- No artifact URIs or background jobs.\n- Many validation failures are returned as ordinary text results with `details.success: false`; aborts throw `ToolAbortError` instead.\n\n## Flow\n1. `packages/coding-agent/src/tools/index.ts` registers `lsp: LspTool.createIf`; session creation also gates it behind `session.enableLsp !== false` and `settings.get(\"lsp.enabled\")`.\n2. `LspTool.execute()` in `packages/coding-agent/src/lsp/index.ts` clamps `timeout` with `clampTimeout(\"lsp\", ...)`, builds an `AbortSignal.timeout(...)`, and combines it with the caller signal.\n3. `getConfig()` loads and caches `LspConfig` per cwd, applies idle-timeout config via `setIdleTimeout()`, and reuses the cached config on later calls.\n4. Config loading in `packages/coding-agent/src/lsp/config.ts` merges `defaults.json` with JSON/YAML overrides from project, project config dirs, user config dirs, plugin roots, and home; if there are no overrides it auto-detects servers from root markers plus executable discovery.\n5. Server routing uses `getServersForFile()` / `getServerForFile()` from `config.ts`: extension or basename match, then sort primary servers before linters. `index.ts` further filters custom linter clients out of navigation/refactor paths with `getLspServersForFile()` / `getLspServerForFile()`.\n6. `getOrCreateClient()` in `client.ts` creates one process per `command:cwd`, optionally wraps supported commands with `lspmux`, spawns the server, starts the background message reader, sends `initialize`, stores server capabilities, then sends `initialized`.\n7. The message reader in `client.ts` parses LSP frames, resolves pending requests, caches `publishDiagnostics`, tracks `$/progress` tokens for project-load completion, answers `workspace/configuration`, and applies `workspace/applyEdit` requests through `applyWorkspaceEdit()`.\n8. File-scoped actions call `ensureFileOpen()` before requests. Column resolution uses `resolveSymbolColumn()` from `utils.ts`: read the target file, pick first non-whitespace when `symbol` is omitted, otherwise find the exact or case-insensitive match on the target line and honor `#N` occurrence selectors.\n9. Actions dispatch in `LspTool.execute()` through dedicated branches: workspace-only branches (`status`, some `diagnostics`, workspace `symbols`, workspace `reload`, `capabilities`, `request`) run before the single-file switch; all other single-file actions share one client lookup and `switch(action)`.\n10. Requests go through `sendRequest()` in `client.ts`, which allocates an incrementing JSON-RPC id, installs abort and timeout handling, sends `$/cancelRequest` on abort, and rejects on timeout or process exit.\n11. Actions that return edits either preview with `formatWorkspaceEdit()` or apply with `applyWorkspaceEdit()` from `edits.ts`; `rename_file` also performs the filesystem rename and then sends `workspace/didRenameFiles`.\n12. Non-abort failures inside the single-file action block are converted to `LSP error: ...`; many precondition failures return explicit text without throwing.\n\n## Modes / Variants\n### Routing and workspace scope\n- `file: \"*\"` is only special for `diagnostics`, `symbols`, and `reload`.\n- `status` ignores `file`.\n- `capabilities` with omitted `file` or `\"*\"` inspects all non-custom LSP servers; with a concrete file it scopes to matching non-custom servers.\n- `request` with omitted `file` or `\"*\"` chooses the first available non-custom LSP server; with a concrete file it chooses that file's primary non-linter server.\n- `rename_file` sends `workspace/willRenameFiles` and `workspace/didRenameFiles` to every non-custom LSP server from `getLspServers(config)`, not just one file-scoped server.\n- Diagnostics are the only tool action that queries both normal LSP servers and custom linter clients (`BiomeClient`, `SwiftLintClient`, or `LspLinterClient`).\n\n### `diagnostics`\n**Inputs**\n- Required: `file`, unless using workspace mode with `file: \"*\"`.\n- Optional: `timeout`.\n\n**Execution**\n- `file: \"*\"`: `runWorkspaceDiagnostics()` detects project type from root markers and runs one subprocess command: Rust `cargo check --message-format=short`, TypeScript `npx tsc --noEmit`, Go `go build ./...`, Python `pyright`.\n- Concrete file or glob: `resolveDiagnosticTargets()` treats non-globs as one target, otherwise expands a `Bun.Glob` up to `MAX_GLOB_DIAGNOSTIC_TARGETS`.\n- Per file, every matching server runs: custom clients call `lint(file)`; real LSP servers optionally wait for project load, capture `diagnosticsVersion`, `refreshFile()`, then `waitForDiagnostics()` for fresh `publishDiagnostics`.\n- Results are deduplicated by range+message and severity-sorted.\n\n**Output text**\n- Single target with no issues: `OK`.\n- Single target with issues: `<summary>:\\n<grouped diagnostics>`.\n- Batch/glob target: one section per file, plus an initial truncation warning when the glob exceeds the file cap.\n- Workspace mode: `Workspace diagnostics (<detected description>):\\n<command output>`.\n\n### `definition`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `timeout`.\n\n**Execution**\n- Sends `textDocument/definition` with `{ textDocument, position }`.\n- Accepts `Location`, `Location[]`, `LocationLink`, or `LocationLink[]`; `normalizeLocationResult()` converts `LocationLink` to `targetSelectionRange ?? targetRange`.\n- Waits for project load before the request.\n\n**Output text**\n- `No definition found` or `Found N definition(s):` followed by `file:line:col` and one context line above/below each location.\n\n### `type_definition`\nSame as `definition`, but sends `textDocument/typeDefinition` and reports `type definition(s)`.\n\n### `implementation`\nSame as `definition`, but sends `textDocument/implementation` and reports `implementation(s)`.\n\n### `references`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `timeout`.\n\n**Execution**\n- Sends `textDocument/references` with `includeDeclaration: true`.\n- For project-aware servers, retries up to `REFERENCES_RETRY_COUNT` times when the only hit is the queried declaration; between retries it waits for project load and sleeps `REFERENCES_RETRY_DELAY_MS`.\n- First `REFERENCE_CONTEXT_LIMIT` references include surrounding context; the rest are location-only.\n\n**Output text**\n- `No references found` or `Found N reference(s):` with contextual entries first, then `... M additional reference(s) shown without context` when truncated.\n\n### `hover`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `timeout`.\n\n**Execution**\n- Sends `textDocument/hover`.\n- `extractHoverText()` flattens strings, markup content, marked-string objects, or arrays into plain text.\n\n**Output text**\n- `No hover information` or the extracted hover text.\n\n### `symbols`\n**Inputs**\n- Workspace mode: `file: \"*\"` or omitted file on the early workspace branch, plus required `query`.\n- Document mode: required `file`.\n- Optional: `timeout`.\n\n**Execution**\n- Workspace mode sends `workspace/symbol` to every non-custom LSP server, post-filters matches with `filterWorkspaceSymbols()`, deduplicates with `dedupeWorkspaceSymbols()`, then truncates to `WORKSPACE_SYMBOL_LIMIT`.\n- Document mode sends `textDocument/documentSymbol` to the primary server. If the first item has `selectionRange`, it formats hierarchical `DocumentSymbol`s; otherwise it formats flat `SymbolInformation`s.\n\n**Output text**\n- Workspace mode: `Found N symbol(s) matching \"query\":` plus formatted `name @ file:line:col`, with an omission line when over the limit.\n- Document mode: `Symbols in <file>:` plus hierarchical or flat symbol lines.\n\n### `rename`\n**Inputs**\n- Required: `file`, `new_name`.\n- Optional: `line`, `symbol`, `apply`, `timeout`.\n\n**Execution**\n- Waits for project load, sends `textDocument/rename`, receives a `WorkspaceEdit`.\n- `apply !== false` applies edits immediately with `applyWorkspaceEdit()`.\n- `apply === false` renders a preview with `formatWorkspaceEdit()`.\n\n**Output text**\n- `Rename returned no edits`, `Applied rename:` plus applied change lines, or `Rename preview:` plus summarized edits.\n\n### `rename_file`\n**Inputs**\n- Required: `file` source path, `new_name` destination path.\n- Optional: `apply`, `timeout`.\n\n**Execution**\n- Resolves absolute source and destination, rejects identical paths, missing source, existing destination, empty rename set, or directories with more than `MAX_RENAME_PAIRS` files.\n- `enumerateRenamePairs()` returns one `{oldUri,newUri}` pair for a file or walks every regular file in a directory tree.\n- Sends `workspace/willRenameFiles` with `{ files: pairs }` to every non-custom LSP server; collects returned `WorkspaceEdit`s and server notes.\n- Preview mode (`apply === false`) only formats those edits.\n- Apply mode runs each returned `WorkspaceEdit`, renames the source path on disk, sends `textDocument/didClose` for every renamed open file, deletes those `openFiles` entries, then sends `workspace/didRenameFiles`.\n\n**Output text**\n- Preview: `Rename preview: <file-count label> → <dest>` plus per-server edit summaries and optional server notes.\n- Apply: `Renamed <file-count label> → <dest>` plus applied edit summaries, filesystem rename line, and optional server notes.\n\n### `code_actions`\n**Inputs**\n- Required: `file`.\n- Optional: `line`, `symbol`, `query`, `apply`, `timeout`.\n\n**Execution**\n- Reads cached diagnostics for the open URI from `client.diagnostics` and sends `textDocument/codeAction` for a zero-width range at the resolved position.\n- When `apply !== true`, `query` is passed as `context.only: [query]`; this is a server-side kind filter.\n- When `apply === true`, `query` becomes a required client-side selector: either a zero-based numeric index or a case-insensitive substring of the action title.\n- Applying a `CodeAction` uses `applyCodeAction()`: optionally `codeAction/resolve`, then `applyWorkspaceEdit(edit)`, then optional `workspace/executeCommand`.\n- Applying a bare `Command` only runs `workspace/executeCommand`.\n\n**Output text**\n- List mode: `N code action(s):` plus `index: [kind] title` lines.\n- Apply mode success: `Applied \"title\":` plus `Workspace edit:` and/or `Executed command(s):` sections.\n- Apply mode miss: `No code action matches \"query\". Available actions:`.\n- Apply mode with no edit/command: `Action \"title\" has no workspace edit or command to apply`.\n\n### `status`\n**Inputs**\n- None.\n\n**Execution**\n- Reads configured servers from cached `LspConfig`, not `getActiveClients()`.\n- Calls `detectLspmux()` and appends status text when `lspmux` is installed.\n\n**Output text**\n- `Active language servers: ...` or `No language servers configured for this project`, optionally followed by `lspmux: active (multiplexing enabled)` or `lspmux: installed but server not running`.\n\n### `reload`\n**Inputs**\n- Workspace mode: `file: \"*\"` or omitted `file`.\n- Single-file mode: required `file`.\n- Optional: `timeout`.\n\n**Execution**\n- Workspace mode reloads every non-custom LSP server.\n- Single-file mode reloads the primary server for that file.\n- `reloadServer()` tries `rust-analyzer/reloadWorkspace`, then `workspace/didChangeConfiguration` with `{ settings: {} }`; if neither works it kills the process so the next request cold-starts a new client.\n\n**Output text**\n- One line per server: `Reloaded <server>`, `Restarted <server>`, or `Failed to reload <server>: ...`.\n\n### `capabilities`\n**Inputs**\n- Optional: `file`, `timeout`.\n\n**Execution**\n- With a concrete `file`, inspects matching non-custom servers for that file.\n- With omitted `file` or `\"*\"`, inspects every non-custom configured server.\n- Starts servers as needed and dumps `client.serverCapabilities ?? {}` as pretty JSON.\n\n**Output text**\n- Per server: `<server>:` followed by indented `capabilities: { ... }`, or `<server>: failed to start (...)`.\n\n### `request`\n**Inputs**\n- Required: `query` method name.\n- Optional: `file`, `line`, `symbol`, `payload`, `timeout`.\n\n**Execution**\n- Chooses one non-custom server: file-scoped primary server, otherwise the first configured non-custom server.\n- Param building precedence:\n 1. If `payload` is present, parse JSON and use it verbatim.\n 2. Else if `file` is concrete and `line` is present, build `{ textDocument: { uri }, position: { line: line - 1, character } }` using `resolveSymbolColumn()`.\n 3. Else if `file` is concrete, build `{ textDocument: { uri } }`.\n 4. Else use `{}`.\n- Opens the file first when `file` is concrete.\n\n**Output text**\n- Success: `<server> ← <method>:\\n<formatted result>`, where non-string results are `JSON.stringify(..., null, 2)` and nullish values become `null`.\n- Failure: `LSP error from <server> on <method>: ...`.\n\n## Side Effects\n- Filesystem\n - Reads config files, target files, and root markers.\n - `rename` and `code_actions` may edit/create/delete/rename files via `applyWorkspaceEdit()`.\n - `rename_file` always renames the source path on disk in apply mode.\n - Server-initiated `workspace/applyEdit` requests also mutate files through `applyWorkspaceEdit()`.\n- Network\n - None directly; communication is local stdio JSON-RPC to subprocesses.\n- Subprocesses / native bindings\n - Spawns language servers with `ptree.spawn()`.\n - Workspace diagnostics spawns `cargo`, `npx`, `go`, or `pyright`.\n - `BiomeClient` and `SwiftLintClient` spawn CLI tools.\n - Optional `lspmux` detection spawns `lspmux status`; supported servers may be wrapped through `lspmux client`.\n- Session state (transcript, memory, jobs, checkpoints, registries)\n - Caches config per cwd in `configCache`.\n - Caches LSP clients per `command:cwd`, with `pendingRequests`, `diagnostics`, `openFiles`, `serverCapabilities`, and project-load state.\n - Caches custom linter clients by `serverName:cwd`.\n - Updates client `lastActivity`; optional idle-timeout cleanup is driven by `setIdleTimeout()`.\n- Background work / cancellation\n - Every request has an abortable timeout signal.\n - Aborting an in-flight LSP request sends `$/cancelRequest`.\n - Background message readers persist for each live client until process exit/shutdown.\n\n## Limits & Caps\n- Tool timeout clamp: default `20`, min `5`, max `60` seconds — `TOOL_TIMEOUTS.lsp` in `packages/coding-agent/src/tools/tool-timeouts.ts`.\n- LSP request default timeout inside `sendRequest()`: `30_000ms` — `DEFAULT_REQUEST_TIMEOUT_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Warmup initialize timeout default: `5_000ms` — `WARMUP_TIMEOUT_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Project-load wait fallback: `15_000ms` — `PROJECT_LOAD_TIMEOUT_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Idle-client sweep interval when enabled: `60_000ms` — `IDLE_CHECK_INTERVAL_MS` in `packages/coding-agent/src/lsp/client.ts`.\n- Diagnostic message output cap: first `50` messages — `DIAGNOSTIC_MESSAGE_LIMIT` in `packages/coding-agent/src/lsp/index.ts`.\n- Single-file diagnostics wait: `3_000ms` — `SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS`.\n- Batch/glob diagnostics wait per file: `400ms` — `BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS`.\n- Glob diagnostic target cap: first `20` matches — `MAX_GLOB_DIAGNOSTIC_TARGETS`.\n- Workspace symbol cap: first `200` entries — `WORKSPACE_SYMBOL_LIMIT`.\n- Reference context cap: first `50` references include source context — `REFERENCE_CONTEXT_LIMIT`.\n- References retry count: `2` retries, `250ms` backoff — `REFERENCES_RETRY_COUNT`, `REFERENCES_RETRY_DELAY_MS`.\n- Directory rename cap: `1_000` file pairs — `MAX_RENAME_PAIRS`.\n- `detectLspmux()` state cache TTL: `5 * 60 * 1000ms`; liveness check timeout: `1_000ms` — `STATE_CACHE_TTL_MS`, `LIVENESS_TIMEOUT_MS` in `packages/coding-agent/src/lsp/lspmux.ts`.\n- Workspace diagnostics output cap: first `50` lines from the subprocess.\n\n## Errors\n- Missing or invalid inputs are usually returned as text with `details.success: false`, not thrown:\n - missing `file`/`query`/`new_name`\n - invalid JSON in `payload`\n - no matching server\n - invalid `rename_file` source/destination conditions\n- `resolveSymbolColumn()` throws explicit errors for missing files, missing symbols, and out-of-bounds `#N` selectors; these surface as `LSP error: ...` or request-specific error text.\n- `sendRequest()` rejects on timeout with `LSP request <method> timed out after <ms>ms`.\n- Client process exit rejects all pending requests with an exit-code/stderr error assembled in `getOrCreateClient()`.\n- Single-file action failures inside the main `try` become `LSP error: <message>`.\n- `request` has its own error envelope: `LSP error from <server> on <method>: <message>`.\n- Some server failures are intentionally softened:\n - diagnostics continue when one server fails\n - `rename_file` suppresses `workspace/willRenameFiles` “method not found” errors and records other server errors as notes\n - `code_actions` ignores `codeAction/resolve` failures and applies unresolved actions when possible\n- Aborts are not converted to text: `ToolAbortError` is rethrown.\n\n## Notes\n- `status` reports configured/available servers from `LspConfig`, not currently active client processes from `getActiveClients()`.\n- `getLspServerForFile()` excludes `createClient` adapters and linter-only servers; navigation/refactor actions never target Biome/SwiftLint custom clients.\n- `getServersForFile()` matches both file extensions and exact basenames from `fileTypes`; config can target names like `Dockerfile` if present.\n- `symbol` matching is exact first, then case-insensitive, and falls back to the Nth occurrence on the specified line only; it never scans other lines.\n- `code_actions` uses `query` in two different ways: server-side `context.only` filter in list mode, client-side title/index selector in apply mode.\n- `rename` and `rename_file` default to apply. Preview requires `apply: false`.\n- `request` with `file: \"*\"` is treated the same as omitted `file`: it does not build workspace-specific params.\n- `reload` does not recreate a client immediately after killing it; the next request triggers reinitialization.\n- `workspace/applyEdit` can apply edits initiated by the server outside the direct tool action result path.\n- `detectLspmux()` can be disabled with `SKC_DISABLE_LSPMUX=1`; only `rust-analyzer` is in `DEFAULT_SUPPORTED_SERVERS`.\n- Startup LSP warmup (`discoverStartupLspServers(cwd)` in `sdk.ts`) is gated on `enableLsp && options.hasUI && settings.get(\"lsp.diagnosticsOnWrite\")` — print/RPC/ACP/script sessions skip it and let `getOrCreateClient()` cold-start servers on demand. See `docs/sdk.md` § Startup performance.\n- `configCache` is per-process and never auto-invalidated; config changes require a fresh process to be observed by `getConfig()` callers.",
|
|
111
112
|
"tools/monitor.md": "# monitor\n\n> Start a background monitor that streams stdout lines as task notifications. Mirrors Claude Code's `Monitor` tool surface.\n\n## Source\n\n- Entry: `packages/coding-agent/src/tools/monitor.ts`\n- Model-facing prompt: `packages/coding-agent/src/prompts/tools/monitor.md`\n- Key collaborators:\n - `packages/coding-agent/src/async/job-manager.ts` — stores the captured stream via `appendOutput`/`readOutputSince` and delivers the final background-job result.\n - `packages/coding-agent/src/exec/bash-executor.ts` and `packages/coding-agent/src/session/streaming-output.ts` — provide the unthrottled `onRawChunk` capture hook that feeds the manager.\n - `packages/coding-agent/src/tools/bash.ts` — exposes `BashTool.startMonitorJob(...)`, a Bash-aligned helper that preserves interception, cwd/env expansion, artifacts, timeouts, and raw stream capture.\n - `packages/coding-agent/src/tools/job.ts` — polls/cancels the monitor task by id; there is no sibling `MonitorKill` tool.\n\n## Inputs\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `command` | `string` | Yes | Shell command to run as a background monitor. Each sanitized stdout line is delivered as a task-notification. |\n| `kind` | `\"log\" \\| \"poll\" \\| \"watch\" \\| \"other\"` | Yes | Category of monitor. Surfaces in listings. |\n| `description` | `string` | Yes | Short human-readable summary of what is being monitored. |\n| `timeout` | `number` | No | Maximum wall-clock seconds the monitor may run before automatic shutdown. Omit for session lifetime. |\n| `persistent` | `boolean` | No | Keep the monitor running after its first emitted event. Persistent monitors survive until session end, timeout, or explicit cancel via `job`. Defaults to `false`. |\n\n## Outputs\n\nThe tool returns one text block plus `details`:\n\n- `content[0].text`: `Monitor started · task <task_id> · persistent: true|false`.\n- `details`: `{ taskId, kind, description, command, persistent }`.\n\nEach newline-terminated stdout line is appended to the manager-owned cursor and sent to the agent as a `<task-notification>` custom message between turns. Use `job` with the returned `taskId` to inspect completion state or terminate the monitor.\n\n## Behavior / Lifecycle\n\n1. `MonitorTool.createIf(session)` gates the tool on `isBackgroundJobSupportEnabled(session.settings)` — identical to `JobTool`'s gate.\n2. `execute(...)` delegates to `BashTool.startMonitorJob(...)`, so Monitor inherits Bash's interception rules, cwd normalization, internal URL expansion, environment construction, artifact allocation, timeout clamping, and unthrottled raw capture.\n3. The helper mirrors every sanitized raw chunk to `manager.appendOutput(jobId, chunk)` and line-buffers the stream so each stdout line dispatches one `<task-notification>` event.\n4. Non-persistent monitors auto-cancel after delivering their first stdout-line notification. Persistent monitors terminate when the underlying command exits, `timeout` elapses, the calling agent is torn down, or the user cancels the returned background task via `job`.\n\n## Errors\n\n- `ToolError`: `Async execution is disabled; the monitor tool is unavailable in this session.` — emitted when `AsyncJobManager.instance()` returns `undefined`.\n- Invalid parameter shapes are rejected by zod with the project's standard validation error path.\n\n## Examples\n\nTail an error log and react when lines appear:\n\n```jsonc\n{\n \"command\": \"tail -F /var/log/app.log | grep -i error\",\n \"kind\": \"log\",\n \"description\": \"Tail app.log for errors\"\n}\n```\n\nPoll CI status until the build completes (15-minute timeout):\n\n```jsonc\n{\n \"command\": \"while true; do gh run view --json status,conclusion --jq .status; sleep 30; done\",\n \"kind\": \"poll\",\n \"description\": \"Watch CI build status\",\n \"timeout\": 900,\n \"persistent\": false\n}\n```\n\n## Parity oracle\n\nThe schema and behavior captured in this doc are pinned by the fixture at\n`packages/coding-agent/test/fixtures/claude-code-tools/monitor.schema.json`,\ncaptured from the upstream Claude Code CLI (`claude --version 2.1.152`). Any\ndeviation from that fixture is a parity bug.\n",
|