@gajae-code/coding-agent 0.11.1 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/launch-disposition.d.ts +23 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +141 -11
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  54. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  55. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  56. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  57. package/dist/types/goals/runtime.d.ts +1 -0
  58. package/dist/types/goals/state.d.ts +8 -0
  59. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  60. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  61. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  62. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  63. package/dist/types/hindsight/state.d.ts +11 -4
  64. package/dist/types/lsp/lspmux.d.ts +5 -3
  65. package/dist/types/lsp/path-trust.d.ts +1 -0
  66. package/dist/types/main.d.ts +4 -2
  67. package/dist/types/memories/storage.d.ts +1 -0
  68. package/dist/types/modes/action-registry.d.ts +29 -0
  69. package/dist/types/modes/components/chrome.d.ts +14 -0
  70. package/dist/types/modes/components/command-palette.d.ts +27 -0
  71. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  72. package/dist/types/modes/components/index.d.ts +2 -1
  73. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  74. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  75. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  76. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  77. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  78. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  79. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  80. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  81. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  82. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  83. package/dist/types/modes/components/status-line.d.ts +1 -94
  84. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  85. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  86. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  87. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  88. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  89. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  90. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  91. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  92. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  93. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  94. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  95. package/dist/types/modes/interactive-mode.d.ts +19 -17
  96. package/dist/types/modes/jobs-observer.d.ts +2 -0
  97. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  98. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  99. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  100. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  101. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  102. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  103. package/dist/types/modes/types.d.ts +20 -13
  104. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  105. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  106. package/dist/types/sdk/broker/broker.d.ts +78 -12
  107. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  108. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  109. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  110. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  111. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  112. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  113. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  114. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  117. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  118. package/dist/types/sdk/bus/index.d.ts +29 -0
  119. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  120. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  121. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  122. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  123. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  124. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  125. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  126. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  127. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  128. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  129. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  130. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  131. package/dist/types/sdk/host/host.d.ts +2 -0
  132. package/dist/types/sdk/index.d.ts +1 -0
  133. package/dist/types/sdk/session-directory.d.ts +56 -0
  134. package/dist/types/sdk/session.d.ts +2 -0
  135. package/dist/types/session/agent-session.d.ts +82 -47
  136. package/dist/types/session/agent-storage.d.ts +7 -0
  137. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  138. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  139. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  140. package/dist/types/session/messages.d.ts +16 -0
  141. package/dist/types/session/session-manager-internal.d.ts +14 -0
  142. package/dist/types/session/session-manager.d.ts +82 -12
  143. package/dist/types/session/session-storage.d.ts +43 -3
  144. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  145. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  146. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  147. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  148. package/dist/types/system-prompt.d.ts +2 -11
  149. package/dist/types/task/executor.d.ts +3 -1
  150. package/dist/types/thinking.d.ts +7 -0
  151. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  152. package/dist/types/tools/ask.d.ts +64 -2
  153. package/dist/types/tools/bash.d.ts +3 -0
  154. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  155. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  156. package/dist/types/tools/cron.d.ts +2 -0
  157. package/dist/types/tools/fetch.d.ts +1 -0
  158. package/dist/types/tools/index.d.ts +30 -15
  159. package/dist/types/tools/output-meta.d.ts +1 -0
  160. package/dist/types/tools/read.d.ts +5 -0
  161. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  162. package/dist/types/tools/subagent.d.ts +1 -0
  163. package/dist/types/utils/event-bus.d.ts +1 -1
  164. package/dist/types/utils/file-mentions.d.ts +3 -3
  165. package/dist/types/web/search/index.d.ts +3 -1
  166. package/package.json +21 -9
  167. package/scripts/build-sdk-package-smoke.ts +34 -1
  168. package/scripts/generate-hotkeys-docs.ts +74 -0
  169. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  170. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  171. package/src/async/job-manager.ts +49 -6
  172. package/src/cli/config-cli.ts +75 -2
  173. package/src/cli/launch-disposition.ts +42 -0
  174. package/src/cli/notify-cli.ts +92 -26
  175. package/src/cli/session-picker.ts +20 -3
  176. package/src/cli/setup-cli.ts +1 -1
  177. package/src/cli.ts +1 -1
  178. package/src/commands/config.ts +1 -1
  179. package/src/commands/harness.ts +34 -106
  180. package/src/commands/launch.ts +6 -4
  181. package/src/commands/mcp-serve.ts +85 -8
  182. package/src/commands/mcp.ts +2 -2
  183. package/src/commands/notify.ts +8 -4
  184. package/src/commands/sdk.ts +61 -40
  185. package/src/commands/ultragoal.ts +4 -1
  186. package/src/config/atomic-yaml-patch.ts +80 -13
  187. package/src/config/file-lock.ts +105 -43
  188. package/src/config/keybindings.ts +64 -20
  189. package/src/config/model-auth.ts +9 -0
  190. package/src/config/model-bindings-applier.ts +139 -0
  191. package/src/config/model-discovery-manager.ts +224 -0
  192. package/src/config/model-equivalence.ts +66 -0
  193. package/src/config/model-registry.ts +169 -302
  194. package/src/config/model-resolver.ts +361 -369
  195. package/src/config/models-config-schema.ts +5 -4
  196. package/src/config/settings-schema.ts +214 -12
  197. package/src/config/settings.ts +458 -482
  198. package/src/coordinator-mcp/policy.ts +76 -11
  199. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  200. package/src/coordinator-mcp/question-state.ts +778 -0
  201. package/src/coordinator-mcp/server.ts +1702 -238
  202. package/src/debug/index.ts +1 -1
  203. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  204. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  205. package/src/discovery/helpers.ts +1 -50
  206. package/src/edit/index.ts +64 -0
  207. package/src/eval/py/env.ts +26 -0
  208. package/src/eval/py/kernel.ts +5 -3
  209. package/src/extensibility/extensions/loader.ts +9 -1
  210. package/src/extensibility/extensions/runner.ts +12 -3
  211. package/src/extensibility/extensions/types.ts +39 -1
  212. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  213. package/src/extensibility/hooks/runner.ts +2 -2
  214. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  215. package/src/extensibility/shared-events.ts +2 -0
  216. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  217. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  218. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  219. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  220. package/src/gjc-runtime/launch-tmux.ts +2 -5
  221. package/src/gjc-runtime/launch-worktree.ts +5 -5
  222. package/src/gjc-runtime/linux-proc.ts +71 -0
  223. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  224. package/src/gjc-runtime/session-resolution.ts +18 -3
  225. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  226. package/src/gjc-runtime/state-migrations.ts +1 -1
  227. package/src/gjc-runtime/state-runtime.ts +327 -249
  228. package/src/gjc-runtime/state-writer.ts +36 -40
  229. package/src/gjc-runtime/team-launch.ts +319 -0
  230. package/src/gjc-runtime/team-notify.ts +326 -0
  231. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  232. package/src/gjc-runtime/team-store.ts +828 -0
  233. package/src/gjc-runtime/team-workers.ts +962 -0
  234. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  235. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  236. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  237. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  238. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  239. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  240. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  241. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  242. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  243. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  244. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  245. package/src/goals/continuation-timeout-guard.ts +79 -0
  246. package/src/goals/runtime.ts +7 -5
  247. package/src/goals/state.ts +18 -0
  248. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  249. package/src/harness-control-plane/phase-rollup.ts +4 -7
  250. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  251. package/src/harness-control-plane/receipt-spool.ts +51 -7
  252. package/src/harness-control-plane/session-lease.ts +12 -130
  253. package/src/hindsight/backend.ts +26 -27
  254. package/src/hindsight/state.ts +101 -32
  255. package/src/hooks/skill-state.ts +17 -11
  256. package/src/internal-urls/docs-index.generated.ts +21 -18
  257. package/src/lsp/client.ts +1 -1
  258. package/src/lsp/config.ts +145 -26
  259. package/src/lsp/index.ts +1 -1
  260. package/src/lsp/lspmux.ts +39 -10
  261. package/src/lsp/path-trust.ts +81 -0
  262. package/src/main.ts +127 -30
  263. package/src/memories/index.ts +1 -0
  264. package/src/memories/storage.ts +6 -2
  265. package/src/modes/action-registry.ts +165 -0
  266. package/src/modes/components/chrome.ts +40 -0
  267. package/src/modes/components/command-palette.ts +168 -0
  268. package/src/modes/components/custom-editor.ts +1 -1
  269. package/src/modes/components/custom-provider-wizard.ts +51 -14
  270. package/src/modes/components/index.ts +2 -1
  271. package/src/modes/components/pet-selector.ts +11 -17
  272. package/src/modes/components/plan-preview-overlay.ts +295 -0
  273. package/src/modes/components/queue-mode-selector.ts +10 -27
  274. package/src/modes/components/queue-pane.ts +79 -0
  275. package/src/modes/components/read-tool-group.ts +13 -0
  276. package/src/modes/components/session-observer-overlay.ts +207 -772
  277. package/src/modes/components/sessions-dashboard.ts +181 -0
  278. package/src/modes/components/settings-selector.ts +1 -1
  279. package/src/modes/components/show-images-selector.ts +10 -24
  280. package/src/modes/components/status-line/types.ts +1 -1
  281. package/src/modes/components/status-line.ts +1 -969
  282. package/src/modes/components/tasks-pane.ts +73 -0
  283. package/src/modes/components/theme-selector.ts +11 -31
  284. package/src/modes/components/tool-execution.ts +22 -0
  285. package/src/modes/components/tool-status-header.ts +1056 -0
  286. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  287. package/src/modes/controllers/btw-controller.ts +1 -0
  288. package/src/modes/controllers/event-controller.ts +3 -2
  289. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  290. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  291. package/src/modes/controllers/input-controller.ts +764 -229
  292. package/src/modes/controllers/mode-gate.ts +24 -0
  293. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  294. package/src/modes/controllers/selector-controller.ts +225 -10
  295. package/src/modes/controllers/stt-controller.ts +125 -0
  296. package/src/modes/interactive-mode.ts +289 -1150
  297. package/src/modes/jobs-observer.ts +3 -0
  298. package/src/modes/prompt-action-autocomplete.ts +7 -0
  299. package/src/modes/runtime-init.ts +8 -0
  300. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  301. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  302. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  303. package/src/modes/tasks-aggregator.ts +199 -0
  304. package/src/modes/transcript-item-registry.ts +229 -0
  305. package/src/modes/types.ts +45 -18
  306. package/src/modes/utils/ui-helpers.ts +17 -9
  307. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  308. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  309. package/src/prompts/agents/architect.md +2 -8
  310. package/src/prompts/agents/critic.md +2 -8
  311. package/src/prompts/agents/executor.md +3 -0
  312. package/src/prompts/agents/planner.md +4 -12
  313. package/src/prompts/system/custom-system-prompt.md +1 -23
  314. package/src/prompts/system/plan-mode-approved.md +8 -0
  315. package/src/prompts/system/system-prompt.md +17 -96
  316. package/src/prompts/tools/bash.md +4 -4
  317. package/src/prompts/tools/browser.md +15 -52
  318. package/src/prompts/tools/find.md +2 -2
  319. package/src/prompts/tools/hashline.md +13 -55
  320. package/src/prompts/tools/read.md +3 -6
  321. package/src/prompts/tools/search-tool-bm25.md +0 -2
  322. package/src/prompts/tools/search.md +2 -4
  323. package/src/prompts/tools/skill-discovery.md +1 -2
  324. package/src/prompts/tools/task.md +1 -1
  325. package/src/runtime-mcp/manager.ts +1 -1
  326. package/src/sdk/broker/broker.ts +206 -78
  327. package/src/sdk/broker/discovery.ts +1 -1
  328. package/src/sdk/broker/ensure.ts +3 -1
  329. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  330. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  331. package/src/sdk/broker/lifecycle.ts +1399 -142
  332. package/src/sdk/broker/process-incarnation.ts +5 -13
  333. package/src/sdk/broker/session-index.ts +145 -56
  334. package/src/sdk/broker/state-version.ts +21 -0
  335. package/src/sdk/broker/transport.ts +1 -1
  336. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  337. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  338. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  339. package/src/sdk/bus/config-commands.ts +5 -4
  340. package/src/sdk/bus/config.ts +98 -0
  341. package/src/sdk/bus/conversation-store.ts +5 -29
  342. package/src/sdk/bus/daemon-paths.ts +2 -0
  343. package/src/sdk/bus/html-format.ts +20 -3
  344. package/src/sdk/bus/index.ts +782 -94
  345. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  346. package/src/sdk/bus/natives-augment.d.ts +26 -1
  347. package/src/sdk/bus/notification-service.ts +168 -20
  348. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  349. package/src/sdk/bus/recent-activity.ts +188 -74
  350. package/src/sdk/bus/rich-document.ts +258 -0
  351. package/src/sdk/bus/rich-render.ts +62 -70
  352. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  353. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  354. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  355. package/src/sdk/bus/threaded-inbound.ts +8 -3
  356. package/src/sdk/bus/threaded-render.ts +57 -1
  357. package/src/sdk/bus/topic-registry.ts +120 -13
  358. package/src/sdk/host/control/dispatch.ts +14 -2
  359. package/src/sdk/host/control/operations.ts +6 -1
  360. package/src/sdk/host/host.ts +44 -2
  361. package/src/sdk/host/query/handlers.ts +20 -22
  362. package/src/sdk/host/query/revision-store.ts +80 -57
  363. package/src/sdk/index.ts +1 -0
  364. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  365. package/src/sdk/session-directory.ts +141 -0
  366. package/src/sdk/session.ts +462 -480
  367. package/src/session/agent-session.ts +1700 -583
  368. package/src/session/agent-storage.ts +18 -0
  369. package/src/session/cache-economics.ts +25 -8
  370. package/src/session/fallback-chain-controller.ts +53 -3
  371. package/src/session/internal/managed-session-scope.ts +2226 -0
  372. package/src/session/internal/managed-session-storage.ts +453 -0
  373. package/src/session/messages.ts +91 -2
  374. package/src/session/session-manager-internal.ts +33 -0
  375. package/src/session/session-manager.ts +1112 -333
  376. package/src/session/session-storage.ts +444 -39
  377. package/src/session/volatile-context-pruning.ts +78 -0
  378. package/src/setup/credential-auto-import.ts +2 -1
  379. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  380. package/src/setup/provider-onboarding.ts +36 -61
  381. package/src/setup/provider-presets.json +5 -0
  382. package/src/skill-state/active-state.ts +3 -13
  383. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  384. package/src/skill-state/workflow-state-contract.ts +6 -41
  385. package/src/slash-commands/builtin-registry.ts +46 -11
  386. package/src/system-prompt.ts +37 -44
  387. package/src/task/agents.ts +19 -1
  388. package/src/task/executor.ts +90 -6
  389. package/src/task/index.ts +36 -21
  390. package/src/thinking.ts +21 -6
  391. package/src/tool-discovery/tool-index.ts +2 -134
  392. package/src/tools/ask.ts +157 -21
  393. package/src/tools/ast-edit.ts +1 -1
  394. package/src/tools/bash.ts +38 -0
  395. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  396. package/src/tools/browser/tab-supervisor.ts +282 -36
  397. package/src/tools/computer.ts +10 -3
  398. package/src/tools/cron.ts +30 -16
  399. package/src/tools/eval.ts +1 -2
  400. package/src/tools/fetch.ts +11 -4
  401. package/src/tools/index.ts +76 -24
  402. package/src/tools/output-meta.ts +19 -7
  403. package/src/tools/read.ts +219 -64
  404. package/src/tools/resource-gc.ts +2 -2
  405. package/src/tools/search-tool-bm25.ts +11 -74
  406. package/src/tools/subagent.ts +43 -2
  407. package/src/utils/event-bus.ts +9 -4
  408. package/src/utils/file-mentions.ts +4 -4
  409. package/src/web/search/index.ts +3 -3
  410. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  411. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  412. package/src/discovery/plugin-dir-roots.ts +0 -28
  413. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -1,7 +1,8 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { Snowflake } from "@gajae-code/utils";
