@sayknow-cli/coding-agent 0.4.7 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (428) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/cli/read-cli.d.ts +1 -0
  6. package/dist/types/cli.d.ts +9 -1
  7. package/dist/types/commands/daemon.d.ts +2 -2
  8. package/dist/types/commands/deep-interview.d.ts +9 -0
  9. package/dist/types/commands/harness.d.ts +22 -0
  10. package/dist/types/commands/read.d.ts +6 -0
  11. package/dist/types/commands/sdk.d.ts +20 -2
  12. package/dist/types/config/keybindings.d.ts +34 -4
  13. package/dist/types/config/model-profile-contract.d.ts +40 -0
  14. package/dist/types/config/model-profiles.d.ts +2 -3
  15. package/dist/types/config/model-registry.d.ts +5 -0
  16. package/dist/types/config/provider-auth-health.d.ts +14 -0
  17. package/dist/types/config/provider-ranking.d.ts +59 -0
  18. package/dist/types/config/settings-schema.d.ts +156 -10
  19. package/dist/types/config/settings.d.ts +3 -0
  20. package/dist/types/edit/modes/patch.d.ts +13 -0
  21. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  22. package/dist/types/eval/py/executor.d.ts +1 -1
  23. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  24. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  25. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  26. package/dist/types/extensibility/shared-events.d.ts +6 -0
  27. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  28. package/dist/types/main.d.ts +33 -2
  29. package/dist/types/memories/index.d.ts +2 -0
  30. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  31. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  32. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  35. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  36. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  37. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  38. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  39. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  41. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  42. package/dist/types/modes/components/welcome.d.ts +5 -0
  43. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  45. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  46. package/dist/types/modes/interactive-mode.d.ts +13 -2
  47. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  48. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  49. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  50. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  51. package/dist/types/modes/types.d.ts +6 -0
  52. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  53. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  54. package/dist/types/runtime/memory-domain.d.ts +9 -0
  55. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  56. package/dist/types/runtime/memory-guard.d.ts +39 -0
  57. package/dist/types/runtime/memory-limit.d.ts +11 -0
  58. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  59. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
  60. package/dist/types/runtime-mcp/manager.d.ts +9 -1
  61. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  62. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  63. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  64. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  65. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  66. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  67. package/dist/types/sdk/broker/broker.d.ts +2 -0
  68. package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
  69. package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
  70. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  71. package/dist/types/sdk/bus/config.d.ts +2 -0
  72. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  73. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  74. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  75. package/dist/types/sdk/bus/index.d.ts +52 -4
  76. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  77. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  78. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  79. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  80. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  81. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
  82. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  83. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  84. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
  85. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  86. package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
  87. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  88. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  89. package/dist/types/sdk/client/discovery.d.ts +2 -0
  90. package/dist/types/sdk/client/liveness.d.ts +7 -0
  91. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  92. package/dist/types/sdk/host/host.d.ts +3 -1
  93. package/dist/types/sdk/host/query/handlers.d.ts +14 -0
  94. package/dist/types/sdk/index.d.ts +2 -0
  95. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  96. package/dist/types/sdk/prompt-status.d.ts +88 -0
  97. package/dist/types/sdk/session-directory.d.ts +1 -1
  98. package/dist/types/sdk/session.d.ts +7 -0
  99. package/dist/types/sdk/startup-capability.d.ts +9 -0
  100. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  101. package/dist/types/sdk/transport/index.d.ts +12 -0
  102. package/dist/types/sdk/transport/relay.d.ts +43 -0
  103. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  104. package/dist/types/sdk/transport/socket.d.ts +7 -0
  105. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  106. package/dist/types/session/agent-session.d.ts +83 -24
  107. package/dist/types/session/btw-contract.d.ts +16 -0
  108. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  109. package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
  110. package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
  111. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  112. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  113. package/dist/types/session/session-manager.d.ts +89 -18
  114. package/dist/types/session/session-storage.d.ts +31 -0
  115. package/dist/types/session/streaming-output.d.ts +60 -5
  116. package/dist/types/setup/credential-import.d.ts +5 -1
  117. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  118. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
  119. package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
  120. package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
  121. package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
  122. package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
  123. package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
  124. package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
  125. package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
  126. package/dist/types/skc-runtime/state-writer.d.ts +5 -1
  127. package/dist/types/skc-runtime/team-launch.d.ts +1 -1
  128. package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
  129. package/dist/types/skc-runtime/team-store.d.ts +12 -0
  130. package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
  131. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
  132. package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
  133. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
  134. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
  135. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  136. package/dist/types/ssh/utils.d.ts +1 -0
  137. package/dist/types/task/discovery.d.ts +3 -1
  138. package/dist/types/task/executor.d.ts +6 -0
  139. package/dist/types/task/index.d.ts +3 -1
  140. package/dist/types/task/provider-retry-status.d.ts +13 -0
  141. package/dist/types/task/receipt.d.ts +2 -0
  142. package/dist/types/task/types.d.ts +133 -0
  143. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  144. package/dist/types/tools/browser/launch.d.ts +7 -0
  145. package/dist/types/tools/fetch.d.ts +22 -9
  146. package/dist/types/tools/image-gen.d.ts +63 -1
  147. package/dist/types/tools/index.d.ts +7 -1
  148. package/dist/types/tools/output-meta.d.ts +21 -1
  149. package/dist/types/tools/read-internals.d.ts +12 -0
  150. package/dist/types/tools/read.d.ts +45 -1
  151. package/dist/types/tools/resource-gc.d.ts +23 -0
  152. package/dist/types/tools/skill-discovery.d.ts +7 -0
  153. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  154. package/dist/types/tools/tool-result.d.ts +7 -1
  155. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  156. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  157. package/dist/types/web/insane/bridge.d.ts +1 -0
  158. package/dist/types/web/insane/url-guard.d.ts +17 -0
  159. package/package.json +8 -12
  160. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  161. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  162. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  163. package/scripts/compile-args.ts +0 -1
  164. package/scripts/dogfood-repository-binding.ts +209 -0
  165. package/scripts/generate-docs-index.ts +4 -1
  166. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  167. package/scripts/run-test-manifest.ts +6 -4
  168. package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
  169. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  170. package/src/capability/index.ts +58 -42
  171. package/src/capability/types.ts +3 -0
  172. package/src/cli/args.ts +16 -100
  173. package/src/cli/fast-help.ts +1 -1
  174. package/src/cli/mcp-cli.ts +4 -21
  175. package/src/cli/notify-cli.ts +6 -38
  176. package/src/cli/read-cli.ts +2 -1
  177. package/src/cli/setup-cli.ts +10 -4
  178. package/src/cli.ts +133 -11
  179. package/src/commands/daemon.ts +22 -5
  180. package/src/commands/deep-interview.ts +26 -1
  181. package/src/commands/harness.ts +35 -12
  182. package/src/commands/read.ts +11 -2
  183. package/src/commands/sdk.ts +134 -17
  184. package/src/commands/team.ts +1 -1
  185. package/src/commit/agentic/index.ts +3 -3
  186. package/src/commit/map-reduce/index.ts +2 -2
  187. package/src/config/keybindings.ts +158 -25
  188. package/src/config/model-profile-contract.ts +174 -0
  189. package/src/config/model-profiles.ts +51 -10
  190. package/src/config/model-registry.ts +24 -4
  191. package/src/config/model-resolver.ts +2 -2
  192. package/src/config/provider-auth-health.ts +42 -0
  193. package/src/config/provider-ranking.ts +119 -0
  194. package/src/config/settings-schema.ts +157 -10
  195. package/src/config/settings.ts +254 -108
  196. package/src/coordinator-mcp/model-preset.ts +21 -74
  197. package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
  198. package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
  199. package/src/defaults/skc/skills/team/SKILL.md +1 -1
  200. package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
  201. package/src/discovery/ssh.ts +12 -1
  202. package/src/edit/index.ts +3 -6
  203. package/src/edit/modes/patch.ts +38 -1
  204. package/src/edit/modes/replace.ts +42 -0
  205. package/src/edit/path-mutation-lock.ts +112 -0
  206. package/src/eval/js/tool-bridge.ts +1 -1
  207. package/src/eval/py/executor.ts +37 -13
  208. package/src/eval/py/kernel.ts +1 -21
  209. package/src/eval/py/prelude.py +2 -2
  210. package/src/eval/py/runner-artifact.ts +84 -0
  211. package/src/eval/py/tool-bridge.ts +34 -18
  212. package/src/extensibility/extensions/runner.ts +4 -0
  213. package/src/extensibility/extensions/types.ts +42 -4
  214. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  215. package/src/extensibility/shared-events.ts +5 -0
  216. package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
  217. package/src/hooks/native-skill-hook.ts +17 -4
  218. package/src/internal-urls/docs-index.generated.ts +17 -16
  219. package/src/internal-urls/local-protocol.ts +87 -25
  220. package/src/internal-urls/local-root-gc.ts +127 -0
  221. package/src/internal-urls/mcp-protocol.ts +73 -11
  222. package/src/main.ts +120 -42
  223. package/src/memories/index.ts +10 -0
  224. package/src/modes/DESIGN.md +52 -0
  225. package/src/modes/acp/acp-agent.ts +647 -83
  226. package/src/modes/acp/acp-event-mapper.ts +106 -12
  227. package/src/modes/acp/acp-mode.ts +39 -3
  228. package/src/modes/action-registry.ts +1 -1
  229. package/src/modes/components/assistant-message.ts +123 -22
  230. package/src/modes/components/btw-panel.ts +82 -16
  231. package/src/modes/components/irc-sidebar.ts +176 -62
  232. package/src/modes/components/model-selector.ts +61 -4
  233. package/src/modes/components/notifications-settings-editor.ts +1 -1
  234. package/src/modes/components/oauth-selector.ts +42 -12
  235. package/src/modes/components/plan-preview-overlay.ts +1 -1
  236. package/src/modes/components/queue-pane.ts +55 -9
  237. package/src/modes/components/queued-message-selector.ts +53 -10
  238. package/src/modes/components/read-tool-group.ts +82 -14
  239. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  240. package/src/modes/components/sayknow-pet-widget.ts +20 -8
  241. package/src/modes/components/session-observer-overlay.ts +187 -32
  242. package/src/modes/components/settings-selector.ts +60 -18
  243. package/src/modes/components/tool-execution.ts +138 -27
  244. package/src/modes/components/tool-status-header.ts +26 -8
  245. package/src/modes/components/welcome.ts +50 -23
  246. package/src/modes/controllers/btw-controller.ts +119 -36
  247. package/src/modes/controllers/command-controller.ts +101 -54
  248. package/src/modes/controllers/event-controller.ts +119 -27
  249. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  250. package/src/modes/controllers/input-controller.ts +100 -8
  251. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  252. package/src/modes/controllers/selector-controller.ts +4 -1
  253. package/src/modes/interactive-mode.ts +201 -60
  254. package/src/modes/irc-observation-ledger.ts +71 -15
  255. package/src/modes/prompt-action-autocomplete.ts +32 -13
  256. package/src/modes/prompt-suggestion-controller.ts +96 -0
  257. package/src/modes/runtime-init.ts +20 -2
  258. package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
  259. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  260. package/src/modes/types.ts +6 -0
  261. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  262. package/src/modes/utils/ui-helpers.ts +99 -4
  263. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  264. package/src/prompts/agents/architect.md +17 -3
  265. package/src/prompts/agents/critic.md +17 -2
  266. package/src/prompts/agents/executor.md +1 -1
  267. package/src/prompts/agents/planner.md +8 -1
  268. package/src/prompts/system/btw-user.md +3 -8
  269. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  270. package/src/prompts/system/system-prompt.md +6 -3
  271. package/src/prompts/tools/cron.md +3 -1
  272. package/src/prompts/tools/read.md +4 -2
  273. package/src/prompts/tools/skill-discovery.md +1 -0
  274. package/src/runtime/memory-domain.ts +66 -0
  275. package/src/runtime/memory-guard-contract.ts +73 -0
  276. package/src/runtime/memory-guard.ts +292 -0
  277. package/src/runtime/memory-limit.ts +50 -0
  278. package/src/runtime-mcp/client.ts +41 -55
  279. package/src/runtime-mcp/content-limits.ts +71 -0
  280. package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
  281. package/src/runtime-mcp/json-rpc.ts +28 -11
  282. package/src/runtime-mcp/manager.ts +77 -59
  283. package/src/runtime-mcp/oauth-flow.ts +0 -72
  284. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  285. package/src/runtime-mcp/redaction.ts +30 -0
  286. package/src/runtime-mcp/smithery-auth.ts +17 -2
  287. package/src/runtime-mcp/smithery-connect.ts +11 -1
  288. package/src/runtime-mcp/transports/http.ts +63 -22
  289. package/src/sdk/acp/adapter.ts +8 -1
  290. package/src/sdk/acp/final-text.ts +40 -0
  291. package/src/sdk/acp/mcp.ts +26 -0
  292. package/src/sdk/broker/broker.ts +34 -20
  293. package/src/sdk/broker/identity.ts +22 -12
  294. package/src/sdk/broker/lifecycle.ts +293 -43
  295. package/src/sdk/broker/session-index.ts +11 -1
  296. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  297. package/src/sdk/bus/chat-daemon-control.ts +22 -5
  298. package/src/sdk/bus/config-commands.ts +2 -2
  299. package/src/sdk/bus/config.ts +10 -2
  300. package/src/sdk/bus/control-drain-lease.ts +104 -0
  301. package/src/sdk/bus/conversation-store.ts +40 -6
  302. package/src/sdk/bus/daemon-paths.ts +2 -0
  303. package/src/sdk/bus/index.ts +793 -121
  304. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  305. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  306. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  307. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  308. package/src/sdk/bus/notification-orchestration.ts +12 -0
  309. package/src/sdk/bus/notification-service.ts +87 -25
  310. package/src/sdk/bus/operator-runtime.ts +31 -5
  311. package/src/sdk/bus/prompt-reconciliation.ts +240 -0
  312. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  313. package/src/sdk/bus/recent-activity.ts +90 -26
  314. package/src/sdk/bus/reconciliation-store.ts +224 -0
  315. package/src/sdk/bus/telegram-cli.ts +6 -13
  316. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  317. package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
  318. package/src/sdk/bus/telegram-daemon-control.ts +161 -40
  319. package/src/sdk/bus/telegram-daemon.ts +3649 -593
  320. package/src/sdk/bus/telegram-reference.ts +20 -2
  321. package/src/sdk/bus/topic-registry.ts +451 -12
  322. package/src/sdk/cli/session-cli.ts +15 -2
  323. package/src/sdk/client/discovery.ts +19 -3
  324. package/src/sdk/client/liveness.ts +49 -0
  325. package/src/sdk/host/control/dispatch.ts +8 -2
  326. package/src/sdk/host/control/operations.ts +3 -2
  327. package/src/sdk/host/host.ts +27 -9
  328. package/src/sdk/host/query/handlers.ts +125 -5
  329. package/src/sdk/index.ts +12 -0
  330. package/src/sdk/lifecycle-session.ts +19 -3
  331. package/src/sdk/mcp/server.ts +46 -18
  332. package/src/sdk/prompt-status.ts +97 -0
  333. package/src/sdk/protocol/operation-inventory.generated.json +145 -0
  334. package/src/sdk/protocol/operation-registry.ts +24 -4
  335. package/src/sdk/session-directory.ts +5 -1
  336. package/src/sdk/session.ts +69 -8
  337. package/src/sdk/startup-capability.ts +15 -1
  338. package/src/sdk/transport/auth-preface.ts +82 -0
  339. package/src/sdk/transport/index.ts +18 -0
  340. package/src/sdk/transport/relay.ts +252 -0
  341. package/src/sdk/transport/serve-cli.ts +105 -0
  342. package/src/sdk/transport/socket.ts +197 -0
  343. package/src/sdk/transport/stdio.ts +17 -0
  344. package/src/session/agent-session.ts +1112 -314
  345. package/src/session/blob-store.ts +25 -7
  346. package/src/session/btw-contract.ts +59 -0
  347. package/src/session/fallback-chain-controller.ts +47 -8
  348. package/src/session/internal/managed-session-scope.ts +1426 -259
  349. package/src/session/internal/managed-session-storage.ts +414 -238
  350. package/src/session/internal/native-publish-outcome.ts +271 -0
  351. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  352. package/src/session/session-manager.ts +2079 -469
  353. package/src/session/session-storage.ts +151 -11
  354. package/src/session/streaming-output.ts +262 -43
  355. package/src/setup/credential-import.ts +28 -3
  356. package/src/setup/provider-onboarding.ts +49 -5
  357. package/src/setup/provider-presets.json +2 -1
  358. package/src/skc-runtime/deep-interview-runtime.ts +303 -36
  359. package/src/skc-runtime/deep-interview-stage.ts +1016 -0
  360. package/src/skc-runtime/deep-interview-state.ts +8 -5
  361. package/src/skc-runtime/gc-render.ts +1 -0
  362. package/src/skc-runtime/gc-runtime.ts +11 -1
  363. package/src/skc-runtime/linux-proc.ts +70 -36
  364. package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
  365. package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
  366. package/src/skc-runtime/ralplan-runtime.ts +1057 -75
  367. package/src/skc-runtime/repository-binding.ts +267 -0
  368. package/src/skc-runtime/session-state-sidecar.ts +25 -9
  369. package/src/skc-runtime/state-runtime.ts +50 -12
  370. package/src/skc-runtime/state-writer.ts +68 -56
  371. package/src/skc-runtime/team-launch.ts +34 -5
  372. package/src/skc-runtime/team-runtime.ts +41 -1
  373. package/src/skc-runtime/team-store.ts +32 -0
  374. package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
  375. package/src/skc-runtime/team-workers.ts +1 -0
  376. package/src/skc-runtime/tmux-common.ts +13 -2
  377. package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
  378. package/src/skc-runtime/tmux-sessions.ts +45 -6
  379. package/src/skc-runtime/ultragoal-guard.ts +93 -15
  380. package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
  381. package/src/skc-runtime/ultragoal-runtime.ts +469 -38
  382. package/src/skc-runtime/workflow-manifest.generated.json +195 -5
  383. package/src/skc-runtime/workflow-manifest.ts +67 -3
  384. package/src/skill-state/workflow-hud.ts +25 -2
  385. package/src/skill-state/workflow-mutation-guard.ts +104 -12
  386. package/src/slash-commands/builtin-registry.ts +1 -1
  387. package/src/ssh/utils.ts +14 -0
  388. package/src/task/discovery.ts +8 -2
  389. package/src/task/executor.ts +165 -47
  390. package/src/task/index.ts +123 -15
  391. package/src/task/provider-retry-status.ts +88 -0
  392. package/src/task/receipt.ts +3 -0
  393. package/src/task/render.ts +45 -26
  394. package/src/task/skc-command.ts +2 -2
  395. package/src/task/types.ts +55 -10
  396. package/src/task/ultragoal-redteam-activation.ts +75 -0
  397. package/src/tools/ask.ts +1 -1
  398. package/src/tools/ast-edit.ts +2 -2
  399. package/src/tools/bash-allowed-prefixes.ts +45 -1
  400. package/src/tools/bash-pty-selection.ts +2 -2
  401. package/src/tools/browser/launch.ts +41 -6
  402. package/src/tools/browser/screenshot-format.ts +6 -1
  403. package/src/tools/cron.ts +1 -1
  404. package/src/tools/fetch.ts +127 -154
  405. package/src/tools/image-gen.ts +476 -23
  406. package/src/tools/index.ts +7 -1
  407. package/src/tools/output-meta.ts +176 -9
  408. package/src/tools/path-utils.ts +5 -1
  409. package/src/tools/read-internals.ts +44 -0
  410. package/src/tools/read.ts +1544 -310
  411. package/src/tools/resource-gc.ts +665 -125
  412. package/src/tools/skill-discovery.ts +39 -3
  413. package/src/tools/sqlite-reader.ts +63 -5
  414. package/src/tools/subagent-render.ts +74 -16
  415. package/src/tools/subagent.ts +2 -0
  416. package/src/tools/tool-result.ts +14 -1
  417. package/src/utils/edit-mode.ts +2 -2
  418. package/src/utils/pasted-image-path.ts +15 -6
  419. package/src/utils/prompt-suggestion.ts +265 -0
  420. package/src/utils/shell-snapshot.ts +173 -63
  421. package/src/utils/sixel.ts +3 -3
  422. package/src/web/insane/bridge.ts +5 -1
  423. package/src/web/insane/url-guard.ts +140 -27
  424. package/src/web/scrapers/docs-rs.ts +30 -5
  425. package/src/web/scrapers/types.ts +19 -49
  426. package/src/web/scrapers/utils.ts +12 -26
  427. package/src/web/search/providers/codex.ts +1 -1
  428. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -0,0 +1,1016 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import { syncSkillActiveState } from "../skill-state/active-state";