4
- import { type Goal, type GoalModeState, normalizeGoal } from "../goals/state";
4
+ import { type Goal, type GoalModeState, type GoalProvenance, normalizeGoal } from "../goals/state";
5
+
5
6
  import {
6
7
  buildSessionContext,
7
8
  loadEntriesFromFile,
@@ -27,6 +28,8 @@ export interface PendingGoalModeRequest {
27
28
  objective: string;
28
29
  createdAt: string;
29
30
  goalsPath?: string;
31
+ provenance: Extract<GoalProvenance, { source: "ultragoal" }>;
32
+
30
33
  /**
31
34
  * Session id that produced this request (from GJC_SESSION_ID). When present,
32
35
  * only the originating session may consume it, so concurrent sessions sharing
@@ -42,6 +45,7 @@ export type CurrentSessionGoalModeWriteResult =
42
45
 
43
46
  interface UltragoalPlanShape {
44
47
  gjcObjective?: unknown;
48
+ goals?: Array<{ id?: unknown }>;
45
49
  }
46
50
 
47
51
  function isEnoent(error: unknown): boolean {
@@ -70,7 +74,7 @@ export function isUltragoalCreateGoalsInvocation(args: readonly string[]): boole
70
74
  export async function readUltragoalGjcObjective(
71
75
  cwd: string,
72
76
  sessionId?: string | null,
73
- ): Promise<{ objective: string; goalsPath: string }> {
77
+ ): Promise<{ objective: string; goalsPath: string; provenance: Extract<GoalProvenance, { source: "ultragoal" }> }> {
74
78
  const session = sessionId?.trim()
75
79
  ? { gjcSessionId: sessionId.trim() }
76
80
  : await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
@@ -78,11 +82,21 @@ export async function readUltragoalGjcObjective(
78
82
  try {
79
83
  const plan = (await Bun.file(goalsPath).json()) as UltragoalPlanShape;
80
84
  const objective = typeof plan.gjcObjective === "string" ? plan.gjcObjective.trim() : "";
81
- return { objective: objective || DEFAULT_ULTRAGOAL_OBJECTIVE, goalsPath };
85
+ const goalId = typeof plan.goals?.[0]?.id === "string" ? plan.goals[0].id : "aggregate";
86
+ return {
87
+ objective: objective || DEFAULT_ULTRAGOAL_OBJECTIVE,
88
+ goalsPath,
89
+ provenance: { source: "ultragoal", runId: session.gjcSessionId, goalId },
90
+ };
82
91
  } catch (error) {
83
92
  if (isEnoent(error)) {
84
- return { objective: DEFAULT_ULTRAGOAL_OBJECTIVE, goalsPath };
93
+ return {
94
+ objective: DEFAULT_ULTRAGOAL_OBJECTIVE,
95
+ goalsPath,
96
+ provenance: { source: "ultragoal", runId: session.gjcSessionId, goalId: "aggregate" },
97
+ };
85
98
  }
99
+
86
100
  throw error;
87
101
  }
88
102
  }
@@ -92,6 +106,7 @@ export async function writePendingGoalModeRequest(input: {
92
106
  objective: string;
93
107
  goalsPath?: string;
94
108
  sessionId?: string | null;
109
+ provenance?: Extract<GoalProvenance, { source: "ultragoal" }>;
95
110
  }): Promise<PendingGoalModeRequest> {
96
111
  const objective = input.objective.trim();
97
112
  if (!objective) throw new Error("goal objective is required");
@@ -106,6 +121,8 @@ export async function writePendingGoalModeRequest(input: {
106
121
  objective,
107
122
  createdAt: new Date().toISOString(),
108
123
  goalsPath: input.goalsPath,
124
+ provenance: input.provenance ?? { source: "ultragoal", runId: sessionId, goalId: "aggregate" },
125
+
109
126
  ...(sessionId ? { sessionId } : {}),
110
127
  };
111
128
  const filePath = requestPath(input.cwd, sessionId);
@@ -125,7 +142,26 @@ function isNonTerminalGoal(goal: Goal | null): goal is Goal {
125
142
  return goal !== null && goal.status !== "complete" && goal.status !== "dropped";
126
143
  }
127
144
 
128
- function createGoalModeState(objective: string): GoalModeState {
145
+ function matchesGoalModeRequest(existingGoal: Goal, objective: string, provenance: Goal["provenance"]): boolean {
146
+ const existingProvenance = existingGoal.provenance;
147
+ if (existingProvenance?.source === "ultragoal" && provenance?.source === "ultragoal") {
148
+ return existingProvenance.runId === provenance.runId && existingProvenance.goalId === provenance.goalId;
149
+ }
150
+ // Legacy goals have no durable identity. Only an exact normalized objective can
151
+ // establish that they are the same goal.
152
+ return existingGoal.objective.trim() === objective;
153
+ }
154
+
155
+ function hasProvenDifferentDurablePlan(existingGoal: Goal, provenance: Goal["provenance"]): boolean {
156
+ const existingProvenance = existingGoal.provenance;
157
+ return (
158
+ existingProvenance?.source === "ultragoal" &&
159
+ provenance?.source === "ultragoal" &&
160
+ existingProvenance.runId !== provenance.runId
161
+ );
162
+ }
163
+
164
+ function createGoalModeState(objective: string, provenance: Goal["provenance"]): GoalModeState {
129
165
  const now = Date.now();
130
166
  const goal: Goal = {
131
167
  id: String(Snowflake.next()),
@@ -135,6 +171,7 @@ function createGoalModeState(objective: string): GoalModeState {
135
171
  timeUsedSeconds: 0,
136
172
  createdAt: now,
137
173
  updatedAt: now,
174
+ ...(provenance ? { provenance } : {}),
138
175
  };
139
176
  return { enabled: true, mode: "active", goal };
140
177
  }
@@ -151,6 +188,7 @@ function nextSessionEntryId(entries: readonly SessionEntry[]): string {
151
188
  export async function writeCurrentSessionGoalModeState(input: {
152
189
  sessionFile?: string | null;
153
190
  objective: string;
191
+ provenance?: Goal["provenance"];
154
192
  }): Promise<CurrentSessionGoalModeWriteResult> {
155
193
  const sessionFile = input.sessionFile?.trim();
156
194
  if (!sessionFile) return { status: "unavailable", reason: "missing_session_file" };
@@ -162,13 +200,22 @@ export async function writeCurrentSessionGoalModeState(input: {
162
200
  const entries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== "session");
163
201
  if (fileEntries.length === 0) return { status: "unavailable", reason: "empty_session_file" };
164
202
 
203
+ const requestedProvenance = input.provenance;
165
204
  const context = buildSessionContext(entries);
166
205
  const existingGoal = goalFromModeData(context.modeData);
167
206
  if ((context.mode === "goal" || context.mode === "goal_paused") && isNonTerminalGoal(existingGoal)) {
168
- return { status: "existing_goal", goal: existingGoal };
207
+ if (matchesGoalModeRequest(existingGoal, objective, requestedProvenance)) {
208
+ return { status: "existing_goal", goal: existingGoal };
209
+ }
210
+ // A legacy or user goal cannot establish that the incoming ultragoal request
211
+ // belongs to a different durable plan. Preserve it rather than overwriting
212
+ // active user work; only a distinct ultragoal run ID may replace it.
213
+ if (!hasProvenDifferentDurablePlan(existingGoal, requestedProvenance)) {
214
+ return { status: "existing_goal", goal: existingGoal };
215
+ }
169
216
  }
170
217
 
171
- const state = createGoalModeState(objective);
218
+ const state = createGoalModeState(objective, requestedProvenance);
172
219
  const entry: ModeChangeEntry = {
173
220
  type: "mode_change",
174
221
  id: nextSessionEntryId(entries),
@@ -213,7 +260,7 @@ export async function consumePendingGoalModeRequest(
213
260
  // (do not delete it) so its rightful owner can still pick it up. Legacy/unscoped
214
261
  // requests (no sessionId) remain consumable by any session in this cwd.
215
262
  const ownerSessionId = typeof candidate.sessionId === "string" ? candidate.sessionId.trim() : "";
216
- if (ownerSessionId && ownerSessionId !== (currentSessionId?.trim() ?? "")) {
263
+ if (ownerSessionId && ownerSessionId !== session.gjcSessionId) {
217
264
  return null;
218
265
  }
219
266
  await removeFileAudited(filePath, {
@@ -5,6 +5,7 @@ import * as path from "node:path";
5
5
  import { VERSION } from "@gajae-code/utils/dirs";
6
6
  import { safeStderrWrite } from "@gajae-code/utils/safe-stderr";
7
7
  import type { Args } from "../cli/args";
8
+ import { readLinuxProcStartTimeSync } from "./linux-proc";
8
9
  import { tmuxRuntimeSessionPath } from "./session-layout";
9
10
  import {
10
11
  GJC_COORDINATOR_SESSION_BRANCH_ENV,
@@ -1108,11 +1109,7 @@ function defaultOwnerIsolationProbe(
1108
1109
  let startTime: string | undefined;
1109
1110
  let cgroupText: string | null = null;
1110
1111
  try {
1111
- const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
1112
- startTime = stat
1113
- .slice(stat.lastIndexOf(")") + 2)
1114
- .trim()
1115
- .split(/\s+/)[19];
1112
+ startTime = readLinuxProcStartTimeSync(pid) ?? undefined;
1116
1113
  cgroupText = fs.readFileSync(`/proc/${pid}/cgroup`, "utf8");
1117
1114
  } catch {}
1118
1115
  if (!startTime) return { state: "unverifiable" };
@@ -299,14 +299,14 @@ export function ensureReusableNodeModules(sourceRoot: string, worktreePath: stri
299
299
  return "symlink";
300
300
  }
301
301
 
302
- export function prepareLaunchWorktree(
303
- cwd: string,
304
- args: string[],
305
- ): {
302
+ /** Result of {@link prepareLaunchWorktree}: the effective working directory, remaining args, and resolved worktree plan. */
303
+ export interface PreparedLaunchWorktree {
306
304
  cwd: string;
307
305
  args: string[];
308
306
  worktree: GjcLaunchWorktreeResult | { enabled: false };
309
- } {
307
+ }
308
+
309
+ export function prepareLaunchWorktree(cwd: string, args: string[]): PreparedLaunchWorktree {
310
310
  const parsed = parseLaunchWorktreeMode(args);
311
311
  const planned = planLaunchWorktree(cwd, parsed.mode);
312
312
  const ensured = ensureLaunchWorktree(planned);
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared helpers for reading Linux `/proc/<pid>/stat` process start time.
3
+ *
4
+ * The `comm` field (field 2, wrapped in parentheses) may itself contain spaces
5
+ * and parentheses, so the only robust anchor is the *last* `)` in the stat
6
+ * string. Field 22 (the process start time in clock ticks since boot) is the
7
+ * 20th whitespace-separated token after that closing paren (index 19).
8
+ *
9
+ * Every caller previously parsed this format independently, with subtly
10
+ * different failure handling. This module fails closed: any malformed input
11
+ * (missing `)`, non-numeric field 22, unreadable `/proc` file, non-Linux
12
+ * platform) yields `null` rather than an inconsistent sentinel.
13
+ */
14
+
15
+ import * as nodeFsSync from "node:fs";
16
+ import * as nodeFs from "node:fs/promises";
17
+
18
+ /**
19
+ * Parse field 22 (process start time, in clock ticks since boot) from one
20
+ * `/proc/<pid>/stat` record. Returns the raw numeric token, or `null` when the
21
+ * record shape is malformed or field 22 is absent/non-numeric.
22
+ */
23
+ export function parseLinuxProcStartTime(stat: string | null | undefined): string | null {
24
+ if (!stat || stat.includes("\0") || stat.includes("\r")) return null;
25
+ const record = stat.endsWith("\n") ? stat.slice(0, -1) : stat;
26
+ if (!record || record.includes("\n")) return null;
27
+
28
+ const open = record.indexOf(" (");
29
+ const close = record.lastIndexOf(")");
30
+ if (open < 1 || close <= open + 1 || !/^[1-9]\d*$/.test(record.slice(0, open))) return null;
31
+
32
+ const suffix = record.slice(close + 1);
33
+ if (!/^[ \t]+/.test(suffix)) return null;
34
+ const fields = suffix.trim().split(/[ \t]+/);
35
+ if (fields.length < 20 || !/^[RSDTtXZPI]$/.test(fields[0])) return null;
36
+
37
+ const startTime = fields[19];
38
+ return /^\d+$/.test(startTime) ? startTime : null;
39
+ }
40
+
41
+ /**
42
+ * Read `/proc/<pid>/stat` synchronously and return the parsed start time.
43
+ * Returns `null` on non-Linux platforms, unreadable files, or malformed input.
44
+ */
45
+ export function readLinuxProcStartTimeSync(pid: number): string | null {
46
+ if (process.platform !== "linux") return null;
47
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
48
+ let stat: string;
49
+ try {
50
+ stat = nodeFsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
51
+ } catch {
52
+ return null;
53
+ }
54
+ return parseLinuxProcStartTime(stat);
55
+ }
56
+
57
+ /**
58
+ * Read `/proc/<pid>/stat` asynchronously and return the parsed start time.
59
+ * Returns `null` on non-Linux platforms, unreadable files, or malformed input.
60
+ */
61
+ export async function readLinuxProcStartTime(pid: number): Promise<string | null> {
62
+ if (process.platform !== "linux") return null;
63
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
64
+ let stat: string;
65
+ try {
66
+ stat = await nodeFs.readFile(`/proc/${pid}/stat`, "utf8");
67
+ } catch {
68
+ return null;
69
+ }
70
+ return parseLinuxProcStartTime(stat);
71
+ }
@@ -19,9 +19,12 @@ import { runNativeStateCommand } from "./state-runtime";
19
19
  import {
20
20
  appendJsonlIdempotent,
21
21
  readExistingStateForMutation,
22
+ withWorkflowStateLock,
22
23
  writeArtifact,
23
24
  writeWorkflowEnvelopeAtomic,
24
25
  } from "./state-writer";
26
+ import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
27
+ import { getSkillManifest } from "./workflow-manifest";
25
28
 
26
29
  /**
27
30
  * Native implementation of `gjc ralplan`.
@@ -55,8 +58,6 @@ type RalplanStage = (typeof KNOWN_STAGES)[number];
55
58
  const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
56
59
  const KNOWN_CRITIC_KINDS = new Set(["openai-code"]);
57
60
 
58
- const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
59
-
60
61
  const SUBAGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
61
62
 
62
63
  const KNOWN_FALLBACK_REASONS = new Set([
@@ -68,12 +69,9 @@ const KNOWN_FALLBACK_REASONS = new Set([
68
69
  "missing_record",
69
70
  ]);
70
71
 
71
- class RalplanCommandError extends Error {
72
- constructor(
73
- public readonly exitStatus: number,
74
- message: string,
75
- ) {
76
- super(message);
72
+ class RalplanCommandError extends CommandError {
73
+ constructor(exitStatus: number, message: string) {
74
+ super(exitStatus, message);
77
75
  this.name = "RalplanCommandError";
78
76
  }
79
77
  }
@@ -95,16 +93,6 @@ const VALUE_FLAGS = new Set([
95
93
  "--fallback-receipt-path",
96
94
  ]);
97
95
 
98
- function flagValue(args: readonly string[], flag: string): string | undefined {
99
- const index = args.indexOf(flag);
100
- if (index < 0) return undefined;
101
- return args[index + 1];
102
- }
103
-
104
- function hasFlag(args: readonly string[], flag: string): boolean {
105
- return args.includes(flag);
106
- }
107
-
108
96
  export function isRalplanArtifactWriteInvocation(args: readonly string[]): boolean {
109
97
  return hasFlag(args, "--write");
110
98
  }
@@ -113,12 +101,6 @@ function isRalplanDoctorInvocation(args: readonly string[]): boolean {
113
101
  return args[0] === "doctor";
114
102
  }
115
103
 
116
- function assertSafePathComponent(value: string, label: string): void {
117
- if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
118
- throw new RalplanCommandError(2, `invalid path component for --${label}: ${value}`);
119
- }
120
- }
121
-
122
104
  function assertKnownStage(stage: string): asserts stage is RalplanStage {
123
105
  if (!(KNOWN_STAGES as readonly string[]).includes(stage)) {
124
106
  throw new RalplanCommandError(2, `unknown --stage: ${stage}. Expected one of: ${KNOWN_STAGES.join(", ")}.`);
@@ -204,61 +186,57 @@ async function readActiveRunId(cwd: string, sessionId: string): Promise<string |
204
186
  * undo Stop semantics. Every other phase advances to track the stage just
205
187
  * persisted so run-state stays coherent with the active ralplan stage.
206
188
  */
207
- const PHASE_LOCK = new Set([
208
- "final",
209
- "handoff",
210
- "complete",
211
- "completed",
212
- "failed",
213
- "cancelled",
214
- "canceled",
215
- "inactive",
216
- ]);
217
189
 
218
190
  /** Phase that keeps run-state coherent with the stage just written, preserving locked phases. */
219
191
  function advanceCurrentPhase(existingPhase: unknown, stage: RalplanStage): string {
220
192
  const current = typeof existingPhase === "string" ? existingPhase.trim() : "";
221
- if (current && PHASE_LOCK.has(current)) return current;
193
+ if (current && getSkillManifest("ralplan").phaseLock.includes(current)) return current;
222
194
  return stage;
223
195
  }
224
196
 
225
197
  async function persistActiveRunId(cwd: string, sessionId: string, runId: string, stage: RalplanStage): Promise<void> {
226
198
  const statePath = ralplanStatePath(cwd, sessionId);
227
- const existingRead = await readExistingStateForMutation(statePath);
228
- if (existingRead.kind === "corrupt") {
229
- throw new RalplanCommandError(
230
- 2,
231
- `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
232
- );
233
- }
234
- let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
235
-
236
- // A new run_id is a fresh run, not a stray write on the prior run: never inherit a
237
- // previous run's terminal/locked phase (which would start the new run already
238
- // "complete"/"handoff" and disarm the Stop hook). PHASE_LOCK only guards same-run writes.
239
- const isNewRun = existing.run_id !== runId;
240
- const nextPhase = isNewRun ? stage : advanceCurrentPhase(existing.current_phase, stage);
241
- if (
242
- existing.run_id === runId &&
243
- existing.version === WORKFLOW_STATE_VERSION &&
244
- existing.current_phase === nextPhase &&
245
- (existing.active === true || PHASE_LOCK.has(nextPhase))
246
- ) {
247
- return;
248
- }
249
- existing.run_id = runId;
250
- if (typeof existing.skill !== "string") existing.skill = "ralplan";
251
- // A successful persist means ralplan is actively writing this run's artifacts, so always
252
- // re-assert active. Fallback-only init left active:false after a clear (#644, sibling of #638).
253
- existing.active = true;
254
- existing.current_phase = nextPhase;
255
- existing = migrateWorkflowState(existing, "ralplan").state;
256
- existing.updated_at = new Date().toISOString();
257
- await writeWorkflowEnvelopeAtomic(statePath, existing, {
258
- cwd,
259
- receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan persist-run-id", sessionId },
260
- audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
261
- });
199
+ return await withWorkflowStateLock(
200
+ statePath,
201
+ async () => {
202
+ const existingRead = await readExistingStateForMutation(statePath);
203
+ if (existingRead.kind === "corrupt") {
204
+ throw new RalplanCommandError(
205
+ 2,
206
+ `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
207
+ );
208
+ }
209
+ let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
210
+
211
+ // A new run_id is a fresh run, not a stray write on the prior run: never inherit a
212
+ // previous run's terminal/locked phase (which would start the new run already
213
+ // "complete"/"handoff" and disarm the Stop hook). PHASE_LOCK only guards same-run writes.
214
+ const isNewRun = existing.run_id !== runId;
215
+ const nextPhase = isNewRun ? stage : advanceCurrentPhase(existing.current_phase, stage);
216
+ if (
217
+ existing.run_id === runId &&
218
+ existing.version === WORKFLOW_STATE_VERSION &&
219
+ existing.current_phase === nextPhase &&
220
+ (existing.active === true || getSkillManifest("ralplan").phaseLock.includes(nextPhase))
221
+ ) {
222
+ return;
223
+ }
224
+ existing.run_id = runId;
225
+ if (typeof existing.skill !== "string") existing.skill = "ralplan";
226
+ // A successful persist means ralplan is actively writing this run's artifacts, so always
227
+ // re-assert active. Fallback-only init left active:false after a clear (#644, sibling of #638).
228
+ existing.active = true;
229
+ existing.current_phase = nextPhase;
230
+ existing = migrateWorkflowState(existing, "ralplan").state;
231
+ existing.updated_at = new Date().toISOString();
232
+ await writeWorkflowEnvelopeAtomic(statePath, existing, {
233
+ cwd,
234
+ receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan persist-run-id", sessionId },
235
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
236
+ });
237
+ },
238
+ { cwd },
239
+ );
262
240
  }
263
241
 
264
242
  /* --------------------------- planner run-state --------------------------- */
@@ -382,25 +360,31 @@ function plannerStatePayload(update: PlannerStateUpdate): Record<string, unknown
382
360
  */
383
361
  async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: PlannerStateUpdate): Promise<void> {
384
362
  const statePath = ralplanStatePath(cwd, sessionId);
385
- const existingRead = await readExistingStateForMutation(statePath);
386
- if (existingRead.kind === "corrupt") {
387
- throw new RalplanCommandError(
388
- 2,
389
- `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
390
- );
391
- }
392
- let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
393
- Object.assign(existing, plannerStatePayload(update));
394
- if (typeof existing.skill !== "string") existing.skill = "ralplan";
395
- if (typeof existing.active !== "boolean") existing.active = true;
396
- if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
397
- existing = migrateWorkflowState(existing, "ralplan").state;
398
- existing.updated_at = new Date().toISOString();
399
- await writeWorkflowEnvelopeAtomic(statePath, existing, {
400
- cwd,
401
- receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planner-state", sessionId },
402
- audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
403
- });
363
+ return await withWorkflowStateLock(
364
+ statePath,
365
+ async () => {
366
+ const existingRead = await readExistingStateForMutation(statePath);
367
+ if (existingRead.kind === "corrupt") {
368
+ throw new RalplanCommandError(
369
+ 2,
370
+ `existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
371
+ );
372
+ }
373
+ let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
374
+ Object.assign(existing, plannerStatePayload(update));
375
+ if (typeof existing.skill !== "string") existing.skill = "ralplan";
376
+ if (typeof existing.active !== "boolean") existing.active = true;
377
+ if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
378
+ existing = migrateWorkflowState(existing, "ralplan").state;
379
+ existing.updated_at = new Date().toISOString();
380
+ await writeWorkflowEnvelopeAtomic(statePath, existing, {
381
+ cwd,
382
+ receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planner-state", sessionId },
383
+ audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
384
+ });
385
+ },
386
+ { cwd },
387
+ );
404
388
  }
405
389
 
406
390
  async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
@@ -916,7 +900,7 @@ export async function runNativeRalplanCommand(args: string[], cwd = process.cwd(
916
900
  if (isRalplanArtifactWriteInvocation(args)) return await handleArtifactWrite(args, cwd);
917
901
  return await handleConsensusHandoff(args, cwd);
918
902
  } catch (error) {
919
- if (error instanceof RalplanCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
903
+ if (error instanceof CommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
920
904
  return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
921
905
  }
922
906
  }
@@ -39,13 +39,22 @@ export interface SessionIdSources {
39
39
  export class SessionResolutionError extends Error {
40
40
  constructor(
41
41
  message: string,
42
- readonly code: "blank_flag" | "no_session" | "ambiguous" | "missing_for_write",
42
+ readonly code: "blank_flag" | "unsafe_session" | "no_session" | "ambiguous" | "missing_for_write",
43
43
  ) {
44
44
  super(message);
45
45
  this.name = "SessionResolutionError";
46
46
  }
47
47
  }
48
48
 
49
+ function assertSafeResolvedSessionId(sessionId: string): void {
50
+ if (sessionId === "." || sessionId === ".." || /[/\\]/.test(sessionId)) {
51
+ throw new SessionResolutionError(
52
+ "session id must be a single path component (no separators or traversal)",
53
+ "unsafe_session",
54
+ );
55
+ }
56
+ }
57
+
49
58
  interface ResolvedFromSources {
50
59
  gjcSessionId: string;
51
60
  source: GjcSessionSource;
@@ -65,13 +74,18 @@ export function resolveSessionIdFromSources(sources: SessionIdSources): Resolved
65
74
  "blank_flag",
66
75
  );
67
76
  }
77
+ assertSafeResolvedSessionId(trimmed);
68
78
  return { gjcSessionId: trimmed, source: "flag" };
69
79
  }
70
80
  if (typeof payloadSessionId === "string" && payloadSessionId.trim() !== "") {
71
- return { gjcSessionId: payloadSessionId.trim(), source: "payload" };
81
+ const trimmed = payloadSessionId.trim();
82
+ assertSafeResolvedSessionId(trimmed);
83
+ return { gjcSessionId: trimmed, source: "payload" };
72
84
  }
73
85
  if (typeof envSessionId === "string" && envSessionId.trim() !== "") {
74
- return { gjcSessionId: envSessionId.trim(), source: "env" };
86
+ const trimmed = envSessionId.trim();
87
+ assertSafeResolvedSessionId(trimmed);
88
+ return { gjcSessionId: trimmed, source: "env" };
75
89
  }
76
90
  return undefined;
77
91
  }
@@ -155,6 +169,7 @@ async function collectActiveSessionCandidates(cwd: string): Promise<SessionCandi
155
169
  if (!entry.isDirectory()) continue;
156
170
  const gjcSessionId = sessionIdFromDirName(entry.name);
157
171
  if (!gjcSessionId) continue;
172
+ assertSafeResolvedSessionId(gjcSessionId);
158
173
  const dir = path.join(root, entry.name);
159
174
  const activityMs = await readActivityMs(path.join(dir, GJC_SESSION_ACTIVITY_FILE));
160
175
  // Sessions with no readable activity marker are considered inactive and