4
+ import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
5
+ import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
6
+ import { applyAmbiguityFloorToEnvelope } from "./deep-interview-ambiguity";
7
+ import {
8
+ assertDeepInterviewEnvelopeInputLimits,
9
+ assertDeepInterviewIntentManifest,
10
+ assertDeepInterviewStructuredResponseWithinLimit,
11
+ mergeDeepInterviewEnvelope,
12
+ normalizeDeepInterviewEnvelope,
13
+ } from "./deep-interview-state";
14
+ import { sessionStateDir } from "./session-layout";
15
+ import { resolveSkcSessionForWrite, SessionResolutionError, writeSessionActivityMarker } from "./session-resolution";
16
+ import { runNativeStateCommand } from "./state-runtime";
17
+ import {
18
+ persistedStateRevision,
19
+ readExistingStateForMutation,
20
+ StateWriteConflictError,
21
+ withWorkflowStateLock,
22
+ workflowEnvelopeContentSha256,
23
+ writeGuardedWorkflowEnvelopeAtomic,
24
+ writeJsonAtomic,
25
+ } from "./state-writer";
26
+ import { CommandError, flagValue, hasFlag, isPlainObject } from "./workflow-cli-common";
27
+
28
+ /**
29
+ * Staged JSON transitions for deep-interview state (`skc deep-interview stage|check|apply|discard`).
30
+ *
31
+ * Design contract (post-#3040 revert; deliberately NOT the typed-flag surface):
32
+ * - The payload is one JSON document supplied whole (`--input '<json>'` or `@file`),
33
+ * merged into current state through the same lossless envelope merge every other
34
+ * sanctioned deep-interview writer uses. There is no per-field flag grammar.
35
+ * - Exactly one pending draft exists per session at a fixed session-scoped path,
36
+ * so no `--draft-id` is needed; the session resolves from `SKC_SESSION_ID` (or
37
+ * payload `session_id`), so no identity flags are needed.
38
+ * - The draft records the `state_revision` it was staged against; `apply` enforces
39
+ * that runtime-side (CAS). A stale draft is auto-invalidated with typed recovery
40
+ * guidance — the agent never does revision arithmetic.
41
+ * - `check` dry-runs the identical merge `apply` performs. Validation is core-schema
42
+ * only (envelope shape, bounded input sizes, locked intent-contract immutability);
43
+ * free-form interview fields pass through untouched.
44
+ */
45
+
46
+ import * as path from "node:path";
47
+
48
+ export const DEEP_INTERVIEW_STAGE_TRANSITIONS = [
49
+ "initialize-context",
50
+ "record-round",
51
+ "update-facts",
52
+ "merge-state",
53
+ ] as const;
54
+ export type DeepInterviewStageTransition = (typeof DEEP_INTERVIEW_STAGE_TRANSITIONS)[number];
55
+
56
+ const DRAFT_VERSION = 1;
57
+ const DRAFT_FILE = "deep-interview-draft.json";
58
+
59
+ export type DeepInterviewStageErrorCode =
60
+ | "DI_STAGE_USAGE"
61
+ | "DI_STAGE_INPUT_INVALID"
62
+ | "DI_STAGE_SESSION_REQUIRED"
63
+ | "DI_STAGE_DRAFT_EXISTS"
64
+ | "DI_STAGE_NO_DRAFT"
65
+ | "DI_STAGE_DRAFT_CORRUPT"
66
+ | "DI_STAGE_STATE_MISSING"
67
+ | "DI_STAGE_STATE_CORRUPT"
68
+ | "DI_STAGE_REVISION_CONFLICT"
69
+ | "DI_STAGE_MERGE_REJECTED";
70
+
71
+ export class DeepInterviewStageError extends CommandError {
72
+ constructor(
73
+ readonly code: DeepInterviewStageErrorCode,
74
+ message: string,
75
+ readonly recovery?: string,
76
+ ) {
77
+ super(2, message);
78
+ this.name = "DeepInterviewStageError";
79
+ }
80
+ }
81
+
82
+ export interface DeepInterviewStageDraft {
83
+ version: typeof DRAFT_VERSION;
84
+ draft_id: string;
85
+ session_id: string;
86
+ transition: DeepInterviewStageTransition;
87
+ /** State revision the draft was staged against; `apply` CAS-checks this. */
88
+ staged_against_revision: number;
89
+ /**
90
+ * Canonical content SHA-256 of the state the draft was staged against. Revision
91
+ * alone cannot catch sanctioned writers that do not bump `state_revision`
92
+ * (seed/spec-persistence), so `apply` requires BOTH to match.
93
+ */
94
+ staged_against_sha256: string;
95
+ payload: Record<string, unknown>;
96
+ created_at: string;
97
+ }
98
+
99
+ export interface DeepInterviewStageCommandResult {
100
+ status: number;
101
+ stdout?: string;
102
+ stderr?: string;
103
+ }
104
+
105
+ export function deepInterviewDraftPath(cwd: string, sessionId: string): string {
106
+ return path.join(sessionStateDir(cwd, sessionId), DRAFT_FILE);
107
+ }
108
+
109
+ function statePathFor(cwd: string, sessionId: string): string {
110
+ return path.join(sessionStateDir(cwd, sessionId), "deep-interview-state.json");
111
+ }
112
+
113
+ // -----------------------------------------------------------------------------
114
+ // Input parsing
115
+ // -----------------------------------------------------------------------------
116
+
117
+ /** Bytes cap for `@file` payloads — conservative for the 100k-char structured-response limit. */
118
+ const MAX_INPUT_FILE_BYTES = 1_000_000;
119
+
120
+ async function parseJsonInput(rawInput: string, cwd: string): Promise<Record<string, unknown>> {
121
+ let text = rawInput;
122
+ if (rawInput.startsWith("@")) {
123
+ const filePath = path.resolve(cwd, rawInput.slice(1));
124
+ try {
125
+ const stat = await fs.stat(filePath);
126
+ if (!stat.isFile()) {
127
+ throw new DeepInterviewStageError(
128
+ "DI_STAGE_INPUT_INVALID",
129
+ `--input file is not a regular file: ${filePath}`,
130
+ );
131
+ }
132
+ if (stat.size > MAX_INPUT_FILE_BYTES) {
133
+ throw new DeepInterviewStageError(
134
+ "DI_STAGE_INPUT_INVALID",
135
+ `--input file exceeds ${MAX_INPUT_FILE_BYTES} bytes (${stat.size}); staged payloads are bounded`,
136
+ );
137
+ }
138
+ text = await fs.readFile(filePath, "utf-8");
139
+ } catch (error) {
140
+ if (error instanceof DeepInterviewStageError) throw error;
141
+ throw new DeepInterviewStageError(
142
+ "DI_STAGE_INPUT_INVALID",
143
+ `failed to read --input file ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
144
+ );
145
+ }
146
+ }
147
+ let parsed: unknown;
148
+ try {
149
+ parsed = JSON.parse(text);
150
+ } catch (error) {
151
+ throw new DeepInterviewStageError(
152
+ "DI_STAGE_INPUT_INVALID",
153
+ `--input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
154
+ );
155
+ }
156
+ if (!isPlainObject(parsed)) {
157
+ throw new DeepInterviewStageError("DI_STAGE_INPUT_INVALID", "--input must be a JSON object");
158
+ }
159
+ return parsed;
160
+ }
161
+
162
+ /**
163
+ * Envelope lifecycle fields the runtime owns exclusively. A staged payload may
164
+ * carry interview data only; phase transitions go through their dedicated verbs
165
+ * (`--write`, `skc state handoff/clear`), never through a staged patch.
166
+ */
167
+ const RUNTIME_OWNED_ENVELOPE_KEYS = [
168
+ "current_phase",
169
+ "active",
170
+ "skill",
171
+ "version",
172
+ "state_revision",
173
+ "source_state_revision",
174
+ "receipt",
175
+ "updated_at",
176
+ "last_applied_draft_id",
177
+ ] as const;
178
+
179
+ /**
180
+ * Nested `state.*` keys owned by the Round-0 ask recorder. A staged/write
181
+ * payload can never set them: a fabricated contract (missing digest/
182
+ * confirmation binding) would poison state so every later merge fails
183
+ * `invalid intent contract`, bricking the interview until a destructive
184
+ * `clear --force`. The recorder is the only writer that can lock intent.
185
+ */
186
+ const RECORDER_OWNED_STATE_KEYS = ["intent_contract", "intent_review"] as const;
187
+
188
+ /** Strip runtime-owned keys from a staged payload; returns the ignored key names. */
189
+ function sanitizeStagedPayload(payload: Record<string, unknown>): {
190
+ payload: Record<string, unknown>;
191
+ ignoredKeys: string[];
192
+ } {
193
+ const next = { ...payload };
194
+ const ignoredKeys: string[] = [];
195
+ for (const key of RUNTIME_OWNED_ENVELOPE_KEYS) {
196
+ if (key in next) {
197
+ delete next[key];
198
+ ignoredKeys.push(key);
199
+ }
200
+ }
201
+ if (isPlainObject(next.state)) {
202
+ const state = { ...(next.state as Record<string, unknown>) };
203
+ for (const key of RECORDER_OWNED_STATE_KEYS) {
204
+ if (key in state) {
205
+ delete state[key];
206
+ ignoredKeys.push(`state.${key}`);
207
+ }
208
+ }
209
+ next.state = state;
210
+ }
211
+ return { payload: next, ignoredKeys };
212
+ }
213
+
214
+ /**
215
+ * Self-heal a poisoned merge base: a persisted `state.intent_contract` that
216
+ * fails canonical validation can only come from a pre-guard poisoned write
217
+ * (the recorder always persists valid manifests). Left in place it makes
218
+ * every merge throw, bricking the interview. Drop it (and any equally
219
+ * unverifiable intent_review) from the base and report the repair.
220
+ */
221
+ function healPoisonedIntentContract(base: Record<string, unknown>): {
222
+ base: Record<string, unknown>;
223
+ healed: boolean;
224
+ } {
225
+ if (!isPlainObject(base.state)) return { base, healed: false };
226
+ const state = base.state as Record<string, unknown>;
227
+ if (state.intent_contract === undefined) return { base, healed: false };
228
+ try {
229
+ assertDeepInterviewIntentManifest(state.intent_contract);
230
+ return { base, healed: false };
231
+ } catch {
232
+ const healedState = { ...state };
233
+ delete healedState.intent_contract;
234
+ delete healedState.intent_review;
235
+ return { base: { ...base, state: healedState }, healed: true };
236
+ }
237
+ }
238
+
239
+ function parseTransition(raw: string | undefined): DeepInterviewStageTransition {
240
+ if (!raw || !(DEEP_INTERVIEW_STAGE_TRANSITIONS as readonly string[]).includes(raw)) {
241
+ throw new DeepInterviewStageError(
242
+ "DI_STAGE_USAGE",
243
+ `--for must be one of: ${DEEP_INTERVIEW_STAGE_TRANSITIONS.join(", ")}`,
244
+ );
245
+ }
246
+ return raw as DeepInterviewStageTransition;
247
+ }
248
+
249
+ // -----------------------------------------------------------------------------
250
+ // Core-schema validation (flexible by design: bounds + envelope shape only)
251
+ // -----------------------------------------------------------------------------
252
+
253
+ /**
254
+ * Validate only what the write gate and durability contract require:
255
+ * JSON-serializable, bounded total size, bounded free-text prose fields, and no
256
+ * attempt to smuggle envelope-reserved keys as interview state. Free-form fields
257
+ * (rounds, facts, notes, anything unknown) pass through — flexibility is the
258
+ * contract; the merge preserves them verbatim.
259
+ */
260
+ function assertCorePayloadSchema(payload: Record<string, unknown>): void {
261
+ try {
262
+ assertDeepInterviewStructuredResponseWithinLimit(payload);
263
+ assertDeepInterviewEnvelopeInputLimits(normalizeDeepInterviewEnvelope(payload) as Record<string, unknown>);
264
+ } catch (error) {
265
+ throw new DeepInterviewStageError(
266
+ "DI_STAGE_INPUT_INVALID",
267
+ error instanceof Error ? error.message : String(error),
268
+ );
269
+ }
270
+ }
271
+
272
+ // -----------------------------------------------------------------------------
273
+ // Draft persistence
274
+ // -----------------------------------------------------------------------------
275
+
276
+ type DraftReadResult =
277
+ | { kind: "absent" }
278
+ | { kind: "corrupt"; error: string }
279
+ | { kind: "valid"; draft: DeepInterviewStageDraft };
280
+
281
+ async function readDraft(cwd: string, sessionId: string): Promise<DraftReadResult> {
282
+ const draftPath = deepInterviewDraftPath(cwd, sessionId);
283
+ let raw: string;
284
+ try {
285
+ raw = await fs.readFile(draftPath, "utf-8");
286
+ } catch (error) {
287
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { kind: "absent" };
288
+ return { kind: "corrupt", error: error instanceof Error ? error.message : String(error) };
289
+ }
290
+ let parsed: unknown;
291
+ try {
292
+ parsed = JSON.parse(raw);
293
+ } catch (error) {
294
+ return { kind: "corrupt", error: error instanceof Error ? error.message : String(error) };
295
+ }
296
+ if (
297
+ !isPlainObject(parsed) ||
298
+ parsed.version !== DRAFT_VERSION ||
299
+ typeof parsed.draft_id !== "string" ||
300
+ typeof parsed.session_id !== "string" ||
301
+ typeof parsed.transition !== "string" ||
302
+ !(DEEP_INTERVIEW_STAGE_TRANSITIONS as readonly string[]).includes(parsed.transition) ||
303
+ typeof parsed.staged_against_revision !== "number" ||
304
+ typeof parsed.staged_against_sha256 !== "string" ||
305
+ !isPlainObject(parsed.payload)
306
+ ) {
307
+ return { kind: "corrupt", error: "draft file does not match the staged-draft shape" };
308
+ }
309
+ return { kind: "valid", draft: parsed as unknown as DeepInterviewStageDraft };
310
+ }
311
+
312
+ /**
313
+ * Remove the session draft only when it still holds `draftId`. Prevents a stale
314
+ * handle (e.g. a concurrent apply/discard that already consumed the draft and a
315
+ * new one was staged) from deleting a draft it never read.
316
+ */
317
+ async function removeDraftIfMatches(cwd: string, sessionId: string, draftId?: string): Promise<boolean> {
318
+ if (draftId !== undefined) {
319
+ const current = await readDraft(cwd, sessionId);
320
+ if (current.kind === "valid" && current.draft.draft_id !== draftId) return false;
321
+ if (current.kind === "absent") return false;
322
+ }
323
+ try {
324
+ await fs.rm(deepInterviewDraftPath(cwd, sessionId));
325
+ return true;
326
+ } catch (error) {
327
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
328
+ throw error;
329
+ }
330
+ }
331
+
332
+ // -----------------------------------------------------------------------------
333
+ // State reading + merge preview
334
+ // -----------------------------------------------------------------------------
335
+
336
+ interface CurrentState {
337
+ value: Record<string, unknown>;
338
+ revision: number;
339
+ sha256: string;
340
+ exists: boolean;
341
+ }
342
+
343
+ async function readCurrentState(cwd: string, sessionId: string): Promise<CurrentState> {
344
+ const read = await readExistingStateForMutation(statePathFor(cwd, sessionId));
345
+ if (read.kind === "corrupt") {
346
+ throw new DeepInterviewStageError(
347
+ "DI_STAGE_STATE_CORRUPT",
348
+ `deep-interview state is corrupt or tampered: ${read.error}`,
349
+ 'repair or clear it with `skc state clear --force --mode deep-interview`, then re-seed with `skc deep-interview "<idea>"`',
350
+ );
351
+ }
352
+ if (read.kind === "absent")
353
+ return { value: {}, revision: 0, sha256: workflowEnvelopeContentSha256({}), exists: false };
354
+ return {
355
+ value: read.value,
356
+ revision: persistedStateRevision(read.value),
357
+ sha256: workflowEnvelopeContentSha256(read.value),
358
+ exists: true,
359
+ };
360
+ }
361
+
362
+ /** Both anchors must hold: revision (fast path) AND content sha (writers that skip revision stamping). */
363
+ function draftIsStale(draft: DeepInterviewStageDraft, current: CurrentState): boolean {
364
+ return draft.staged_against_revision !== current.revision || draft.staged_against_sha256 !== current.sha256;
365
+ }
366
+
367
+ /**
368
+ * Lossless keyed merge for `state.established_facts` under the staged surface.
369
+ * The generic envelope merge replaces the whole facts array; the staged contract
370
+ * is delta-only, so a one-fact patch must never erase prior confirmed/disputed
371
+ * facts (they carry the deterministic-floor evidence). Facts with an `id` merge
372
+ * field-wise by id; facts without an id are appended with exact-duplicate dedup.
373
+ * Staged deltas can never hard-delete a fact — dispute/supersede instead.
374
+ */
375
+ function mergeEstablishedFacts(existing: readonly unknown[], incoming: readonly unknown[]): Record<string, unknown>[] {
376
+ const result: Record<string, unknown>[] = [];
377
+ const indexById = new Map<string, number>();
378
+ const add = (value: unknown): void => {
379
+ if (!isPlainObject(value)) return;
380
+ const id = typeof value.id === "string" && value.id.trim() !== "" ? value.id : undefined;
381
+ if (id !== undefined) {
382
+ const existingIndex = indexById.get(id);
383
+ if (existingIndex === undefined) {
384
+ indexById.set(id, result.length);
385
+ result.push({ ...value });
386
+ } else {
387
+ result[existingIndex] = { ...result[existingIndex], ...value };
388
+ }
389
+ return;
390
+ }
391
+ if (result.some(item => JSON.stringify(item) === JSON.stringify(value))) return;
392
+ result.push({ ...value });
393
+ };
394
+ for (const fact of existing) add(fact);
395
+ for (const fact of incoming) add(fact);
396
+ return result;
397
+ }
398
+
399
+ /**
400
+ * The single merge both `check` and `apply` execute. `check` reports its result;
401
+ * `apply` persists it. Divergence between the two is structurally impossible.
402
+ *
403
+ * Ambiguity is runtime-owned: after the merge, `current_ambiguity` is derived
404
+ * from the latest scored round, and the deterministic floor is recomputed and
405
+ * clamped via `applyAmbiguityFloorToEnvelope` — an agent-supplied
406
+ * `current_ambiguity` or under-reported round score is advisory input only and
407
+ * can never under-report below what persisted evidence supports.
408
+ */
409
+ function computeMergedEnvelope(
410
+ current: Record<string, unknown>,
411
+ draft: DeepInterviewStageDraft,
412
+ nowIso: string,
413
+ ): Record<string, unknown> {
414
+ // A poisoned (unverifiable) intent contract in the persisted base would make
415
+ // every merge throw forever; heal it instead of bricking the interview.
416
+ const { base: healedCurrent, healed } = healPoisonedIntentContract(current);
417
+ let merged: Record<string, unknown>;
418
+ try {
419
+ merged = mergeDeepInterviewEnvelope(healedCurrent, draft.payload) as Record<string, unknown>;
420
+ } catch (error) {
421
+ throw new DeepInterviewStageError(
422
+ "DI_STAGE_MERGE_REJECTED",
423
+ `staged payload violates a core invariant: ${error instanceof Error ? error.message : String(error)}`,
424
+ "fix the payload and re-stage (`skc deep-interview discard` then `stage`)",
425
+ );
426
+ }
427
+ if (healed) merged.intent_contract_healed_at = nowIso;
428
+ merged.skill = "deep-interview";
429
+ merged.active = true;
430
+ merged.updated_at = nowIso;
431
+ merged.version = WORKFLOW_STATE_VERSION;
432
+ if (typeof merged.current_phase !== "string" || !merged.current_phase) merged.current_phase = "interviewing";
433
+ merged.session_id = draft.session_id;
434
+ // Staged facts are deltas: re-merge against the prior facts losslessly so a
435
+ // one-fact patch cannot erase confirmed/disputed history (#3387 finding 2).
436
+ const mergedState = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : undefined;
437
+ const priorState = isPlainObject(current.state) ? (current.state as Record<string, unknown>) : undefined;
438
+ if (mergedState && priorState && Array.isArray(priorState.established_facts)) {
439
+ mergedState.established_facts = mergeEstablishedFacts(
440
+ priorState.established_facts,
441
+ Array.isArray(mergedState.established_facts) ? mergedState.established_facts : [],
442
+ );
443
+ }
444
+ merged = deriveRuntimeAmbiguity(merged, current);
445
+ try {
446
+ assertDeepInterviewEnvelopeInputLimits(merged);
447
+ } catch (error) {
448
+ throw new DeepInterviewStageError(
449
+ "DI_STAGE_MERGE_REJECTED",
450
+ `merged state violates a bounded-input invariant: ${error instanceof Error ? error.message : String(error)}`,
451
+ "fix the payload and re-stage (`skc deep-interview discard` then `stage`)",
452
+ );
453
+ }
454
+ return merged;
455
+ }
456
+
457
+ /**
458
+ * Derive `state.current_ambiguity` — the CLI, not the agent, owns the effective
459
+ * ambiguity. A staged `state.current_ambiguity` is never trusted directly:
460
+ * - with a valid latest scored round (finite numeric `round` AND finite
461
+ * `ambiguity`), the value derives from that round;
462
+ * - with no valid scored evidence, the PRIOR persisted value is retained (a
463
+ * fresh interview keeps its seeded 1.0 — a staged 0.01 cannot survive);
464
+ * then the deterministic floor is recomputed and clamped.
465
+ */
466
+ function deriveRuntimeAmbiguity(
467
+ merged: Record<string, unknown>,
468
+ previousState: Record<string, unknown>,
469
+ ): Record<string, unknown> {
470
+ const state = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : undefined;
471
+ if (state) {
472
+ const rounds = Array.isArray(state.rounds) ? state.rounds.filter(isPlainObject) : [];
473
+ let latestScored: Record<string, unknown> | undefined;
474
+ for (const round of rounds) {
475
+ if (round.lifecycle !== "scored") continue;
476
+ if (typeof round.ambiguity !== "number" || !Number.isFinite(round.ambiguity)) continue;
477
+ if (typeof round.round !== "number" || !Number.isFinite(round.round)) continue;
478
+ if (!latestScored || round.round >= (latestScored.round as number)) latestScored = round;
479
+ }
480
+ if (latestScored) {
481
+ state.current_ambiguity = latestScored.ambiguity;
482
+ } else {
483
+ // No valid scored evidence in the merged state: the staged value is
484
+ // discarded and the prior runtime-owned value (seed default 1.0) holds.
485
+ const prior = isPlainObject(previousState.state)
486
+ ? (previousState.state as Record<string, unknown>).current_ambiguity
487
+ : undefined;
488
+ if (typeof prior === "number" && Number.isFinite(prior)) state.current_ambiguity = prior;
489
+ else delete state.current_ambiguity;
490
+ }
491
+ }
492
+ return applyAmbiguityFloorToEnvelope(merged).envelope as Record<string, unknown>;
493
+ }
494
+
495
+ // -----------------------------------------------------------------------------
496
+ // Verbs
497
+ // -----------------------------------------------------------------------------
498
+
499
+ /**
500
+ * One explicit session boundary for every staged verb: `--session-id` flag,
501
+ * payload `session_id` (stage only), or `SKC_SESSION_ID`. Mutating verbs never
502
+ * fall back to latest-session auto-detect.
503
+ */
504
+ function resolveStageSession(args: readonly string[], cwd: string, payloadSessionId?: unknown): string {
505
+ const session = resolveSkcSessionForWrite(cwd, {
506
+ flagValue: flagValue(args, "--session-id"),
507
+ payloadSessionId,
508
+ envSessionId: process.env.SKC_SESSION_ID,
509
+ });
510
+ return session.skcSessionId;
511
+ }
512
+
513
+ async function handleStage(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
514
+ const rawInput = flagValue(args, "--input");
515
+ if (rawInput === undefined || rawInput === "") {
516
+ throw new DeepInterviewStageError("DI_STAGE_USAGE", "--input '<json>' (or @file) is required for stage");
517
+ }
518
+ const rawPayload = await parseJsonInput(rawInput, cwd);
519
+ const transition = parseTransition(flagValue(args, "--for"));
520
+ const sessionId = resolveStageSession(args, cwd, rawPayload.session_id);
521
+ const { payload, ignoredKeys } = sanitizeStagedPayload(rawPayload);
522
+ assertCorePayloadSchema(payload);
523
+
524
+ const statePath = statePathFor(cwd, sessionId);
525
+ return withWorkflowStateLock(
526
+ statePath,
527
+ async () => {
528
+ const existingDraft = await readDraft(cwd, sessionId);
529
+ if (existingDraft.kind === "valid") {
530
+ throw new DeepInterviewStageError(
531
+ "DI_STAGE_DRAFT_EXISTS",
532
+ `a staged draft already exists (draft_id=${existingDraft.draft.draft_id}, transition=${existingDraft.draft.transition}, created_at=${existingDraft.draft.created_at})`,
533
+ "apply it (`skc deep-interview apply`) or discard it (`skc deep-interview discard`) before staging again",
534
+ );
535
+ }
536
+ // A corrupt draft never blocks staging: it cannot be applied anyway, so
537
+ // staging over it is the self-healing path.
538
+ const current = await readCurrentState(cwd, sessionId);
539
+ if (!current.exists && transition !== "initialize-context") {
540
+ throw new DeepInterviewStageError(
541
+ "DI_STAGE_STATE_MISSING",
542
+ `no deep-interview state exists for session ${sessionId}; only --for initialize-context may stage against absent state`,
543
+ 'seed the interview first with `skc deep-interview "<idea>"` or stage --for initialize-context',
544
+ );
545
+ }
546
+ const nowIso = new Date().toISOString();
547
+ const draft: DeepInterviewStageDraft = {
548
+ version: DRAFT_VERSION,
549
+ draft_id: randomUUID(),
550
+ session_id: sessionId,
551
+ transition,
552
+ staged_against_revision: current.revision,
553
+ staged_against_sha256: current.sha256,
554
+ payload,
555
+ created_at: nowIso,
556
+ };
557
+ // Fail-closed preview at stage time: reject payloads that could never apply.
558
+ computeMergedEnvelope(current.value, draft, nowIso);
559
+ await writeJsonAtomic(deepInterviewDraftPath(cwd, sessionId), draft, {
560
+ cwd,
561
+ audit: {
562
+ category: "state",
563
+ verb: "stage-draft",
564
+ owner: "skc-runtime",
565
+ skill: "deep-interview",
566
+ sessionId,
567
+ },
568
+ });
569
+ await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-stage" });
570
+ return {
571
+ ok: true,
572
+ verb: "stage",
573
+ draft_id: draft.draft_id,
574
+ transition,
575
+ session_id: sessionId,
576
+ staged_against_revision: draft.staged_against_revision,
577
+ draft_path: deepInterviewDraftPath(cwd, sessionId),
578
+ ...(ignoredKeys.length > 0 ? { ignored_runtime_owned_keys: ignoredKeys } : {}),
579
+ };
580
+ },
581
+ { cwd },
582
+ );
583
+ }
584
+
585
+ function requireDraftRead(read: DraftReadResult, sessionId: string): DeepInterviewStageDraft {
586
+ if (read.kind === "absent") {
587
+ throw new DeepInterviewStageError(
588
+ "DI_STAGE_NO_DRAFT",
589
+ `no staged draft exists for session ${sessionId}`,
590
+ "stage one first: `skc deep-interview stage --for <transition> --input '<json>'`",
591
+ );
592
+ }
593
+ if (read.kind === "corrupt") {
594
+ throw new DeepInterviewStageError(
595
+ "DI_STAGE_DRAFT_CORRUPT",
596
+ `the staged draft is unreadable: ${read.error}`,
597
+ "discard it (`skc deep-interview discard`) and re-stage",
598
+ );
599
+ }
600
+ return read.draft;
601
+ }
602
+
603
+ async function handleCheck(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
604
+ const sessionId = resolveStageSession(args, cwd);
605
+ const draft = requireDraftRead(await readDraft(cwd, sessionId), sessionId);
606
+ const current = await readCurrentState(cwd, sessionId);
607
+ const summaryBase = {
608
+ verb: "check",
609
+ draft_id: draft.draft_id,
610
+ transition: draft.transition,
611
+ session_id: sessionId,
612
+ staged_against_revision: draft.staged_against_revision,
613
+ current_revision: current.revision,
614
+ };
615
+ if (draftIsStale(draft, current)) {
616
+ return {
617
+ ...summaryBase,
618
+ ok: false,
619
+ code: "DI_STAGE_REVISION_CONFLICT",
620
+ recovery: "state moved since staging; discard and re-stage against current state",
621
+ };
622
+ }
623
+ const merged = computeMergedEnvelope(current.value, draft, new Date().toISOString());
624
+ const state = isPlainObject(merged.state) ? merged.state : {};
625
+ return {
626
+ ...summaryBase,
627
+ ok: true,
628
+ would_apply: true,
629
+ result_phase: merged.current_phase,
630
+ result_round_count: Array.isArray(state.rounds) ? state.rounds.length : 0,
631
+ result_fact_count: Array.isArray(state.established_facts) ? state.established_facts.length : 0,
632
+ ...(typeof state.current_ambiguity === "number" ? { result_ambiguity: state.current_ambiguity } : {}),
633
+ };
634
+ }
635
+
636
+ async function handleApply(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
637
+ const sessionId = resolveStageSession(args, cwd);
638
+ const statePath = statePathFor(cwd, sessionId);
639
+ return withWorkflowStateLock(
640
+ statePath,
641
+ async () => {
642
+ // Re-read the draft INSIDE the lock so a concurrent discard/stage cannot
643
+ // hand us a draft that no longer exists or was replaced.
644
+ const draft = requireDraftRead(await readDraft(cwd, sessionId), sessionId);
645
+ const current = await readCurrentState(cwd, sessionId);
646
+ // Replay safety: a prior apply that committed but crashed before draft
647
+ // removal leaves `last_applied_draft_id` in state. Recognize the commit,
648
+ // finish the cleanup, and settle as an idempotent no-op.
649
+ if (current.value.last_applied_draft_id === draft.draft_id) {
650
+ await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
651
+ return {
652
+ ok: true,
653
+ verb: "apply",
654
+ draft_id: draft.draft_id,
655
+ transition: draft.transition,
656
+ session_id: sessionId,
657
+ applied_revision: current.revision,
658
+ state_path: statePath,
659
+ already_applied: true,
660
+ };
661
+ }
662
+ if (draftIsStale(draft, current)) {
663
+ // CAS conflict: the draft can never legally apply, so auto-invalidate it.
664
+ await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
665
+ throw new DeepInterviewStageError(
666
+ "DI_STAGE_REVISION_CONFLICT",
667
+ `state moved since staging (revision ${draft.staged_against_revision} -> ${current.revision} or content changed); the draft was invalidated`,
668
+ "re-stage against current state: `skc deep-interview stage --for <transition> --input '<json>'`",
669
+ );
670
+ }
671
+ const nowIso = new Date().toISOString();
672
+ const merged = computeMergedEnvelope(current.value, draft, nowIso);
673
+ merged.last_applied_draft_id = draft.draft_id;
674
+ let appliedRevision: number;
675
+ try {
676
+ const written = await writeGuardedWorkflowEnvelopeAtomic(statePath, merged, {
677
+ cwd,
678
+ policy: "source",
679
+ expectedRevision: current.revision,
680
+ lockHeld: true,
681
+ receipt: {
682
+ cwd,
683
+ skill: "deep-interview",
684
+ owner: "skc-runtime",
685
+ command: `skc deep-interview apply (${draft.transition})`,
686
+ sessionId,
687
+ nowIso,
688
+ mutationId: draft.draft_id,
689
+ },
690
+ audit: {
691
+ category: "state",
692
+ verb: "apply-staged-transition",
693
+ owner: "skc-runtime",
694
+ skill: "deep-interview",
695
+ sessionId,
696
+ mutationId: draft.draft_id,
697
+ },
698
+ });
699
+ appliedRevision = written.revision;
700
+ } catch (error) {
701
+ if (error instanceof StateWriteConflictError) {
702
+ await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
703
+ throw new DeepInterviewStageError(
704
+ "DI_STAGE_REVISION_CONFLICT",
705
+ `state revision moved since staging; the draft was invalidated (${error.message})`,
706
+ "re-stage against current state: `skc deep-interview stage --for <transition> --input '<json>'`",
707
+ );
708
+ }
709
+ throw error;
710
+ }
711
+ // Post-commit cleanup is best-effort: the commit already happened, and a
712
+ // replay recognizes it via last_applied_draft_id instead of failing.
713
+ try {
714
+ await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
715
+ await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-stage", path: statePath });
716
+ } catch {
717
+ // Swallow: state is committed; the next apply/discard settles the draft.
718
+ }
719
+ await syncStageHud(cwd, sessionId, merged);
720
+ const appliedState = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : {};
721
+ return {
722
+ ok: true,
723
+ verb: "apply",
724
+ draft_id: draft.draft_id,
725
+ transition: draft.transition,
726
+ session_id: sessionId,
727
+ applied_revision: appliedRevision,
728
+ state_path: statePath,
729
+ ...(typeof appliedState.current_ambiguity === "number"
730
+ ? { current_ambiguity: appliedState.current_ambiguity }
731
+ : {}),
732
+ content_sha256: createHash("sha256").update(JSON.stringify(merged)).digest("hex").slice(0, 32),
733
+ };
734
+ },
735
+ { cwd },
736
+ );
737
+ }
738
+
739
+ async function handleDiscard(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
740
+ const sessionId = resolveStageSession(args, cwd);
741
+ const statePath = statePathFor(cwd, sessionId);
742
+ // Same lock as stage/apply: a discard racing an in-flight apply must not
743
+ // delete the draft mid-consumption or return a torn read.
744
+ return withWorkflowStateLock(
745
+ statePath,
746
+ async () => {
747
+ const read = await readDraft(cwd, sessionId);
748
+ const removed = await removeDraftIfMatches(
749
+ cwd,
750
+ sessionId,
751
+ read.kind === "valid" ? read.draft.draft_id : undefined,
752
+ );
753
+ return {
754
+ ok: true,
755
+ verb: "discard",
756
+ session_id: sessionId,
757
+ removed,
758
+ ...(read.kind === "valid" ? { draft_id: read.draft.draft_id, transition: read.draft.transition } : {}),
759
+ };
760
+ },
761
+ { cwd },
762
+ );
763
+ }
764
+
765
+ // -----------------------------------------------------------------------------
766
+ // Direct verbs: read / write / clear / handoff
767
+ // -----------------------------------------------------------------------------
768
+
769
+ async function handleRead(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
770
+ const sessionId = resolveStageSession(args, cwd);
771
+ const current = await readCurrentState(cwd, sessionId);
772
+ if (!current.exists) {
773
+ return {
774
+ ok: true,
775
+ verb: "read",
776
+ session_id: sessionId,
777
+ exists: false,
778
+ state_path: statePathFor(cwd, sessionId),
779
+ };
780
+ }
781
+ const draftRead = await readDraft(cwd, sessionId);
782
+ return {
783
+ ok: true,
784
+ verb: "read",
785
+ session_id: sessionId,
786
+ exists: true,
787
+ state_path: statePathFor(cwd, sessionId),
788
+ revision: current.revision,
789
+ content_sha256: current.sha256,
790
+ envelope: current.value,
791
+ ...(draftRead.kind === "valid"
792
+ ? {
793
+ pending_draft: {
794
+ draft_id: draftRead.draft.draft_id,
795
+ transition: draftRead.draft.transition,
796
+ created_at: draftRead.draft.created_at,
797
+ },
798
+ }
799
+ : {}),
800
+ };
801
+ }
802
+
803
+ /**
804
+ * Direct write: an immediate stage+apply of one sanitized JSON payload —
805
+ * incremental merge by default, whole-state replacement with `--reset` (the
806
+ * locked intent contract survives a reset through the merge's immutability
807
+ * guard). Uses the same lock, sanitizer, merge, ambiguity derivation, and
808
+ * guarded revision-stamping writer as the staged path, so `write` cannot
809
+ * express anything `stage`+`apply` could not.
810
+ */
811
+ async function handleWrite(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
812
+ const rawInput = flagValue(args, "--input");
813
+ if (rawInput === undefined || rawInput === "") {
814
+ throw new DeepInterviewStageError("DI_STAGE_USAGE", "--input '<json>' (or @file) is required for write");
815
+ }
816
+ const rawPayload = await parseJsonInput(rawInput, cwd);
817
+ const sessionId = resolveStageSession(args, cwd, rawPayload.session_id);
818
+ const reset = hasFlag(args, "--reset");
819
+ const { payload, ignoredKeys } = sanitizeStagedPayload(rawPayload);
820
+ assertCorePayloadSchema(payload);
821
+
822
+ const statePath = statePathFor(cwd, sessionId);
823
+ return withWorkflowStateLock(
824
+ statePath,
825
+ async () => {
826
+ const pendingDraft = await readDraft(cwd, sessionId);
827
+ if (pendingDraft.kind === "valid") {
828
+ throw new DeepInterviewStageError(
829
+ "DI_STAGE_DRAFT_EXISTS",
830
+ `a staged draft is pending (draft_id=${pendingDraft.draft.draft_id}); direct write would race it`,
831
+ "apply it (`skc deep-interview apply`) or discard it (`skc deep-interview discard`) first",
832
+ );
833
+ }
834
+ const current = await readCurrentState(cwd, sessionId);
835
+ const nowIso = new Date().toISOString();
836
+ const syntheticDraft: DeepInterviewStageDraft = {
837
+ version: DRAFT_VERSION,
838
+ draft_id: randomUUID(),
839
+ session_id: sessionId,
840
+ transition: "merge-state",
841
+ staged_against_revision: current.revision,
842
+ staged_against_sha256: current.sha256,
843
+ payload,
844
+ created_at: nowIso,
845
+ };
846
+ // --reset replaces: merge against an empty base but re-lock the intent
847
+ // contract from prior state through the merge's own immutability guard.
848
+ const base = reset
849
+ ? (() => {
850
+ const priorState = isPlainObject(current.value.state)
851
+ ? (current.value.state as Record<string, unknown>)
852
+ : {};
853
+ return priorState.intent_contract !== undefined
854
+ ? { state: { intent_contract: priorState.intent_contract, intent_contract_required: true } }
855
+ : {};
856
+ })()
857
+ : current.value;
858
+ const merged = computeMergedEnvelope(base as Record<string, unknown>, syntheticDraft, nowIso);
859
+ merged.last_applied_draft_id = syntheticDraft.draft_id;
860
+ const written = await writeGuardedWorkflowEnvelopeAtomic(statePath, merged, {
861
+ cwd,
862
+ policy: "source",
863
+ expectedRevision: current.revision,
864
+ lockHeld: true,
865
+ receipt: {
866
+ cwd,
867
+ skill: "deep-interview",
868
+ owner: "skc-runtime",
869
+ command: `skc deep-interview write${reset ? " --reset" : ""}`,
870
+ sessionId,
871
+ nowIso,
872
+ mutationId: syntheticDraft.draft_id,
873
+ },
874
+ audit: {
875
+ category: "state",
876
+ verb: reset ? "write-reset" : "write-incremental",
877
+ owner: "skc-runtime",
878
+ skill: "deep-interview",
879
+ sessionId,
880
+ mutationId: syntheticDraft.draft_id,
881
+ },
882
+ });
883
+ await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-stage", path: statePath });
884
+ await syncStageHud(cwd, sessionId, merged);
885
+ const writtenState = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : {};
886
+ return {
887
+ ok: true,
888
+ verb: "write",
889
+ mode: reset ? "reset" : "incremental",
890
+ session_id: sessionId,
891
+ applied_revision: written.revision,
892
+ state_path: statePath,
893
+ ...(typeof writtenState.current_ambiguity === "number"
894
+ ? { current_ambiguity: writtenState.current_ambiguity }
895
+ : {}),
896
+ ...(ignoredKeys.length > 0 ? { ignored_runtime_owned_keys: ignoredKeys } : {}),
897
+ };
898
+ },
899
+ { cwd },
900
+ );
901
+ }
902
+
903
+ /**
904
+ * Publish a committed envelope to the active-state/HUD mirror. Best-effort:
905
+ * the durable commit already happened; a projection failure must never fail
906
+ * the command (matches syncDeepInterviewHud in deep-interview-runtime.ts).
907
+ */
908
+ async function syncStageHud(cwd: string, sessionId: string, envelope: Record<string, unknown>): Promise<void> {
909
+ try {
910
+ const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
911
+ await syncSkillActiveState({
912
+ cwd,
913
+ skill: "deep-interview",
914
+ active: phase !== "complete",
915
+ phase,
916
+ sessionId,
917
+ source: "skc-deep-interview-native",
918
+ hud: deriveDeepInterviewHud(envelope, { phase }),
919
+ });
920
+ } catch {
921
+ // HUD sync is best-effort and must not change command semantics.
922
+ }
923
+ }
924
+
925
+ /** Thin lifecycle passthroughs: same runtime plumbing, skc deep-interview surface. */
926
+ async function handleLifecyclePassthrough(
927
+ verb: "clear" | "handoff",
928
+ args: readonly string[],
929
+ cwd: string,
930
+ ): Promise<DeepInterviewStageCommandResult> {
931
+ const forwarded =
932
+ verb === "clear"
933
+ ? ["clear", "--mode", "deep-interview", ...args]
934
+ : ["handoff", "--mode", "deep-interview", ...args];
935
+ return runNativeStateCommand([...forwarded], cwd);
936
+ }
937
+
938
+ // -----------------------------------------------------------------------------
939
+ // Dispatch
940
+ // -----------------------------------------------------------------------------
941
+
942
+ export const DEEP_INTERVIEW_STAGE_VERBS = [
943
+ "stage",
944
+ "check",
945
+ "apply",
946
+ "discard",
947
+ "read",
948
+ "write",
949
+ "clear",
950
+ "handoff",
951
+ ] as const;
952
+ export type DeepInterviewStageVerb = (typeof DEEP_INTERVIEW_STAGE_VERBS)[number];
953
+
954
+ export function isDeepInterviewStageVerb(value: string | undefined): value is DeepInterviewStageVerb {
955
+ return value !== undefined && (DEEP_INTERVIEW_STAGE_VERBS as readonly string[]).includes(value);
956
+ }
957
+
958
+ export async function runDeepInterviewStageCommand(
959
+ verb: DeepInterviewStageVerb,
960
+ args: readonly string[],
961
+ cwd = process.cwd(),
962
+ ): Promise<DeepInterviewStageCommandResult> {
963
+ const json = hasFlag(args, "--json");
964
+ try {
965
+ let summary: Record<string, unknown>;
966
+ switch (verb) {
967
+ case "stage":
968
+ summary = await handleStage(args, cwd);
969
+ break;
970
+ case "check":
971
+ summary = await handleCheck(args, cwd);
972
+ break;
973
+ case "apply":
974
+ summary = await handleApply(args, cwd);
975
+ break;
976
+ case "discard":
977
+ summary = await handleDiscard(args, cwd);
978
+ break;
979
+ case "read":
980
+ summary = await handleRead(args, cwd);
981
+ break;
982
+ case "write":
983
+ summary = await handleWrite(args, cwd);
984
+ break;
985
+ case "clear":
986
+ case "handoff":
987
+ return await handleLifecyclePassthrough(verb, args, cwd);
988
+ }
989
+ const status = summary.ok === false ? 3 : 0;
990
+ const stdout = json
991
+ ? `${JSON.stringify(summary)}\n`
992
+ : `${Object.entries(summary)
993
+ .map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`)
994
+ .join(" ")}\n`;
995
+ return { status, stdout };
996
+ } catch (error) {
997
+ const staged =
998
+ error instanceof DeepInterviewStageError
999
+ ? error
1000
+ : error instanceof SessionResolutionError
1001
+ ? new DeepInterviewStageError(
1002
+ "DI_STAGE_SESSION_REQUIRED",
1003
+ error.message,
1004
+ "pass --session-id, set SKC_SESSION_ID, or include session_id in the staged payload, then retry",
1005
+ )
1006
+ : undefined;
1007
+ if (staged) {
1008
+ const body = json
1009
+ ? `${JSON.stringify({ ok: false, code: staged.code, message: staged.message, ...(staged.recovery ? { recovery: staged.recovery } : {}) })}\n`
1010
+ : `${staged.code}: ${staged.message}${staged.recovery ? `\nrecovery: ${staged.recovery}` : ""}\n`;
1011
+ return { status: staged.exitStatus, stderr: body };
1012
+ }
1013
+ if (error instanceof CommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
1014
+ return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
1015
+ }
1016
+ }