@gajae-code/coding-agent 0.11.0 → 0.11.2

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 (418) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/async/job-manager.d.ts +46 -7
  3. package/dist/types/cli/config-cli.d.ts +11 -1
  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 +56 -1
  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-receipt-freshness.d.ts +11 -0
  54. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  55. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  56. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  57. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  58. package/dist/types/goals/runtime.d.ts +1 -0
  59. package/dist/types/goals/state.d.ts +8 -0
  60. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  61. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  62. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  63. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  64. package/dist/types/hindsight/state.d.ts +11 -4
  65. package/dist/types/lsp/lspmux.d.ts +5 -3
  66. package/dist/types/lsp/path-trust.d.ts +1 -0
  67. package/dist/types/main.d.ts +3 -2
  68. package/dist/types/memories/storage.d.ts +1 -0
  69. package/dist/types/modes/action-registry.d.ts +29 -0
  70. package/dist/types/modes/components/chrome.d.ts +14 -0
  71. package/dist/types/modes/components/command-palette.d.ts +27 -0
  72. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +2 -1
  74. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  75. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  76. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  77. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  78. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  79. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  80. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  81. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  82. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  83. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  84. package/dist/types/modes/components/status-line.d.ts +1 -94
  85. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  86. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  87. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  88. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  89. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  90. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  91. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  92. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  93. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  94. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  95. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  96. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  97. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  98. package/dist/types/modes/interactive-mode.d.ts +21 -19
  99. package/dist/types/modes/jobs-observer.d.ts +2 -0
  100. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  101. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +21 -2
  102. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  103. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  104. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  105. package/dist/types/modes/types.d.ts +22 -15
  106. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  107. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  108. package/dist/types/sdk/broker/broker.d.ts +78 -12
  109. package/dist/types/sdk/broker/ensure.d.ts +9 -0
  110. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  111. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  112. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  113. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  114. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  115. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  116. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  117. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  120. package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
  121. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  122. package/dist/types/sdk/bus/index.d.ts +27 -0
  123. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  124. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  125. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  126. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  127. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  128. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  129. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  130. package/dist/types/sdk/bus/telegram-daemon.d.ts +69 -3
  131. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  132. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  133. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  134. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  135. package/dist/types/sdk/host/host.d.ts +2 -0
  136. package/dist/types/sdk/index.d.ts +1 -0
  137. package/dist/types/sdk/session-directory.d.ts +56 -0
  138. package/dist/types/session/agent-session.d.ts +82 -47
  139. package/dist/types/session/agent-storage.d.ts +7 -0
  140. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  141. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  142. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  143. package/dist/types/session/messages.d.ts +16 -0
  144. package/dist/types/session/session-manager-internal.d.ts +14 -0
  145. package/dist/types/session/session-manager.d.ts +82 -12
  146. package/dist/types/session/session-storage.d.ts +43 -3
  147. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  148. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  149. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  150. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  151. package/dist/types/system-prompt.d.ts +2 -11
  152. package/dist/types/task/executor.d.ts +3 -1
  153. package/dist/types/thinking.d.ts +7 -0
  154. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  155. package/dist/types/tools/ask.d.ts +64 -2
  156. package/dist/types/tools/bash.d.ts +3 -0
  157. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  158. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  159. package/dist/types/tools/cron.d.ts +2 -0
  160. package/dist/types/tools/fetch.d.ts +1 -0
  161. package/dist/types/tools/index.d.ts +30 -15
  162. package/dist/types/tools/output-meta.d.ts +1 -0
  163. package/dist/types/tools/read.d.ts +3 -0
  164. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  165. package/dist/types/tools/subagent-render.d.ts +1 -0
  166. package/dist/types/tools/subagent.d.ts +6 -1
  167. package/dist/types/utils/event-bus.d.ts +1 -1
  168. package/dist/types/web/search/index.d.ts +3 -1
  169. package/package.json +21 -9
  170. package/scripts/build-sdk-package-smoke.ts +39 -1
  171. package/scripts/generate-hotkeys-docs.ts +74 -0
  172. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  173. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  174. package/src/async/job-manager.ts +455 -69
  175. package/src/cli/config-cli.ts +75 -2
  176. package/src/cli/notify-cli.ts +92 -26
  177. package/src/cli/session-picker.ts +20 -3
  178. package/src/cli/setup-cli.ts +1 -1
  179. package/src/cli.ts +1 -1
  180. package/src/commands/config.ts +1 -1
  181. package/src/commands/harness.ts +34 -106
  182. package/src/commands/launch.ts +6 -4
  183. package/src/commands/mcp-serve.ts +85 -8
  184. package/src/commands/mcp.ts +2 -2
  185. package/src/commands/notify.ts +8 -4
  186. package/src/commands/sdk.ts +61 -40
  187. package/src/commands/ultragoal.ts +4 -1
  188. package/src/config/atomic-yaml-patch.ts +80 -13
  189. package/src/config/file-lock.ts +105 -43
  190. package/src/config/keybindings.ts +64 -20
  191. package/src/config/model-auth.ts +9 -0
  192. package/src/config/model-bindings-applier.ts +139 -0
  193. package/src/config/model-discovery-manager.ts +224 -0
  194. package/src/config/model-equivalence.ts +66 -0
  195. package/src/config/model-registry.ts +169 -302
  196. package/src/config/model-resolver.ts +361 -369
  197. package/src/config/models-config-schema.ts +5 -4
  198. package/src/config/settings-schema.ts +159 -2
  199. package/src/config/settings.ts +458 -482
  200. package/src/coordinator-mcp/policy.ts +76 -11
  201. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  202. package/src/coordinator-mcp/question-state.ts +778 -0
  203. package/src/coordinator-mcp/server.ts +1702 -238
  204. package/src/debug/index.ts +1 -1
  205. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  206. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  207. package/src/discovery/helpers.ts +1 -50
  208. package/src/edit/index.ts +64 -0
  209. package/src/eval/py/env.ts +26 -0
  210. package/src/eval/py/kernel.ts +5 -3
  211. package/src/extensibility/extensions/loader.ts +9 -1
  212. package/src/extensibility/extensions/runner.ts +12 -3
  213. package/src/extensibility/extensions/types.ts +39 -1
  214. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  215. package/src/extensibility/hooks/runner.ts +2 -2
  216. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  217. package/src/extensibility/shared-events.ts +2 -0
  218. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  219. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  220. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  221. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  222. package/src/gjc-runtime/launch-tmux.ts +2 -5
  223. package/src/gjc-runtime/launch-worktree.ts +5 -5
  224. package/src/gjc-runtime/linux-proc.ts +71 -0
  225. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  226. package/src/gjc-runtime/session-resolution.ts +18 -3
  227. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  228. package/src/gjc-runtime/state-migrations.ts +1 -1
  229. package/src/gjc-runtime/state-runtime.ts +320 -249
  230. package/src/gjc-runtime/state-writer.ts +36 -40
  231. package/src/gjc-runtime/team-launch.ts +319 -0
  232. package/src/gjc-runtime/team-notify.ts +326 -0
  233. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  234. package/src/gjc-runtime/team-store.ts +828 -0
  235. package/src/gjc-runtime/team-workers.ts +962 -0
  236. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  237. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  238. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  239. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  240. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  241. package/src/gjc-runtime/ultragoal-guard.ts +83 -12
  242. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  243. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  244. package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
  245. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  246. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  247. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  248. package/src/goals/continuation-timeout-guard.ts +79 -0
  249. package/src/goals/runtime.ts +7 -5
  250. package/src/goals/state.ts +18 -0
  251. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  252. package/src/harness-control-plane/phase-rollup.ts +4 -7
  253. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  254. package/src/harness-control-plane/receipt-spool.ts +51 -7
  255. package/src/harness-control-plane/session-lease.ts +12 -130
  256. package/src/hindsight/backend.ts +26 -27
  257. package/src/hindsight/state.ts +101 -32
  258. package/src/hooks/skill-state.ts +17 -11
  259. package/src/internal-urls/docs-index.generated.ts +21 -18
  260. package/src/lsp/client.ts +1 -1
  261. package/src/lsp/config.ts +145 -26
  262. package/src/lsp/index.ts +1 -1
  263. package/src/lsp/lspmux.ts +39 -10
  264. package/src/lsp/path-trust.ts +81 -0
  265. package/src/main.ts +28 -8
  266. package/src/memories/index.ts +1 -0
  267. package/src/memories/storage.ts +6 -2
  268. package/src/modes/action-registry.ts +165 -0
  269. package/src/modes/components/chrome.ts +40 -0
  270. package/src/modes/components/command-palette.ts +168 -0
  271. package/src/modes/components/custom-editor.ts +1 -1
  272. package/src/modes/components/custom-provider-wizard.ts +51 -14
  273. package/src/modes/components/index.ts +2 -1
  274. package/src/modes/components/pet-selector.ts +11 -17
  275. package/src/modes/components/plan-preview-overlay.ts +295 -0
  276. package/src/modes/components/queue-mode-selector.ts +10 -27
  277. package/src/modes/components/queue-pane.ts +79 -0
  278. package/src/modes/components/read-tool-group.ts +13 -0
  279. package/src/modes/components/session-observer-overlay.ts +207 -772
  280. package/src/modes/components/sessions-dashboard.ts +181 -0
  281. package/src/modes/components/settings-selector.ts +1 -1
  282. package/src/modes/components/show-images-selector.ts +10 -24
  283. package/src/modes/components/status-line/types.ts +1 -1
  284. package/src/modes/components/status-line.ts +1 -969
  285. package/src/modes/components/tasks-pane.ts +73 -0
  286. package/src/modes/components/theme-selector.ts +11 -31
  287. package/src/modes/components/tool-execution.ts +22 -0
  288. package/src/modes/components/tool-status-header.ts +1056 -0
  289. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  290. package/src/modes/controllers/btw-controller.ts +1 -0
  291. package/src/modes/controllers/command-controller.ts +9 -15
  292. package/src/modes/controllers/event-controller.ts +3 -2
  293. package/src/modes/controllers/extension-ui-controller.ts +72 -25
  294. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  295. package/src/modes/controllers/input-controller.ts +764 -229
  296. package/src/modes/controllers/mode-gate.ts +24 -0
  297. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  298. package/src/modes/controllers/selector-controller.ts +225 -10
  299. package/src/modes/controllers/stt-controller.ts +125 -0
  300. package/src/modes/interactive-mode.ts +302 -1151
  301. package/src/modes/jobs-observer.ts +3 -0
  302. package/src/modes/runtime-init.ts +8 -0
  303. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  304. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +78 -19
  305. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  306. package/src/modes/tasks-aggregator.ts +199 -0
  307. package/src/modes/transcript-item-registry.ts +229 -0
  308. package/src/modes/types.ts +47 -20
  309. package/src/modes/utils/ui-helpers.ts +17 -9
  310. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  311. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  312. package/src/prompts/agents/architect.md +2 -8
  313. package/src/prompts/agents/critic.md +2 -8
  314. package/src/prompts/agents/executor.md +3 -0
  315. package/src/prompts/agents/planner.md +4 -12
  316. package/src/prompts/system/custom-system-prompt.md +1 -23
  317. package/src/prompts/system/plan-mode-approved.md +8 -0
  318. package/src/prompts/system/system-prompt.md +17 -96
  319. package/src/prompts/tools/bash.md +4 -4
  320. package/src/prompts/tools/browser.md +15 -52
  321. package/src/prompts/tools/find.md +2 -2
  322. package/src/prompts/tools/hashline.md +13 -55
  323. package/src/prompts/tools/read.md +3 -6
  324. package/src/prompts/tools/search-tool-bm25.md +0 -2
  325. package/src/prompts/tools/search.md +2 -4
  326. package/src/prompts/tools/skill-discovery.md +1 -2
  327. package/src/prompts/tools/task.md +1 -1
  328. package/src/runtime-mcp/manager.ts +1 -1
  329. package/src/sdk/broker/broker.ts +206 -78
  330. package/src/sdk/broker/discovery.ts +21 -2
  331. package/src/sdk/broker/ensure.ts +85 -25
  332. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  333. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  334. package/src/sdk/broker/lifecycle.ts +1399 -142
  335. package/src/sdk/broker/process-incarnation.ts +5 -13
  336. package/src/sdk/broker/session-index.ts +145 -56
  337. package/src/sdk/broker/state-version.ts +21 -0
  338. package/src/sdk/broker/transport.ts +1 -1
  339. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  340. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  341. package/src/sdk/bus/chat-effect-journal.ts +14 -4
  342. package/src/sdk/bus/config-commands.ts +5 -4
  343. package/src/sdk/bus/config.ts +98 -0
  344. package/src/sdk/bus/conversation-store.ts +5 -29
  345. package/src/sdk/bus/daemon-paths.ts +2 -0
  346. package/src/sdk/bus/discord-daemon.ts +25 -8
  347. package/src/sdk/bus/html-format.ts +20 -3
  348. package/src/sdk/bus/index.ts +834 -113
  349. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  350. package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
  351. package/src/sdk/bus/natives-augment.d.ts +26 -1
  352. package/src/sdk/bus/notification-service.ts +168 -20
  353. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  354. package/src/sdk/bus/recent-activity.ts +188 -74
  355. package/src/sdk/bus/rich-document.ts +258 -0
  356. package/src/sdk/bus/rich-render.ts +62 -70
  357. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  358. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  359. package/src/sdk/bus/telegram-daemon.ts +1491 -247
  360. package/src/sdk/bus/threaded-inbound.ts +8 -3
  361. package/src/sdk/bus/threaded-render.ts +57 -1
  362. package/src/sdk/bus/topic-registry.ts +120 -13
  363. package/src/sdk/host/control/dispatch.ts +14 -2
  364. package/src/sdk/host/control/operations.ts +6 -1
  365. package/src/sdk/host/host.ts +31 -1
  366. package/src/sdk/host/query/handlers.ts +20 -22
  367. package/src/sdk/host/query/revision-store.ts +80 -57
  368. package/src/sdk/index.ts +1 -0
  369. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  370. package/src/sdk/session-directory.ts +141 -0
  371. package/src/sdk/session.ts +458 -479
  372. package/src/session/agent-session.ts +1901 -637
  373. package/src/session/agent-storage.ts +18 -0
  374. package/src/session/cache-economics.ts +25 -8
  375. package/src/session/fallback-chain-controller.ts +53 -3
  376. package/src/session/internal/managed-session-scope.ts +2226 -0
  377. package/src/session/internal/managed-session-storage.ts +453 -0
  378. package/src/session/messages.ts +91 -2
  379. package/src/session/session-manager-internal.ts +33 -0
  380. package/src/session/session-manager.ts +1112 -333
  381. package/src/session/session-storage.ts +444 -39
  382. package/src/session/volatile-context-pruning.ts +78 -0
  383. package/src/setup/credential-auto-import.ts +2 -1
  384. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  385. package/src/setup/provider-onboarding.ts +36 -61
  386. package/src/setup/provider-presets.json +5 -0
  387. package/src/skill-state/active-state.ts +3 -13
  388. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  389. package/src/skill-state/workflow-state-contract.ts +6 -41
  390. package/src/slash-commands/builtin-registry.ts +46 -11
  391. package/src/system-prompt.ts +37 -44
  392. package/src/task/agents.ts +19 -1
  393. package/src/task/executor.ts +90 -6
  394. package/src/task/index.ts +62 -37
  395. package/src/thinking.ts +21 -6
  396. package/src/tool-discovery/tool-index.ts +2 -134
  397. package/src/tools/ask.ts +157 -21
  398. package/src/tools/ast-edit.ts +1 -1
  399. package/src/tools/bash.ts +38 -0
  400. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  401. package/src/tools/browser/tab-supervisor.ts +282 -36
  402. package/src/tools/computer.ts +10 -3
  403. package/src/tools/cron.ts +30 -16
  404. package/src/tools/eval.ts +1 -2
  405. package/src/tools/fetch.ts +11 -4
  406. package/src/tools/index.ts +76 -24
  407. package/src/tools/output-meta.ts +13 -5
  408. package/src/tools/read.ts +37 -1
  409. package/src/tools/resource-gc.ts +2 -2
  410. package/src/tools/search-tool-bm25.ts +11 -74
  411. package/src/tools/subagent-render.ts +19 -8
  412. package/src/tools/subagent.ts +84 -32
  413. package/src/utils/event-bus.ts +9 -4
  414. package/src/web/search/index.ts +3 -3
  415. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  416. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  417. package/src/discovery/plugin-dir-roots.ts +0 -28
  418. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { getWorktreesDir } from "@gajae-code/utils/dirs";
5
- import { SPAWN_PROVENANCE_ENV } from "../sdk/bus/config";
6
5
  import type { WorkflowHudSummary } from "../skill-state/active-state";
7
6
  import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
8
7
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
@@ -15,12 +14,66 @@ import {
15
14
  appendJsonl as appendJsonlAudited,
16
15
  appendText,
17
16
  createJsonNoClobber,
18
- deleteIfOwned,
19
17
  removeFileAudited,
20
18
  writeJsonAtomic,
21
19
  writeReport,
22
20
  writeWorkflowEnvelopeAtomic,
23
21
  } from "./state-writer";
22
+ import { buildWorkerCommand, startGjcTeamLaunch } from "./team-launch";
23
+ import {
24
+ listTeamMailbox,
25
+ markTeamMailboxMessage,
26
+ reconcileTeamNotifications,
27
+ replayTeamNotifications,
28
+ sendTeamMessage,
29
+ summarizeTeamNotifications,
30
+ type TeamNotificationRuntime,
31
+ } from "./team-notify";
32
+ import type {
33
+ GjcTeamApiClaimResult,
34
+ GjcTeamMailboxDeliveryTransport,
35
+ GjcTeamNotification,
36
+ GjcTeamNotificationDeliveryState,
37
+ GjcTeamNotificationSummary,
38
+ GjcTeamPaneAttemptResult,
39
+ GjcTeamTask,
40
+ GjcTeamTaskClaim,
41
+ GjcTeamTaskMetadataInput,
42
+ GjcTeamTaskMutationCapability,
43
+ GjcTeamTaskStatus,
44
+ } from "./team-store";
45
+ import {
46
+ GjcTeamTaskStore,
47
+ isCanonicalPersistedGjcTeamTask,
48
+ isCanonicalPersistedGjcTeamTaskClaim,
49
+ isGjcTeamTaskCompletionVerified,
50
+ readGjcTeamTasksFromDir as readTasks,
51
+ taskMetadataFromInput,
52
+ withGjcTeamMutationFence,
53
+ withGjcTeamTaskMutation,
54
+ } from "./team-store";
55
+
56
+ import {
57
+ GJC_TEAM_CONTINUATION_PROMPT,
58
+ type GjcTeamWorkerOrchestrationRuntime,
59
+ type GjcTeamWorkerRuntime,
60
+ gjcContinuationReservationDigest,
61
+ isValidGjcContinuationOutcome,
62
+ isValidGjcContinuationReservation,
63
+ readGjcShutdownAuthority,
64
+ readWorkerLifecycleById as readLifecycleById,
65
+ readGjcShutdownAck as readShutdownAck,
66
+ readGjcWorkerHeartbeat as readWorkerHeartbeat,
67
+ readGjcWorkerStatus as readWorkerStatus,
68
+ reconcileGjcTeamStaleClaimsUnlocked,
69
+ shutdownGjcTeamWorkers,
70
+ updateGjcWorkerHeartbeat as updateWorkerHeartbeat,
71
+ updateGjcWorkerStatus as updateWorkerStatus,
72
+ writeWorkerLifecycleForConfig as writeLifecycleForConfig,
73
+ writeGjcShutdownRequest as writeShutdownRequest,
74
+ writeGjcWorkerStartupAck as writeWorkerStartupAck,
75
+ } from "./team-workers";
76
+
24
77
  import {
25
78
  buildGjcTmuxExactOptionTarget,
26
79
  buildGjcTmuxUntaggedSessionHint,
@@ -31,18 +84,21 @@ import {
31
84
  resolveGjcTmuxCommand,
32
85
  } from "./tmux-common";
33
86
 
34
- export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
35
- export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
36
- export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "failed" | "draining" | "unknown";
37
- export type GjcTeamWorkerLifecycleState =
38
- | "starting"
39
- | "ready"
40
- | "working"
41
- | "draining"
42
- | "stopped"
43
- | "failed"
44
- | "unknown";
45
- export type GjcTeamShutdownMode = "graceful" | "force" | "abort";
87
+ export type {
88
+ GjcTeamApiClaimResult,
89
+ GjcTeamMailboxDeliveryInput,
90
+ GjcTeamMailboxDeliveryResult,
91
+ GjcTeamMailboxDeliveryTransport,
92
+ GjcTeamMailboxDeliveryTransportKind,
93
+ GjcTeamNotification,
94
+ GjcTeamNotificationDeliveryState,
95
+ GjcTeamNotificationSummary,
96
+ GjcTeamPaneAttemptResult,
97
+ GjcTeamTask,
98
+ GjcTeamTaskClaim,
99
+ GjcTeamTaskMetadataInput,
100
+ GjcTeamTaskStatus,
101
+ } from "./team-store";
46
102
 
47
103
  export const GJC_TEAM_DEFAULT_WORKERS = 3;
48
104
  export const GJC_TEAM_MAX_WORKERS = 20;
@@ -76,57 +132,17 @@ export interface GjcTeamWorker {
76
132
  worktree_base_ref?: string;
77
133
  team_state_root?: string;
78
134
  }
79
-
80
- export interface GjcTeamTaskClaim {
81
- owner: string;
82
- token: string;
83
- leased_until: string;
84
- }
85
- export type GjcTeamTaskCompletionEvidenceKind = "command" | "inspection" | "artifact";
86
- export type GjcTeamTaskCompletionEvidenceStatus = "passed" | "failed" | "not_run" | "verified" | "rejected";
87
-
88
- export interface GjcTeamTaskCompletionEvidenceItem {
89
- kind: GjcTeamTaskCompletionEvidenceKind;
90
- status: GjcTeamTaskCompletionEvidenceStatus;
91
- summary: string;
92
- command?: string;
93
- artifact?: string;
94
- location?: string;
95
- output?: string;
96
- }
97
-
98
- export interface GjcTeamTaskCompletionEvidence {
99
- summary: string;
100
- items: GjcTeamTaskCompletionEvidenceItem[];
101
- files?: string[];
102
- notes?: string;
103
- recorded_by: string;
104
- recorded_at: string;
105
- }
106
-
107
- export interface GjcTeamTask {
108
- id: string;
109
- subject: string;
110
- description: string;
111
- title: string;
112
- objective: string;
113
- status: GjcTeamTaskStatus;
114
- assignee?: string;
115
- owner?: string;
116
- result?: string;
117
- completion_evidence?: GjcTeamTaskCompletionEvidence;
118
- error?: string;
119
- blocked_by?: string[];
120
- depends_on?: string[];
121
- lane?: string;
122
- required_role?: string;
123
- allowed_roles?: string[];
124
- version: number;
125
- claim?: GjcTeamTaskClaim;
126
- created_at: string;
127
- updated_at: string;
128
- completed_at?: string;
129
- }
135
+ export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
136
+ export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "failed" | "draining" | "unknown";
137
+ export type GjcTeamWorkerLifecycleState =
138
+ | "starting"
139
+ | "ready"
140
+ | "working"
141
+ | "draining"
142
+ | "stopped"
143
+ | "failed"
144
+ | "unknown";
145
+ export type GjcTeamShutdownMode = "graceful" | "force" | "abort";
130
146
 
131
147
  export type GjcTeamWorktreeMode =
132
148
  | { enabled: false }
@@ -142,6 +158,7 @@ export interface GjcTeamConfig {
142
158
  worker_count: number;
143
159
  max_workers: number;
144
160
  state_root: string;
161
+ gjc_session_id?: string;
145
162
  worker_command: string;
146
163
  worker_cli_plan: GjcTeamWorkerCli[];
147
164
  tmux_command: string;
@@ -198,70 +215,6 @@ export interface GjcTeamWorkerLifecycle {
198
215
  shutdown_mode?: GjcTeamShutdownMode;
199
216
  }
200
217
 
201
- export type GjcTeamNotificationDeliveryState =
202
- | "pending"
203
- | "sent"
204
- | "queued"
205
- | "deferred"
206
- | "failed"
207
- | "delivered"
208
- | "acknowledged";
209
-
210
- export type GjcTeamPaneAttemptResult = "sent" | "queued" | "deferred" | "failed";
211
- export type GjcTeamMailboxDeliveryTransportKind = "sdk" | "pane";
212
-
213
- export interface GjcTeamNotification {
214
- id: string;
215
- kind: "mailbox_message" | "worker_lifecycle" | "invalid_attempt";
216
- team_name: string;
217
- recipient: string;
218
- source: { type: "message" | "task" | "worker" | "event"; id: string };
219
- idempotency_key?: string;
220
- delivery_state: GjcTeamNotificationDeliveryState;
221
- pane_attempt_result?: GjcTeamPaneAttemptResult;
222
- pane_attempt_reason?: string;
223
- pane_attempt_at?: string;
224
- created_at: string;
225
- updated_at: string;
226
- replay_count: number;
227
- }
228
- export interface GjcTeamMailboxDeliveryInput {
229
- team_name: string;
230
- state_dir: string;
231
- config: GjcTeamConfig;
232
- notification: GjcTeamNotification;
233
- message: GjcTeamMailboxMessage;
234
- cwd: string;
235
- env: NodeJS.ProcessEnv;
236
- }
237
- export type GjcTeamMailboxDeliveryResult =
238
- | { transport: "sdk"; state: GjcTeamNotificationDeliveryState; reason?: string }
239
- | { transport: "pane"; state: GjcTeamPaneAttemptResult; reason?: string };
240
- export interface GjcTeamMailboxDeliveryTransport {
241
- deliverMailboxMessage(input: GjcTeamMailboxDeliveryInput): Promise<GjcTeamMailboxDeliveryResult | null>;
242
- }
243
-
244
- let gjcTeamMailboxDeliveryTransport: GjcTeamMailboxDeliveryTransport | undefined;
245
-
246
- export function setGjcTeamMailboxDeliveryTransport(transport: GjcTeamMailboxDeliveryTransport | undefined): () => void {
247
- const previous = gjcTeamMailboxDeliveryTransport;
248
- gjcTeamMailboxDeliveryTransport = transport;
249
- return () => {
250
- gjcTeamMailboxDeliveryTransport = previous;
251
- };
252
- }
253
- export function setGjcTeamMailboxDeliveryTransportForTest(
254
- transport: GjcTeamMailboxDeliveryTransport | undefined,
255
- ): () => void {
256
- return setGjcTeamMailboxDeliveryTransport(transport);
257
- }
258
-
259
- export interface GjcTeamNotificationSummary {
260
- total: number;
261
- replay_eligible: number;
262
- by_state: Record<GjcTeamNotificationDeliveryState, number>;
263
- }
264
-
265
218
  export interface GjcTeamSnapshot {
266
219
  team_name: string;
267
220
  display_name: string;
@@ -295,13 +248,6 @@ export interface GjcTeamStartOptions {
295
248
  mailboxDeliveryTransport?: GjcTeamMailboxDeliveryTransport;
296
249
  }
297
250
 
298
- export interface GjcTeamApiClaimResult {
299
- ok: boolean;
300
- task?: GjcTeamTask;
301
- worker_id?: string;
302
- claim_token?: string;
303
- reason?: string;
304
- }
305
251
  export type GjcTeamLivenessRecoveryReason =
306
252
  | "claim_expired"
307
253
  | "stale_heartbeat"
@@ -370,7 +316,10 @@ function notificationReceiptFields(notification: GjcTeamNotification): Record<st
370
316
 
371
317
  function notificationSummaryReceipt(
372
318
  teamName: string,
373
- result: { notifications: GjcTeamNotification[]; summary: GjcTeamNotificationSummary },
319
+ result: {
320
+ notifications: GjcTeamNotification[];
321
+ summary: GjcTeamNotificationSummary;
322
+ },
374
323
  ): Record<string, unknown> {
375
324
  return {
376
325
  team_name: teamName,
@@ -475,13 +424,13 @@ export interface GjcTeamTraceEvent {
475
424
  evidence_refs?: string[];
476
425
  data?: Record<string, unknown>;
477
426
  }
478
- interface WorkerStatusFile {
427
+ export interface WorkerStatusFile {
479
428
  state: GjcWorkerStatusState;
480
429
  current_task_id?: string;
481
430
  reason?: string;
482
431
  updated_at: string;
483
432
  }
484
- interface WorkerHeartbeatFile {
433
+ export interface WorkerHeartbeatFile {
485
434
  pid: number;
486
435
  last_turn_at: string;
487
436
  turn_count: number;
@@ -530,6 +479,10 @@ export interface GjcWorkerIntegrationAttemptRequestResult {
530
479
  status?: GjcWorkerCheckpointClassification["kind"];
531
480
  }
532
481
 
482
+ export interface GjcWorkerIntegrationAttemptOptions {
483
+ signal?: AbortSignal;
484
+ }
485
+
533
486
  function isGjcTeamTaskStatus(value: string): value is GjcTeamTaskStatus {
534
487
  return ["pending", "blocked", "in_progress", "completed", "failed"].includes(value);
535
488
  }
@@ -581,9 +534,28 @@ export const GJC_TEAM_API_OPERATIONS = [
581
534
  "write-task-approval",
582
535
  ] as const;
583
536
 
537
+ function currentTimeMs(): number {
538
+ return gjcTeamRuntimeTestSeams?.nowMs?.() ?? Date.now();
539
+ }
540
+
584
541
  function now(): string {
585
- return new Date().toISOString();
542
+ return new Date(currentTimeMs()).toISOString();
543
+ }
544
+
545
+ export interface GjcTeamRuntimeTestSeams {
546
+ nowMs?: () => number;
547
+ continuationTmuxDispatch?: (command: string, args: readonly string[]) => { exitCode?: number };
548
+
549
+ continuationBeforeDispatch?: () => Promise<void>;
550
+ }
551
+
552
+ let gjcTeamRuntimeTestSeams: GjcTeamRuntimeTestSeams | undefined;
553
+
554
+ /** @internal Test-only seam; production code leaves the real clock and tmux dispatch intact. */
555
+ export function __setGjcTeamRuntimeTestSeamsForTests(seams: GjcTeamRuntimeTestSeams | undefined): void {
556
+ gjcTeamRuntimeTestSeams = seams;
586
557
  }
558
+
587
559
  function isEnoent(error: unknown): error is FsError {
588
560
  return typeof error === "object" && error !== null && "code" in error && (error as FsError).code === "ENOENT";
589
561
  }
@@ -599,7 +571,12 @@ function stateWriterOptions(filePath: string, category: "state" | "ledger" | "re
599
571
  // Session-scoped audit requires a GJC session. When an explicit env-root override
600
572
  // (e.g. GJC_TEAM_STATE_ROOT) is in effect with no resolvable session, omit the audit
601
573
  // context entirely so the override write does not fail on a session-scoped audit.
602
- return sessionId ? { cwd, audit: { category, verb, owner: "gjc-runtime" as const, sessionId } } : { cwd };
574
+ return sessionId
575
+ ? {
576
+ cwd,
577
+ audit: { category, verb, owner: "gjc-runtime" as const, sessionId },
578
+ }
579
+ : { cwd };
603
580
  }
604
581
 
605
582
  function sanitizeName(value: string): string {
@@ -646,9 +623,6 @@ function safePathSegment(kind: string, value: string): string {
646
623
  assertSafeId(kind, value);
647
624
  return value;
648
625
  }
649
- function taskPath(dir: string, taskId: string): string {
650
- return path.join(dir, "tasks", `${safePathSegment("task_id", taskId)}.json`);
651
- }
652
626
  function mailboxPath(dir: string, worker: string): string {
653
627
  return path.join(dir, "mailbox", `${safePathSegment("worker_id", worker)}.json`);
654
628
  }
@@ -723,7 +697,9 @@ function workerIntegrationDedupePath(dir: string, worker: string): string {
723
697
  export function resolveGjcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
724
698
  const explicit = env.GJC_TEAM_STATE_ROOT?.trim();
725
699
  if (explicit) return path.resolve(cwd, explicit);
726
- const session = resolveGjcSessionForWrite(cwd, { envSessionId: env.GJC_SESSION_ID });
700
+ const session = resolveGjcSessionForWrite(cwd, {
701
+ envSessionId: env.GJC_SESSION_ID,
702
+ });
727
703
  return teamStateRoot(cwd, session.gjcSessionId);
728
704
  }
729
705
 
@@ -735,6 +711,110 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
735
711
  throw error;
736
712
  }
737
713
  }
714
+
715
+ async function readContinuationJson<T>(filePath: string): Promise<T | null> {
716
+ try {
717
+ return await readJsonFile<T>(filePath);
718
+ } catch {
719
+ return null;
720
+ }
721
+ }
722
+ type GjcContinuationAuthorityInventory =
723
+ | { valid: true; tasks: GjcTeamTask[]; claims: Map<string, GjcTeamTaskClaim> }
724
+ | { valid: false };
725
+
726
+ async function readGjcContinuationAuthorityInventory(dir: string): Promise<GjcContinuationAuthorityInventory> {
727
+ let taskNames: string[];
728
+ try {
729
+ taskNames = (await fs.readdir(path.join(dir, "tasks"), { withFileTypes: true }))
730
+ .filter(entry => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".evidence.json"))
731
+ .map(entry => entry.name);
732
+ } catch {
733
+ return { valid: false };
734
+ }
735
+ const tasks: GjcTeamTask[] = [];
736
+ const taskIds = new Set<string>();
737
+ for (const name of taskNames) {
738
+ const id = name.slice(0, -".json".length);
739
+ try {
740
+ const task = await Bun.file(path.join(dir, "tasks", name)).json();
741
+ if (!isCanonicalPersistedGjcTeamTask(task, id) || taskIds.has(task.id)) return { valid: false };
742
+ taskIds.add(task.id);
743
+ tasks.push(task);
744
+ } catch {
745
+ return { valid: false };
746
+ }
747
+ }
748
+ let claimNames: string[];
749
+ try {
750
+ claimNames = (await fs.readdir(path.join(dir, "claims"), { withFileTypes: true }))
751
+ .filter(entry => entry.isFile() && entry.name.endsWith(".json"))
752
+ .map(entry => entry.name);
753
+ } catch (error) {
754
+ if (!isEnoent(error)) return { valid: false };
755
+ claimNames = [];
756
+ }
757
+ const claims = new Map<string, GjcTeamTaskClaim>();
758
+ for (const name of claimNames) {
759
+ const id = name.slice(0, -".json".length);
760
+ if (!taskIds.has(id) || claims.has(id)) return { valid: false };
761
+ try {
762
+ const claim = await Bun.file(path.join(dir, "claims", name)).json();
763
+ if (!isCanonicalPersistedGjcTeamTaskClaim(claim)) return { valid: false };
764
+ claims.set(id, claim);
765
+ } catch {
766
+ return { valid: false };
767
+ }
768
+ }
769
+ for (const task of tasks) {
770
+ const claim = claims.get(task.id);
771
+ if ((task.claim === undefined) !== (claim === undefined)) return { valid: false };
772
+ if (
773
+ task.claim !== undefined &&
774
+ (!claim ||
775
+ claim.owner !== task.claim.owner ||
776
+ claim.token !== task.claim.token ||
777
+ claim.leased_until !== task.claim.leased_until)
778
+ )
779
+ return { valid: false };
780
+ }
781
+ return { valid: true, tasks, claims };
782
+ }
783
+
784
+ type GjcContinuationWorkerAuthority =
785
+ | { valid: true; task: GjcTeamTask & { claim: GjcTeamTaskClaim }; claim: GjcTeamTaskClaim }
786
+ | { valid: false; taskCount: number; claimCount: number };
787
+
788
+ function selectGjcContinuationWorkerAuthority(
789
+ inventory: Extract<GjcContinuationAuthorityInventory, { valid: true }>,
790
+ worker: string,
791
+ ): GjcContinuationWorkerAuthority {
792
+ const tasks = inventory.tasks.filter(
793
+ task =>
794
+ task.status === "in_progress" &&
795
+ task.claim?.owner === worker &&
796
+ task.assignee === worker &&
797
+ task.owner === worker,
798
+ );
799
+ const claims = [...inventory.claims.entries()]
800
+ .filter(([, claim]) => claim.owner === worker)
801
+ .map(([taskId, claim]) => ({ taskId, claim }));
802
+ if (tasks.length !== 1 || claims.length !== 1)
803
+ return { valid: false, taskCount: tasks.length, claimCount: claims.length };
804
+ const task = tasks[0];
805
+ const stored = claims[0];
806
+ if (
807
+ !task?.claim ||
808
+ !stored ||
809
+ stored.taskId !== task.id ||
810
+ stored.claim.owner !== task.claim.owner ||
811
+ stored.claim.token !== task.claim.token ||
812
+ stored.claim.leased_until !== task.claim.leased_until
813
+ )
814
+ return { valid: false, taskCount: tasks.length, claimCount: claims.length };
815
+ return { valid: true, task: { ...task, claim: task.claim }, claim: stored.claim };
816
+ }
817
+
738
818
  function isPositivePid(value: unknown): value is number {
739
819
  return typeof value === "number" && Number.isInteger(value) && value > 0;
740
820
  }
@@ -763,7 +843,10 @@ interface TeamGcPidClassification {
763
843
  */
764
844
  function classifyTeamGcWorkerPids(pids: number[], probe: GcPidProbe): TeamGcPidClassification {
765
845
  if (pids.length === 0) return { removable: false, pidStatus: "none" };
766
- const statuses = pids.map(pid => ({ pid, status: gcProbeStatus(probe, pid) }));
846
+ const statuses = pids.map(pid => ({
847
+ pid,
848
+ status: gcProbeStatus(probe, pid),
849
+ }));
767
850
  const kept = statuses.find(entry => entry.status !== "dead");
768
851
  if (kept) return { removable: false, pidStatus: kept.status, pid: kept.pid };
769
852
  return { removable: true, pidStatus: "dead", pid: statuses[0]?.pid };
@@ -795,7 +878,9 @@ export async function listTeamWorkerGcRecords(teamRoot: string, probe: GcPidProb
795
878
  const teamDirPath = path.join(teamRoot, teamName);
796
879
  let workerEntries: import("node:fs").Dirent[];
797
880
  try {
798
- workerEntries = await fs.readdir(path.join(teamDirPath, "workers"), { withFileTypes: true });
881
+ workerEntries = await fs.readdir(path.join(teamDirPath, "workers"), {
882
+ withFileTypes: true,
883
+ });
799
884
  } catch (error) {
800
885
  if (isEnoent(error)) continue;
801
886
  throw error;
@@ -863,6 +948,18 @@ export async function listTeamWorkerGcRecords(teamRoot: string, probe: GcPidProb
863
948
 
864
949
  /** @internal */
865
950
  export async function pruneTeamWorkerGcRecord(record: GcRecord, probe: GcPidProbe): Promise<boolean> {
951
+ if (!record.path || !record.id.includes("/")) return false;
952
+ const teamDirPath = path.dirname(path.dirname(record.path));
953
+ return withGjcTeamTaskMutation(taskStore(teamDirPath), capability =>
954
+ pruneTeamWorkerGcRecordUnlocked(record, probe, capability),
955
+ );
956
+ }
957
+
958
+ async function pruneTeamWorkerGcRecordUnlocked(
959
+ record: GcRecord,
960
+ probe: GcPidProbe,
961
+ capability: GjcTeamTaskMutationCapability,
962
+ ): Promise<boolean> {
866
963
  if (!record.path || !record.id.includes("/")) return false;
867
964
  const [teamName, workerId] = record.id.split("/", 2);
868
965
  if (!teamName || !workerId) return false;
@@ -888,7 +985,7 @@ export async function pruneTeamWorkerGcRecord(record: GcRecord, probe: GcPidProb
888
985
  for (const task of await readTasks(teamDirPath)) {
889
986
  if (task.claim?.owner !== workerId && task.assignee !== workerId) continue;
890
987
  if (task.status === "completed" || task.status === "failed") continue;
891
- await writeTask(teamDirPath, {
988
+ await capability.writeRecovered({
892
989
  ...task,
893
990
  status: "pending",
894
991
  assignee: undefined,
@@ -1065,7 +1162,11 @@ async function appendTraceForEvent(dir: string, event: GjcTeamEvent): Promise<vo
1065
1162
  }
1066
1163
  }
1067
1164
  async function appendEvent(dir: string, event: Omit<GjcTeamEvent, "ts" | "event_id">): Promise<GjcTeamEvent> {
1068
- const full = { event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2)}`, ts: now(), ...event };
1165
+ const full = {
1166
+ event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2)}`,
1167
+ ts: now(),
1168
+ ...event,
1169
+ };
1069
1170
  await appendJsonl(path.join(dir, "events.jsonl"), full);
1070
1171
  await appendTraceForEvent(dir, full);
1071
1172
  return full;
@@ -1094,7 +1195,11 @@ async function readConfig(dir: string): Promise<GjcTeamConfig> {
1094
1195
  };
1095
1196
  }
1096
1197
  const WORKER_INTEGRATION_CONFIG_CACHE_TTL_MS = 100;
1097
- type WorkerIntegrationConfigCacheEntry = { checkedAt: number; mtimeMs: number; config: GjcTeamConfig };
1198
+ type WorkerIntegrationConfigCacheEntry = {
1199
+ checkedAt: number;
1200
+ mtimeMs: number;
1201
+ config: GjcTeamConfig;
1202
+ };
1098
1203
  const workerIntegrationConfigCache = new Map<string, WorkerIntegrationConfigCacheEntry>();
1099
1204
 
1100
1205
  async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfig> {
@@ -1105,142 +1210,80 @@ async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfi
1105
1210
  if (cached && cached.mtimeMs === stat.mtimeMs && nowMs - cached.checkedAt <= WORKER_INTEGRATION_CONFIG_CACHE_TTL_MS)
1106
1211
  return cached.config;
1107
1212
  const config = await readConfig(dir);
1108
- workerIntegrationConfigCache.set(configPath, { checkedAt: nowMs, mtimeMs: stat.mtimeMs, config });
1213
+ workerIntegrationConfigCache.set(configPath, {
1214
+ checkedAt: nowMs,
1215
+ mtimeMs: stat.mtimeMs,
1216
+ config,
1217
+ });
1109
1218
 
1110
1219
  return config;
1111
1220
  }
1112
1221
  async function readPhase(dir: string): Promise<GjcTeamPhase> {
1113
- const phase = await readJsonFile<{ current_phase?: GjcTeamPhase }>(path.join(dir, "phase.json"));
1114
- return phase?.current_phase ?? "running";
1222
+ try {
1223
+ const phase = await readJsonFile<{ current_phase?: GjcTeamPhase }>(path.join(dir, "phase.json"));
1224
+ return phase?.current_phase ?? "running";
1225
+ } catch (error) {
1226
+ if (error instanceof SyntaxError) return "running";
1227
+ throw error;
1228
+ }
1115
1229
  }
1116
1230
  async function writePhase(dir: string, phase: GjcTeamPhase): Promise<void> {
1117
- await writeJsonFile(path.join(dir, "phase.json"), { current_phase: phase, updated_at: now() });
1231
+ await writeJsonFile(path.join(dir, "phase.json"), {
1232
+ current_phase: phase,
1233
+ updated_at: now(),
1234
+ });
1118
1235
  }
1119
1236
  function isGjcWorkerStatusState(value: string): value is GjcWorkerStatusState {
1120
1237
  return ["idle", "working", "blocked", "done", "failed", "draining", "unknown"].includes(value);
1121
1238
  }
1122
-
1123
- function parseGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
1124
- return typeof value === "string" && isGjcWorkerStatusState(value) ? value : "unknown";
1125
- }
1126
1239
  function parseRequiredGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
1127
1240
  const raw = typeof value === "string" ? value.trim() : "";
1128
1241
  if (isGjcWorkerStatusState(raw)) return raw;
1129
1242
  throw new Error(`invalid_worker_status:${raw}`);
1130
1243
  }
1131
-
1132
- function lifecycleStateForWorkerStatus(status: GjcWorkerStatusState): GjcTeamWorkerLifecycleState {
1133
- switch (status) {
1134
- case "working":
1135
- return "working";
1136
- case "draining":
1137
- return "draining";
1138
- case "failed":
1139
- return "failed";
1140
- case "unknown":
1141
- return "unknown";
1142
- case "idle":
1143
- case "blocked":
1144
- case "done":
1145
- return "ready";
1146
- }
1147
- }
1148
-
1149
1244
  function parseGjcTeamShutdownMode(value: unknown): GjcTeamShutdownMode {
1150
1245
  const raw = typeof value === "string" ? value.trim() : "graceful";
1151
1246
  if (raw === "graceful" || raw === "force" || raw === "abort") return raw;
1152
1247
  throw new Error(`invalid_shutdown_mode:${raw}`);
1153
1248
  }
1154
-
1155
- function isGjcTeamWorkerLifecycleState(value: string): value is GjcTeamWorkerLifecycleState {
1156
- return ["starting", "ready", "working", "draining", "stopped", "failed", "unknown"].includes(value);
1157
- }
1158
-
1159
- function parseGjcTeamWorkerLifecycleState(value: unknown): GjcTeamWorkerLifecycleState {
1160
- return typeof value === "string" && isGjcTeamWorkerLifecycleState(value) ? value : "unknown";
1161
- }
1162
-
1163
- async function readWorkerStatusFile(dir: string, worker: string): Promise<WorkerStatusFile> {
1164
- return (
1165
- (await readJsonFile<WorkerStatusFile>(path.join(workerDir(dir, worker), "status.json"))) ?? {
1166
- state: "unknown",
1167
- updated_at: now(),
1168
- }
1169
- );
1170
- }
1171
-
1172
- async function readWorkerLifecycleRecord(dir: string, worker: GjcTeamWorker): Promise<GjcTeamWorkerLifecycle> {
1173
- const workerStatus = await readWorkerStatusFile(dir, worker.id);
1174
- const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
1175
- const rawLifecycle = await readJsonFile<Partial<GjcTeamWorkerLifecycle>>(workerLifecyclePath(dir, worker.id));
1176
- const shutdownAck = await readJsonFile<Record<string, unknown>>(
1177
- path.join(workerDir(dir, worker.id), "shutdown-ack.json"),
1178
- );
1179
- const lifecycle: GjcTeamWorkerLifecycle = {
1180
- worker: worker.id,
1181
- lifecycle_state: parseGjcTeamWorkerLifecycleState(rawLifecycle?.lifecycle_state),
1182
- worker_status_state: parseGjcWorkerStatusState(workerStatus.state),
1183
- pane_id: worker.pane_id ?? rawLifecycle?.pane_id,
1184
- updated_at: rawLifecycle?.updated_at ?? workerStatus.updated_at ?? now(),
1185
- };
1186
- if (typeof rawLifecycle?.pid === "number") lifecycle.pid = rawLifecycle.pid;
1187
- else if (typeof heartbeat?.pid === "number") lifecycle.pid = heartbeat.pid;
1188
- if (rawLifecycle?.started_at) lifecycle.started_at = rawLifecycle.started_at;
1189
- if (rawLifecycle?.stopped_at) lifecycle.stopped_at = rawLifecycle.stopped_at;
1190
- if (rawLifecycle?.stop_reason) lifecycle.stop_reason = rawLifecycle.stop_reason;
1191
- if (rawLifecycle?.shutdown_request_id) lifecycle.shutdown_request_id = rawLifecycle.shutdown_request_id;
1192
- if (rawLifecycle?.shutdown_requested_at) lifecycle.shutdown_requested_at = rawLifecycle.shutdown_requested_at;
1193
- if (
1194
- rawLifecycle?.shutdown_mode === "graceful" ||
1195
- rawLifecycle?.shutdown_mode === "force" ||
1196
- rawLifecycle?.shutdown_mode === "abort"
1197
- )
1198
- lifecycle.shutdown_mode = rawLifecycle.shutdown_mode;
1199
- if (typeof shutdownAck?.acknowledged_at === "string")
1200
- lifecycle.shutdown_acknowledged_at = shutdownAck.acknowledged_at;
1201
- if (typeof shutdownAck?.status === "string") lifecycle.shutdown_ack_status = shutdownAck.status;
1202
- return lifecycle;
1203
- }
1204
-
1205
- async function readWorkerLifecycleById(
1206
- dir: string,
1207
- config: GjcTeamConfig,
1208
- ): Promise<Record<string, GjcTeamWorkerLifecycle>> {
1209
- const entries = await Promise.all(config.workers.map(worker => readWorkerLifecycleRecord(dir, worker)));
1210
- return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
1211
- }
1212
-
1213
- async function writeWorkerLifecycleRecord(
1214
- dir: string,
1215
- worker: GjcTeamWorker,
1216
- lifecycleState: GjcTeamWorkerLifecycleState,
1217
- updates: Partial<GjcTeamWorkerLifecycle> = {},
1218
- ): Promise<GjcTeamWorkerLifecycle> {
1219
- const current = await readWorkerLifecycleRecord(dir, worker);
1220
- const next: GjcTeamWorkerLifecycle = {
1221
- ...current,
1222
- ...updates,
1223
- worker: worker.id,
1224
- lifecycle_state: lifecycleState,
1225
- worker_status_state: current.worker_status_state,
1226
- pane_id: updates.pane_id ?? worker.pane_id ?? current.pane_id,
1227
- updated_at: now(),
1228
- };
1229
- await writeJsonFile(workerLifecyclePath(dir, worker.id), next);
1230
- return next;
1231
- }
1232
-
1233
- async function writeWorkerLifecycleForConfig(
1249
+ const workerRuntime: GjcTeamWorkerRuntime = {
1250
+ findTeamDir,
1251
+ readConfig,
1252
+ assertKnownWorker,
1253
+ assertKnownParticipant,
1254
+ findKnownWorker,
1255
+ workerDir,
1256
+ readJson: readJsonFile,
1257
+ writeJson: writeJsonFile,
1258
+ appendEvent,
1259
+ now,
1260
+ nowMs: currentTimeMs,
1261
+ stableHash,
1262
+ };
1263
+
1264
+ const readWorkerLifecycleById = (dir: string, config: GjcTeamConfig) => readLifecycleById(workerRuntime, dir, config);
1265
+ const writeWorkerLifecycleForConfig = (
1234
1266
  dir: string,
1235
1267
  config: GjcTeamConfig,
1236
- lifecycleState: GjcTeamWorkerLifecycleState,
1268
+ state: GjcTeamWorkerLifecycleState,
1237
1269
  updatesFor: (worker: GjcTeamWorker) => Partial<GjcTeamWorkerLifecycle> = () => ({}),
1238
- ): Promise<Record<string, GjcTeamWorkerLifecycle>> {
1239
- const entries = await Promise.all(
1240
- config.workers.map(worker => writeWorkerLifecycleRecord(dir, worker, lifecycleState, updatesFor(worker))),
1241
- );
1242
- return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
1243
- }
1270
+ ) => writeLifecycleForConfig(workerRuntime, dir, config, state, updatesFor);
1271
+
1272
+ const workerOrchestrationRuntime: GjcTeamWorkerOrchestrationRuntime = {
1273
+ ...workerRuntime,
1274
+ readTasks,
1275
+ withTaskMutation: (dir, fn) => withGjcTeamTaskMutation(taskStore(dir), fn),
1276
+ appendTelemetry,
1277
+ paneBelongsToTeamTarget,
1278
+ parseDurationEnv,
1279
+ parseHeartbeatStaleMs,
1280
+ killWorkerPanes,
1281
+ removeCleanCreatedWorktrees,
1282
+ readMonitorSnapshot: dir => readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir)),
1283
+ hasPendingIntegration: hasPendingGjcTeamIntegration,
1284
+ writePhase,
1285
+ readSnapshot: readGjcTeamSnapshot,
1286
+ };
1244
1287
 
1245
1288
  function teamModeStatePath(cwd: string, sessionId: string): string {
1246
1289
  return modeStatePath(cwd, sessionId, "team");
@@ -1249,7 +1292,9 @@ function teamModeStatePath(cwd: string, sessionId: string): string {
1249
1292
  export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot, cwd = process.cwd()): Promise<void> {
1250
1293
  const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
1251
1294
  const updatedAt = now();
1252
- const sessionId = resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
1295
+ const sessionId = resolveGjcSessionForWrite(cwd, {
1296
+ envSessionId: process.env.GJC_SESSION_ID,
1297
+ }).gjcSessionId;
1253
1298
  const statePath = teamModeStatePath(cwd, sessionId);
1254
1299
  await writeWorkflowEnvelopeAtomic(
1255
1300
  statePath,
@@ -1272,25 +1317,24 @@ export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot,
1272
1317
  sessionId,
1273
1318
  nowIso: updatedAt,
1274
1319
  },
1275
- audit: { category: "state", verb: "sync-team-summary", owner: "gjc-runtime", skill: "team", sessionId },
1320
+ audit: {
1321
+ category: "state",
1322
+ verb: "sync-team-summary",
1323
+ owner: "gjc-runtime",
1324
+ skill: "team",
1325
+ sessionId,
1326
+ },
1276
1327
  },
1277
1328
  );
1278
- await writeSessionActivityMarker(cwd, sessionId, { writer: "team-runtime", path: statePath });
1279
- }
1280
-
1281
- function appendLivenessRecoveryReason(
1282
- reasons: GjcTeamLivenessRecoveryReason[],
1283
- reason: GjcTeamLivenessRecoveryReason,
1284
- ): void {
1285
- if (!reasons.includes(reason)) reasons.push(reason);
1329
+ await writeSessionActivityMarker(cwd, sessionId, {
1330
+ writer: "team-runtime",
1331
+ path: statePath,
1332
+ });
1286
1333
  }
1287
1334
 
1288
- function isPastTimestamp(value: string | undefined): boolean {
1289
- if (!value) return false;
1290
- const timestamp = Date.parse(value);
1291
- return Number.isFinite(timestamp) && timestamp <= Date.now();
1335
+ function isRecord(value: unknown): value is Record<string, unknown> {
1336
+ return typeof value === "object" && value != null;
1292
1337
  }
1293
-
1294
1338
  function readClaimRecord(value: unknown): GjcTeamTaskClaim | undefined {
1295
1339
  if (!isRecord(value)) return undefined;
1296
1340
  const owner = typeof value.owner === "string" ? value.owner : "";
@@ -1300,162 +1344,17 @@ function readClaimRecord(value: unknown): GjcTeamTaskClaim | undefined {
1300
1344
  return { owner, token, leased_until: leasedUntil };
1301
1345
  }
1302
1346
 
1303
- function isWorkerHeartbeatStale(
1304
- worker: GjcTeamWorker,
1305
- heartbeat: WorkerHeartbeatFile | null,
1306
- env: NodeJS.ProcessEnv,
1307
- ): boolean {
1308
- const thresholdMs = parseDurationEnv(env, "GJC_TEAM_HEARTBEAT_STALE_MS", 120_000);
1309
- if (thresholdMs <= 0) return false;
1310
- const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
1311
- return Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= thresholdMs;
1312
- }
1313
-
1314
- async function detectGjcTeamWorkerLivenessReasons(
1315
- dir: string,
1316
- config: GjcTeamConfig,
1317
- worker: GjcTeamWorker,
1318
- env: NodeJS.ProcessEnv,
1319
- ): Promise<GjcTeamLivenessRecoveryReason[]> {
1320
- const reasons: GjcTeamLivenessRecoveryReason[] = [];
1321
- const lifecycle = await readWorkerLifecycleRecord(dir, worker);
1322
- const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
1323
- if (lifecycle.lifecycle_state === "failed") appendLivenessRecoveryReason(reasons, "worker_lifecycle_failed");
1324
- if (lifecycle.lifecycle_state === "stopped") appendLivenessRecoveryReason(reasons, "worker_lifecycle_stopped");
1325
- if (isWorkerHeartbeatStale(worker, heartbeat, env)) appendLivenessRecoveryReason(reasons, "stale_heartbeat");
1326
- if (!config.dry_run && (!worker.pane_id?.startsWith("%") || !paneBelongsToTeamTarget(config, worker.pane_id)))
1327
- appendLivenessRecoveryReason(reasons, "missing_pane");
1328
- return reasons;
1329
- }
1330
-
1331
- async function reconcileGjcTeamStaleClaims(
1332
- teamName: string,
1333
- dir: string,
1334
- config: GjcTeamConfig,
1335
- env: NodeJS.ProcessEnv,
1336
- ): Promise<GjcTeamLivenessRecoveryResult> {
1337
- const staleWorkers: Record<string, GjcTeamLivenessRecoveryReason[]> = {};
1338
- for (const worker of config.workers) {
1339
- const reasons = await detectGjcTeamWorkerLivenessReasons(dir, config, worker, env);
1340
- if (reasons.length === 0) continue;
1341
- staleWorkers[worker.id] = reasons;
1342
- if (reasons.includes("missing_pane") && reasons.includes("worker_lifecycle_stopped") === false) {
1343
- await writeWorkerLifecycleRecord(dir, worker, "failed", { stop_reason: "pane_missing" });
1344
- }
1345
- }
1346
-
1347
- const recoveredClaims: GjcTeamRecoveredClaim[] = [];
1348
- for (const task of await readTasks(dir)) {
1349
- if (task.status === "completed" || task.status === "failed") continue;
1350
- const claimPath = path.join(dir, "claims", `${task.id}.json`);
1351
- const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
1352
- const claim = task.claim ?? diskClaim;
1353
- if (!claim) continue;
1354
-
1355
- const reasons = [...(staleWorkers[claim.owner] ?? [])];
1356
- if (isPastTimestamp(claim.leased_until)) appendLivenessRecoveryReason(reasons, "claim_expired");
1357
- if (reasons.length === 0) continue;
1358
-
1359
- await fs.rm(claimPath, { force: true });
1360
- recoveredClaims.push({ task_id: task.id, worker: claim.owner, reasons });
1361
- if (task.status !== "in_progress") {
1362
- await appendEvent(dir, {
1363
- type: "task_claim_recovered",
1364
- task_id: task.id,
1365
- worker: claim.owner,
1366
- message: "Removed stale task claim file",
1367
- data: { reasons },
1368
- });
1369
- continue;
1370
- }
1371
-
1372
- const recoveredTask = normalizeTask({
1373
- ...task,
1374
- status: "pending",
1375
- assignee: undefined,
1376
- claim: undefined,
1377
- version: task.version + 1,
1378
- updated_at: now(),
1379
- });
1380
- await writeTask(dir, recoveredTask);
1381
- await appendEvent(dir, {
1382
- type: "task_claim_recovered",
1383
- task_id: task.id,
1384
- worker: claim.owner,
1385
- message: "Recovered task from stale worker claim",
1386
- data: { reasons },
1387
- });
1388
- }
1389
-
1390
- if (recoveredClaims.length > 0)
1391
- await appendTelemetry(dir, {
1392
- type: "team_liveness_recovery",
1393
- message: `Recovered ${recoveredClaims.length} stale team task claim(s)`,
1394
- data: { team_name: teamName, recovered_claims: recoveredClaims },
1395
- });
1396
-
1397
- return { recovered_claims: recoveredClaims, stale_workers: staleWorkers };
1398
- }
1399
-
1400
1347
  export async function recoverGjcTeamStaleClaims(
1401
1348
  teamName: string,
1402
1349
  cwd = process.cwd(),
1403
1350
  env: NodeJS.ProcessEnv = process.env,
1404
1351
  ): Promise<GjcTeamLivenessRecoveryResult> {
1405
1352
  const dir = await findTeamDir(teamName, cwd, env);
1406
- const config = await readConfig(dir);
1407
- return reconcileGjcTeamStaleClaims(teamName, dir, config, env);
1408
- }
1409
- function normalizeOptionalTaskString(value: unknown): string | undefined {
1410
- if (typeof value !== "string") return undefined;
1411
- const trimmed = value.trim();
1412
- return trimmed || undefined;
1413
- }
1414
-
1415
- function normalizeOptionalTaskStringArray(value: unknown): string[] | undefined {
1416
- if (!Array.isArray(value)) return undefined;
1417
- const items = Array.from(
1418
- new Set(value.map(item => (typeof item === "string" ? item.trim() : "")).filter(item => item.length > 0)),
1419
- ).sort();
1420
- return items.length > 0 ? items : undefined;
1421
- }
1422
- type GjcTeamTaskMetadataInput = Partial<
1423
- Pick<GjcTeamTask, "owner" | "lane" | "required_role" | "allowed_roles" | "depends_on" | "blocked_by">
1424
- >;
1425
-
1426
- function taskMetadataFromInput(input: Record<string, unknown>, includeOwner = false): GjcTeamTaskMetadataInput {
1427
- const metadata: GjcTeamTaskMetadataInput = {};
1428
- const owner = normalizeOptionalTaskString(input.owner);
1429
- const lane = normalizeOptionalTaskString(input.lane);
1430
- const requiredRole = normalizeOptionalTaskString(input.required_role ?? input.requiredRole);
1431
- const allowedRoles = normalizeOptionalTaskStringArray(input.allowed_roles ?? input.allowedRoles);
1432
- const dependsOn = normalizeOptionalTaskStringArray(input.depends_on ?? input.dependsOn);
1433
- const blockedBy = normalizeOptionalTaskStringArray(input.blocked_by ?? input.blockedBy);
1434
- if (includeOwner && owner) metadata.owner = owner;
1435
- if (lane) metadata.lane = lane;
1436
- if (requiredRole) metadata.required_role = requiredRole;
1437
- if (allowedRoles) metadata.allowed_roles = allowedRoles;
1438
- if (dependsOn) metadata.depends_on = dependsOn;
1439
- if (blockedBy) metadata.blocked_by = blockedBy;
1440
- return metadata;
1441
- }
1442
-
1443
- function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
1444
- const status = raw.status === ("complete" as GjcTeamTaskStatus) ? "completed" : raw.status;
1445
- return {
1446
- ...raw,
1447
- status,
1448
- subject: raw.subject ?? raw.title,
1449
- description: raw.description ?? raw.objective,
1450
- title: raw.title ?? raw.subject,
1451
- objective: raw.objective ?? raw.description,
1452
- version: raw.version ?? 1,
1453
- lane: normalizeOptionalTaskString(raw.lane),
1454
- required_role: normalizeOptionalTaskString(raw.required_role),
1455
- allowed_roles: normalizeOptionalTaskStringArray(raw.allowed_roles),
1456
- };
1353
+ return withGjcTeamTaskMutation(taskStore(dir), async capability => {
1354
+ const config = await readConfig(dir);
1355
+ return reconcileGjcTeamStaleClaimsUnlocked(workerOrchestrationRuntime, teamName, dir, config, env, capability);
1356
+ });
1457
1357
  }
1458
-
1459
1358
  const GJC_TEAM_INTEGRATION_ATTENTION_STATUSES = new Set<GjcTeamIntegrationStatus>([
1460
1359
  "integration_failed",
1461
1360
  "merge_conflict",
@@ -1500,230 +1399,18 @@ async function resolveGjcTeamSnapshotPhase(
1500
1399
  return (await hasPendingGjcTeamIntegration(dir, config, monitor)) ? "awaiting_integration" : storedPhase;
1501
1400
  }
1502
1401
 
1503
- function isRecord(value: unknown): value is Record<string, unknown> {
1504
- return typeof value === "object" && value != null;
1505
- }
1506
- const GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX = 4_000;
1507
- const GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX = 8_000;
1508
- const GJC_TEAM_COMMAND_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>([
1509
- "passed",
1510
- "failed",
1511
- "not_run",
1512
- ]);
1513
- const GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>(["verified", "rejected"]);
1514
-
1515
- function completionEvidenceError(taskId: string, field: string): Error {
1516
- return new Error(`invalid_completion_evidence:${taskId}:${field}`);
1517
- }
1518
-
1519
- function trimRequiredCompletionEvidenceString(
1520
- taskId: string,
1521
- field: string,
1522
- value: unknown,
1523
- maxLength = GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX,
1524
- ): string {
1525
- if (typeof value !== "string") throw completionEvidenceError(taskId, field);
1526
- const trimmed = value.trim();
1527
- if (!trimmed || trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
1528
- return trimmed;
1529
- }
1530
-
1531
- function trimOptionalCompletionEvidenceString(
1532
- taskId: string,
1533
- field: string,
1534
- value: unknown,
1535
- maxLength = GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX,
1536
- ): string | undefined {
1537
- if (value == null) return undefined;
1538
- if (typeof value !== "string") throw completionEvidenceError(taskId, field);
1539
- const trimmed = value.trim();
1540
- if (!trimmed) return undefined;
1541
- if (trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
1542
- return trimmed;
1543
- }
1544
-
1545
- function normalizeGjcTeamCompletionEvidenceStatus(
1546
- taskId: string,
1547
- kind: GjcTeamTaskCompletionEvidenceKind,
1548
- value: unknown,
1549
- ): GjcTeamTaskCompletionEvidenceStatus {
1550
- const status = trimRequiredCompletionEvidenceString(taskId, "items.status", value);
1551
- const allowed = kind === "command" ? GJC_TEAM_COMMAND_EVIDENCE_STATUSES : GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES;
1552
- if (!allowed.has(status as GjcTeamTaskCompletionEvidenceStatus))
1553
- throw completionEvidenceError(taskId, "items.status");
1554
- return status as GjcTeamTaskCompletionEvidenceStatus;
1555
- }
1556
-
1557
- function normalizeGjcTeamCompletionEvidenceItem(taskId: string, value: unknown): GjcTeamTaskCompletionEvidenceItem {
1558
- if (!isRecord(value) || Array.isArray(value)) throw completionEvidenceError(taskId, "items");
1559
- const kind = trimRequiredCompletionEvidenceString(taskId, "items.kind", value.kind);
1560
- if (kind !== "command" && kind !== "inspection" && kind !== "artifact")
1561
- throw completionEvidenceError(taskId, "items.kind");
1562
- const status = normalizeGjcTeamCompletionEvidenceStatus(taskId, kind, value.status);
1563
- const item: GjcTeamTaskCompletionEvidenceItem = {
1564
- kind,
1565
- status,
1566
- summary: trimRequiredCompletionEvidenceString(taskId, "items.summary", value.summary),
1567
- };
1568
- const command = trimOptionalCompletionEvidenceString(taskId, "items.command", value.command);
1569
- const artifact = trimOptionalCompletionEvidenceString(taskId, "items.artifact", value.artifact);
1570
- const location = trimOptionalCompletionEvidenceString(taskId, "items.location", value.location);
1571
- const output = trimOptionalCompletionEvidenceString(taskId, "items.output", value.output);
1572
- if (kind === "command" && !command) throw completionEvidenceError(taskId, "items.command");
1573
- if (command) item.command = command;
1574
- if (artifact) item.artifact = artifact;
1575
- if (location) item.location = location;
1576
- if (output) item.output = output;
1577
- return item;
1578
- }
1579
-
1580
- function normalizeGjcTeamCompletionEvidenceFiles(taskId: string, value: unknown): string[] | undefined {
1581
- if (value == null) return undefined;
1582
- if (!Array.isArray(value)) throw completionEvidenceError(taskId, "files");
1583
- const files = new Set<string>();
1584
- for (const entry of value) {
1585
- if (typeof entry !== "string") throw completionEvidenceError(taskId, "files");
1586
- const filePath = entry.trim().replace(/\\/g, "/");
1587
- if (!filePath || filePath.includes("\0") || path.isAbsolute(filePath) || filePath.split("/").includes("..")) {
1588
- throw completionEvidenceError(taskId, "files");
1589
- }
1590
- files.add(filePath);
1591
- }
1592
- return files.size > 0 ? [...files].sort() : undefined;
1593
- }
1594
-
1595
- function isGjcTeamCompletionEvidenceItemVerified(item: GjcTeamTaskCompletionEvidenceItem): boolean {
1596
- return (
1597
- (item.kind === "command" && item.status === "passed") ||
1598
- ((item.kind === "inspection" || item.kind === "artifact") && item.status === "verified")
1599
- );
1600
- }
1601
-
1602
- function normalizeGjcTeamTaskCompletionEvidence(
1603
- taskId: string,
1604
- owner: string,
1605
- input: unknown,
1606
- recordedAt = now(),
1607
- ): GjcTeamTaskCompletionEvidence {
1608
- if (!isRecord(input) || Array.isArray(input)) throw new Error(`completion_evidence_required:${taskId}`);
1609
- const itemsValue = input.items;
1610
- if (!Array.isArray(itemsValue) || itemsValue.length === 0) throw completionEvidenceError(taskId, "items");
1611
- const items = itemsValue.map(item => normalizeGjcTeamCompletionEvidenceItem(taskId, item));
1612
- if (!items.some(isGjcTeamCompletionEvidenceItemVerified))
1613
- throw new Error(`completion_evidence_no_verified_item:${taskId}`);
1614
- const evidence: GjcTeamTaskCompletionEvidence = {
1615
- summary: trimRequiredCompletionEvidenceString(taskId, "summary", input.summary),
1616
- items,
1617
- recorded_by: owner,
1618
- recorded_at: recordedAt,
1619
- };
1620
- const files = normalizeGjcTeamCompletionEvidenceFiles(taskId, input.files);
1621
- const notes = trimOptionalCompletionEvidenceString(taskId, "notes", input.notes);
1622
- if (files) evidence.files = files;
1623
- if (notes) evidence.notes = notes;
1624
- return evidence;
1625
- }
1626
-
1627
- function getGjcTeamTaskCompletionEvidenceFailure(task: GjcTeamTask): string | null {
1628
- if (task.status !== "completed") return `task_not_completed:${task.id}`;
1629
- const evidence = task.completion_evidence;
1630
- if (!isRecord(evidence) || Array.isArray(evidence)) return `completion_evidence_required:${task.id}`;
1631
- if (typeof evidence.recorded_by !== "string" || evidence.recorded_by.trim().length === 0)
1632
- return `invalid_completion_evidence:${task.id}:recorded_by`;
1633
- if (typeof evidence.recorded_at !== "string" || evidence.recorded_at.trim().length === 0)
1634
- return `invalid_completion_evidence:${task.id}:recorded_at`;
1635
- try {
1636
- normalizeGjcTeamTaskCompletionEvidence(task.id, evidence.recorded_by.trim(), evidence, evidence.recorded_at);
1637
- return null;
1638
- } catch (error) {
1639
- return error instanceof Error ? error.message : `invalid_completion_evidence:${task.id}:unknown`;
1640
- }
1641
- }
1642
-
1643
- function isGjcTeamTaskCompletionVerified(task: GjcTeamTask): boolean {
1644
- return getGjcTeamTaskCompletionEvidenceFailure(task) == null;
1645
- }
1646
- function roleValuesForWorker(worker: GjcTeamWorker): Set<string> {
1647
- return new Set([worker.role, worker.agent_type].map(value => value.trim()).filter(value => value.length > 0));
1648
- }
1649
-
1650
- function getGjcTeamTaskClaimEligibilityReason(
1651
- task: GjcTeamTask,
1652
- worker: GjcTeamWorker,
1653
- tasks: GjcTeamTask[],
1654
- ): string | null {
1655
- if (task.status !== "pending") return `task_not_pending:${task.id}`;
1656
- if (task.owner && task.owner !== worker.id) return `task_owner_mismatch:${task.id}:${task.owner}`;
1657
- if (task.assignee && task.assignee !== worker.id) return `task_assignee_mismatch:${task.id}:${task.assignee}`;
1658
-
1659
- const workerRoles = roleValuesForWorker(worker);
1660
- if (task.required_role && !workerRoles.has(task.required_role))
1661
- return `task_role_mismatch:${task.id}:${task.required_role}`;
1662
- if (task.allowed_roles?.length && !task.allowed_roles.some(role => workerRoles.has(role)))
1663
- return `task_role_mismatch:${task.id}:${task.allowed_roles.join(",")}`;
1664
-
1665
- if (task.blocked_by?.length) return `task_blocked:${task.id}:${task.blocked_by.join(",")}`;
1666
- for (const dependencyId of task.depends_on ?? []) {
1667
- const dependency = tasks.find(candidate => candidate.id === dependencyId);
1668
- if (!dependency || !isGjcTeamTaskCompletionVerified(dependency))
1669
- return `task_dependency_incomplete:${task.id}:${dependencyId}`;
1670
- }
1671
-
1672
- return null;
1673
- }
1674
-
1675
- async function getActiveClaimReason(dir: string, task: GjcTeamTask): Promise<string | null> {
1676
- const claimPath = path.join(dir, "claims", `${task.id}.json`);
1677
- const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
1678
- const claim = task.claim ?? diskClaim;
1679
- if (!claim || isPastTimestamp(claim.leased_until)) return null;
1680
- return `task_already_claimed:${task.id}`;
1681
- }
1682
- function isGjcTeamTaskRecord(value: unknown): value is GjcTeamTask {
1683
- return (
1684
- isRecord(value) &&
1685
- typeof value.id === "string" &&
1686
- typeof value.status === "string" &&
1687
- (isGjcTeamTaskStatus(value.status) || value.status === "complete") &&
1688
- (typeof value.subject === "string" || typeof value.title === "string") &&
1689
- (typeof value.description === "string" || typeof value.objective === "string")
1690
- );
1691
- }
1692
- function isGjcTeamTaskFile(entry: { isFile(): boolean; name: string }): boolean {
1693
- return entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".evidence.json");
1694
- }
1695
-
1696
- async function readTasks(dir: string): Promise<GjcTeamTask[]> {
1697
- try {
1698
- const entries = await fs.readdir(path.join(dir, "tasks"), { withFileTypes: true });
1699
- const tasks = await Promise.all(
1700
- entries.filter(isGjcTeamTaskFile).map(entry => readJsonFile<unknown>(path.join(dir, "tasks", entry.name))),
1701
- );
1702
- return tasks
1703
- .filter(isGjcTeamTaskRecord)
1704
- .map(normalizeTask)
1705
- .sort((a, b) => a.id.localeCompare(b.id));
1706
- } catch (error) {
1707
- if (isEnoent(error)) return [];
1708
- throw error;
1709
- }
1710
- }
1711
- async function writeTask(dir: string, task: GjcTeamTask): Promise<void> {
1712
- await writeJsonFile(taskPath(dir, task.id), normalizeTask(task));
1713
- }
1714
-
1715
- async function findTeamDir(
1716
- teamName: string,
1717
- cwd = process.cwd(),
1718
- env: NodeJS.ProcessEnv = process.env,
1719
- ): Promise<string> {
1720
- const root = resolveGjcTeamStateRoot(cwd, env);
1721
- const exact = teamDir(root, teamName);
1722
- if (await readJsonFile<GjcTeamConfig>(path.join(exact, "config.json"))) return exact;
1723
- const candidates = await listGjcTeams(cwd, env);
1724
- const input = sanitizeName(teamName);
1725
- const matches = candidates.filter(
1726
- candidate => candidate.team_name === input || sanitizeName(candidate.display_name) === input,
1402
+ async function findTeamDir(
1403
+ teamName: string,
1404
+ cwd = process.cwd(),
1405
+ env: NodeJS.ProcessEnv = process.env,
1406
+ ): Promise<string> {
1407
+ const root = resolveGjcTeamStateRoot(cwd, env);
1408
+ const exact = teamDir(root, teamName);
1409
+ if (await readJsonFile<GjcTeamConfig>(path.join(exact, "config.json"))) return exact;
1410
+ const candidates = await listGjcTeams(cwd, env);
1411
+ const input = sanitizeName(teamName);
1412
+ const matches = candidates.filter(
1413
+ candidate => candidate.team_name === input || sanitizeName(candidate.display_name) === input,
1727
1414
  );
1728
1415
  if (matches.length === 1) return matches[0].state_dir;
1729
1416
  if (matches.length > 1)
@@ -1750,28 +1437,43 @@ function sanitizePathToken(value: string): string {
1750
1437
  return sanitizeName(value) || "default";
1751
1438
  }
1752
1439
  function runGitResult(cwd: string, args: string[]): GitResult {
1753
- const result = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
1440
+ const result = Bun.spawnSync(["git", ...args], {
1441
+ cwd,
1442
+ stdout: "pipe",
1443
+ stderr: "pipe",
1444
+ });
1754
1445
  return {
1755
1446
  ok: result.exitCode === 0,
1756
1447
  stdout: result.stdout.toString().trim(),
1757
1448
  stderr: result.stderr.toString().trim(),
1758
1449
  };
1759
1450
  }
1760
- async function runGitResultAsync(cwd: string, args: string[]): Promise<GitResult> {
1451
+ async function runGitResultAsync(cwd: string, args: string[], signal?: AbortSignal): Promise<GitResult> {
1761
1452
  const result = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
1762
- const [exitCode, stdout, stderr] = await Promise.all([
1763
- result.exited,
1764
- new Response(result.stdout).text(),
1765
- new Response(result.stderr).text(),
1766
- ]);
1767
- return {
1768
- ok: exitCode === 0,
1769
- stdout: stdout.trim(),
1770
- stderr: stderr.trim(),
1453
+ const kill = () => {
1454
+ try {
1455
+ result.kill("SIGKILL");
1456
+ } catch {}
1771
1457
  };
1458
+ if (signal?.aborted) kill();
1459
+ else signal?.addEventListener("abort", kill, { once: true });
1460
+ try {
1461
+ const [exitCode, stdout, stderr] = await Promise.all([
1462
+ result.exited,
1463
+ new Response(result.stdout).text(),
1464
+ new Response(result.stderr).text(),
1465
+ ]);
1466
+ return {
1467
+ ok: exitCode === 0 && !signal?.aborted,
1468
+ stdout: stdout.trim(),
1469
+ stderr: signal?.aborted ? "worker integration request aborted" : stderr.trim(),
1470
+ };
1471
+ } finally {
1472
+ signal?.removeEventListener("abort", kill);
1473
+ }
1772
1474
  }
1773
- async function tryRunGitAsync(cwd: string, args: string[]): Promise<string | null> {
1774
- const result = await runGitResultAsync(cwd, args);
1475
+ async function tryRunGitAsync(cwd: string, args: string[], signal?: AbortSignal): Promise<string | null> {
1476
+ const result = await runGitResultAsync(cwd, args, signal);
1775
1477
  return result.ok ? result.stdout : null;
1776
1478
  }
1777
1479
  function runGit(cwd: string, args: string[]): string {
@@ -1787,7 +1489,10 @@ function isGitRepository(cwd: string): boolean {
1787
1489
  return tryRunGit(cwd, ["rev-parse", "--show-toplevel"]) != null;
1788
1490
  }
1789
1491
 
1790
- function parseWorktreeMode(args: string[]): { mode: GjcTeamWorktreeMode; remainingArgs: string[] } {
1492
+ function parseWorktreeMode(args: string[]): {
1493
+ mode: GjcTeamWorktreeMode;
1494
+ remainingArgs: string[];
1495
+ } {
1791
1496
  let mode: GjcTeamWorktreeMode = { enabled: false };
1792
1497
  const remainingArgs: string[] = [];
1793
1498
  for (let index = 0; index < args.length; index += 1) {
@@ -1960,7 +1665,10 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
1960
1665
  const args = displayTarget
1961
1666
  ? ["display-message", "-p", "-t", displayTarget, "#S:#I #{pane_id}"]
1962
1667
  : ["display-message", "-p", "#S:#I #{pane_id}"];
1963
- const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe" });
1668
+ const result = Bun.spawnSync([tmuxCommand, ...args], {
1669
+ stdout: "pipe",
1670
+ stderr: "pipe",
1671
+ });
1964
1672
  if (result.exitCode !== 0) {
1965
1673
  // Distinguish "you are not inside any tmux session" from a genuine tmux
1966
1674
  // query failure so the caller gets actionable guidance instead of raw
@@ -1993,7 +1701,12 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
1993
1701
  ),
1994
1702
  );
1995
1703
  }
1996
- return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
1704
+ return {
1705
+ sessionName,
1706
+ windowIndex,
1707
+ leaderPaneId,
1708
+ target: `${sessionName}:${windowIndex}`,
1709
+ };
1997
1710
  }
1998
1711
  function isBunVirtualPath(candidate: string | undefined): boolean {
1999
1712
  const normalized = candidate?.trim().replace(/\\/g, "/").toLowerCase();
@@ -2060,85 +1773,7 @@ export function resolveGjcWorkerCommand(
2060
1773
  if (!executable) throw unresolvedWorkerAuthorityError();
2061
1774
  return formatWorkerExecutable(platform, executable);
2062
1775
  }
2063
- /** @internal Exported for unit tests. */
2064
- export function buildWorkerCommand(
2065
- config: GjcTeamConfig,
2066
- worker: GjcTeamWorker,
2067
- platform: NodeJS.Platform = process.platform,
2068
- ): string {
2069
- const quote = platform === "win32" ? powershellQuote : shellQuote;
2070
- const envAssignment = (key: string, value: string): string =>
2071
- platform === "win32" ? `$env:${key} = ${quote(value)};` : `${key}=${quote(value)}`;
2072
- const workspace = worker.worktree_path
2073
- ? `Worker worktree: ${worker.worktree_path}.`
2074
- : `Worker cwd: ${config.leader.cwd}.`;
2075
- // The worker prompt body must stay single-line because fallback startup on
2076
- // Windows/psmux dispatches the command through `tmux send-keys`, where
2077
- // embedded LF characters are treated as Enter keypresses. POSIX tmux starts
2078
- // workers by passing the command directly to `split-window`, but keeping the
2079
- // body normalized there too makes fake-tmux logs and shell argv handling
2080
- // deterministic. Strip a defensive U+FEFF in case a caller managed to inject
2081
- // a UTF-8 BOM into the task text. Empty / whitespace-only bodies fall back to
2082
- // a one-line placeholder so the worker never sits idle at an empty prompt.
2083
- const normalizePrompt = (raw: string): string =>
2084
- raw
2085
- .replace(/[\uFEFF\u200B]/g, "")
2086
- .replace(/\r?\n+/g, " ")
2087
- .trim();
2088
- const rawPrompt = [
2089
- `You are ${worker.id} in gjc team ${config.team_name}.`,
2090
- `Team state root: ${config.state_root}.`,
2091
- workspace,
2092
- `Team brief (context only): ${config.task}`,
2093
- "Before implementation, claim your worker-owned task and treat the claimed task record as the source of truth. Do not implement directly from the broad team brief.",
2094
- `Before claiming work, send startup ACK: gjc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
2095
- `Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; keep heartbeat current during long work, record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
2096
- ].join("\n");
2097
- const prompt = normalizePrompt(rawPrompt) || `Worker ${worker.id} ready.`;
2098
- const envLines = [
2099
- envAssignment("GJC_TEAM_WORKER", `${config.team_name}/${worker.id}`),
2100
- envAssignment("GJC_TEAM_INTERNAL_WORKER", `${config.team_name}/${worker.id}`),
2101
- envAssignment("GJC_TEAM_NAME", config.team_name),
2102
- envAssignment("GJC_TEAM_WORKER_ID", worker.id),
2103
- envAssignment("GJC_TEAM_STATE_ROOT", config.state_root),
2104
- envAssignment("GJC_TEAM_LEADER_CWD", config.leader.cwd),
2105
- envAssignment("GJC_TEAM_DISPLAY_NAME", config.display_name),
2106
- // Canonical GJC spawn-provenance marker so `notifications.sessionScope =
2107
- // "primary"` suppresses the worker's own notification endpoint/topic. The
2108
- // value is informational (leader session id, falling back to the team name
2109
- // so it is always non-blank); presence is what marks the worker.
2110
- envAssignment(SPAWN_PROVENANCE_ENV, config.leader.session_id.trim() || config.team_name),
2111
- ...(worker.worktree_path ? [envAssignment("GJC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
2112
- ];
2113
- const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
2114
- // On Windows we wrap the worker command invocation in `& { & 'cmd' 'arg1' ... }`
2115
- // so pwsh keeps the whole multi-statement body in command position. Two
2116
- // failure modes this avoids:
2117
- // 1. Bare `bun 'cli.ts' 'prompt'` after `$env:X = 'y'; ...` would be
2118
- // parsed in expression position and PowerShell would reject the
2119
- // second quoted token with "Unexpected token '<cli.ts>'".
2120
- // 2. `& { 'cmd' 'arg1' 'arg2' }` (single & inside a block with adjacent
2121
- // single-quoted tokens) is itself invalid because pwsh does not
2122
- // concatenate adjacent single-quoted strings inside a script block.
2123
- // The nested `&` inside the block forces command-position parsing for
2124
- // the invocation. POSIX shells do not need this — they already treat
2125
- // `cmd 'arg'` as a normal command invocation after `;`-separated
2126
- // variable assignments.
2127
- //
2128
- // ASSUMPTION: this branch only fires when the worker pane is a PowerShell
2129
- // shell. psmux launches pwsh by default on Windows, so the unset /
2130
- // unset-and-replace cases are correct. If a user has explicitly set
2131
- // `set -g default-shell "C:/Program Files/Git/bin/bash.exe"` (or any
2132
- // other non-pwsh shell), this branch will send PowerShell syntax to a
2133
- // bash pane and the worker will fail with a parse error. Detecting the
2134
- // pane's shell at runtime and switching the quoting + invocation style
2135
- // accordingly is a follow-up; for now the pwsh default is assumed.
2136
- if (platform === "win32") {
2137
- const invocation = `& ${config.worker_command} ${quote(prompt)}`;
2138
- return `& { ${joined} ${invocation} }`;
2139
- }
2140
- return `${joined} ${config.worker_command} ${quote(prompt)}`;
2141
- }
1776
+ export { buildWorkerCommand } from "./team-launch";
2142
1777
 
2143
1778
  function shouldDispatchWorkerWithSendKeys(tmuxCommand: string, platform: NodeJS.Platform = process.platform): boolean {
2144
1779
  return platform === "win32" || path.basename(tmuxCommand).toLowerCase() === "psmux";
@@ -2238,7 +1873,11 @@ async function startTmuxSession(
2238
1873
  dryRun: boolean,
2239
1874
  env: NodeJS.ProcessEnv = process.env,
2240
1875
  ): Promise<GjcTeamWorker[]> {
2241
- if (dryRun) return config.workers.map(worker => ({ ...worker, pane_id: `%dry-run-${worker.id}` }));
1876
+ if (dryRun)
1877
+ return config.workers.map(worker => ({
1878
+ ...worker,
1879
+ pane_id: `%dry-run-${worker.id}`,
1880
+ }));
2242
1881
  const rollbackPaneIds: string[] = [];
2243
1882
  try {
2244
1883
  const workers: GjcTeamWorker[] = [];
@@ -2344,12 +1983,18 @@ async function startTmuxSession(
2344
1983
  await appendTelemetry(dir, {
2345
1984
  type: "tmux_started",
2346
1985
  message: "Started gjc team worker panes in current tmux window",
2347
- data: { tmux_target: config.tmux_target, panes: workers.map(worker => worker.pane_id).filter(Boolean) },
1986
+ data: {
1987
+ tmux_target: config.tmux_target,
1988
+ panes: workers.map(worker => worker.pane_id).filter(Boolean),
1989
+ },
2348
1990
  });
2349
1991
  return workers;
2350
1992
  } catch (error) {
2351
1993
  for (const paneId of rollbackPaneIds)
2352
- Bun.spawnSync([config.tmux_command, "kill-pane", "-t", paneId], { stdout: "ignore", stderr: "ignore" });
1994
+ Bun.spawnSync([config.tmux_command, "kill-pane", "-t", paneId], {
1995
+ stdout: "ignore",
1996
+ stderr: "ignore",
1997
+ });
2353
1998
  throw error;
2354
1999
  }
2355
2000
  }
@@ -2400,7 +2045,9 @@ function commitHygieneLedgerPath(config: GjcTeamConfig): string {
2400
2045
  return path.join(
2401
2046
  sessionReportsDir(
2402
2047
  config.leader_cwd,
2403
- resolveGjcSessionForWrite(config.leader_cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId,
2048
+ resolveGjcSessionForWrite(config.leader_cwd, {
2049
+ envSessionId: process.env.GJC_SESSION_ID,
2050
+ }).gjcSessionId,
2404
2051
  ),
2405
2052
  "team-commit-hygiene",
2406
2053
  `${config.team_name}.ledger.json`,
@@ -2413,7 +2060,12 @@ function integrationNowState(
2413
2060
  }
2414
2061
  async function appendIntegrationReport(
2415
2062
  dir: string,
2416
- entry: { worker: string; operation: "merge" | "cherry-pick" | "rebase"; files: string[]; detail: string },
2063
+ entry: {
2064
+ worker: string;
2065
+ operation: "merge" | "cherry-pick" | "rebase";
2066
+ files: string[];
2067
+ detail: string;
2068
+ },
2417
2069
  ): Promise<void> {
2418
2070
  const line = `- [${now()}] ${entry.worker}: ${entry.operation}; files=${entry.files.join(",") || "unknown"}; ${entry.detail}\n`;
2419
2071
  if (await pathExists(integrationReportPath(dir)))
@@ -2432,17 +2084,23 @@ async function appendIntegrationReport(
2432
2084
  async function appendCommitHygieneEntries(config: GjcTeamConfig, entries: GjcTeamCommitHygieneEntry[]): Promise<void> {
2433
2085
  if (entries.length === 0) return;
2434
2086
  const ledgerPath = commitHygieneLedgerPath(config);
2435
- const existing = (await readJsonFile<{ version: number; entries: GjcTeamCommitHygieneEntry[] }>(ledgerPath)) ?? {
2087
+ const existing = (await readJsonFile<{
2088
+ version: number;
2089
+ entries: GjcTeamCommitHygieneEntry[];
2090
+ }>(ledgerPath)) ?? {
2436
2091
  version: 1,
2437
2092
  entries: [],
2438
2093
  };
2439
- await writeJsonFile(ledgerPath, { version: 1, entries: [...existing.entries, ...entries] });
2094
+ await writeJsonFile(ledgerPath, {
2095
+ version: 1,
2096
+ entries: [...existing.entries, ...entries],
2097
+ });
2440
2098
  }
2441
2099
  function resolveHead(cwd: string): string | null {
2442
2100
  return tryRunGit(cwd, ["rev-parse", "HEAD"]);
2443
2101
  }
2444
- async function resolveHeadAsync(cwd: string): Promise<string | null> {
2445
- return tryRunGitAsync(cwd, ["rev-parse", "HEAD"]);
2102
+ async function resolveHeadAsync(cwd: string, signal?: AbortSignal): Promise<string | null> {
2103
+ return tryRunGitAsync(cwd, ["rev-parse", "HEAD"], signal);
2446
2104
  }
2447
2105
  function isAncestor(cwd: string, ancestor: string, descendant: string): boolean {
2448
2106
  return runGitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok;
@@ -2463,8 +2121,8 @@ function listConflictFiles(cwd: string): string[] {
2463
2121
  .map(line => line.trim())
2464
2122
  .filter(Boolean);
2465
2123
  }
2466
- async function listConflictFilesAsync(cwd: string): Promise<string[]> {
2467
- const result = await runGitResultAsync(cwd, ["diff", "--name-only", "--diff-filter=U"]);
2124
+ async function listConflictFilesAsync(cwd: string, signal?: AbortSignal): Promise<string[]> {
2125
+ const result = await runGitResultAsync(cwd, ["diff", "--name-only", "--diff-filter=U"], signal);
2468
2126
  if (!result.ok || !result.stdout) return [];
2469
2127
  return result.stdout
2470
2128
  .split(/\r?\n/)
@@ -2502,7 +2160,10 @@ function normalizeGitStatusPath(filePath: string): string {
2502
2160
  return (filePath.split(" -> ").at(-1) ?? filePath).replace(/\\/g, "/").replace(/^\.\//, "");
2503
2161
  }
2504
2162
 
2505
- export function classifyGjcTeamCheckpointFiles(files: string[]): { eligible: string[]; protected: string[] } {
2163
+ export function classifyGjcTeamCheckpointFiles(files: string[]): {
2164
+ eligible: string[];
2165
+ protected: string[];
2166
+ } {
2506
2167
  const eligible: string[] = [];
2507
2168
  const protectedFiles: string[] = [];
2508
2169
  for (const file of files) {
@@ -2521,7 +2182,11 @@ export function classifyGjcTeamCheckpointFiles(files: string[]): { eligible: str
2521
2182
  export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpointClassification {
2522
2183
  const status = runGitResult(cwd, ["status", "--porcelain", "-uall"]);
2523
2184
  if (!status.ok) {
2524
- return { kind: "git_error", files: [], detail: status.stderr || status.stdout || "git status failed" };
2185
+ return {
2186
+ kind: "git_error",
2187
+ files: [],
2188
+ detail: status.stderr || status.stdout || "git status failed",
2189
+ };
2525
2190
  }
2526
2191
  if (!status.stdout.trim()) return { kind: "clean", files: [] };
2527
2192
  const files = parsePorcelainStatusFiles(status.stdout);
@@ -2531,17 +2196,27 @@ export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpoint
2531
2196
  .some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
2532
2197
  const conflictFiles = listConflictFiles(cwd);
2533
2198
  if (hasUnmergedStatus || conflictFiles.length > 0) {
2534
- return { kind: "conflicted", files: conflictFiles.length > 0 ? conflictFiles : files };
2199
+ return {
2200
+ kind: "conflicted",
2201
+ files: conflictFiles.length > 0 ? conflictFiles : files,
2202
+ };
2535
2203
  }
2536
2204
  const classified = classifyGjcTeamCheckpointFiles(files);
2537
2205
  if (classified.eligible.length === 0 && classified.protected.length > 0)
2538
2206
  return { kind: "protected_only", files: classified.protected };
2539
2207
  return { kind: "eligible", files: classified.eligible };
2540
2208
  }
2541
- export async function classifyWorkerCheckpointStatusAsync(cwd: string): Promise<GjcWorkerCheckpointClassification> {
2542
- const status = await runGitResultAsync(cwd, ["status", "--porcelain", "-uall"]);
2209
+ export async function classifyWorkerCheckpointStatusAsync(
2210
+ cwd: string,
2211
+ signal?: AbortSignal,
2212
+ ): Promise<GjcWorkerCheckpointClassification> {
2213
+ const status = await runGitResultAsync(cwd, ["status", "--porcelain", "-uall"], signal);
2543
2214
  if (!status.ok) {
2544
- return { kind: "git_error", files: [], detail: status.stderr || status.stdout || "git status failed" };
2215
+ return {
2216
+ kind: "git_error",
2217
+ files: [],
2218
+ detail: status.stderr || status.stdout || "git status failed",
2219
+ };
2545
2220
  }
2546
2221
  if (!status.stdout.trim()) return { kind: "clean", files: [] };
2547
2222
  const files = parsePorcelainStatusFiles(status.stdout);
@@ -2549,9 +2224,12 @@ export async function classifyWorkerCheckpointStatusAsync(cwd: string): Promise<
2549
2224
  .split(/\r?\n/)
2550
2225
  .filter(Boolean)
2551
2226
  .some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
2552
- const conflictFiles = await listConflictFilesAsync(cwd);
2227
+ const conflictFiles = await listConflictFilesAsync(cwd, signal);
2553
2228
  if (hasUnmergedStatus || conflictFiles.length > 0) {
2554
- return { kind: "conflicted", files: conflictFiles.length > 0 ? conflictFiles : files };
2229
+ return {
2230
+ kind: "conflicted",
2231
+ files: conflictFiles.length > 0 ? conflictFiles : files,
2232
+ };
2555
2233
  }
2556
2234
  const classified = classifyGjcTeamCheckpointFiles(files);
2557
2235
  if (classified.eligible.length === 0 && classified.protected.length > 0)
@@ -2613,7 +2291,11 @@ function autoCommitDirtyWorker(worker: GjcTeamWorker): {
2613
2291
  const message = `gjc(team): auto-checkpoint ${worker.id} [${worker.assigned_tasks[0] ?? "unknown"}]`;
2614
2292
  if (!runGitResult(worker.worktree_path, ["commit", "--no-verify", "-m", message]).ok)
2615
2293
  return { ...empty, classification };
2616
- return { committed: true, commit: resolveHead(worker.worktree_path), classification };
2294
+ return {
2295
+ committed: true,
2296
+ commit: resolveHead(worker.worktree_path),
2297
+ classification,
2298
+ };
2617
2299
  }
2618
2300
  function workerMergeRef(worker: GjcTeamWorker, workerHead: string): string {
2619
2301
  if (!worker.worktree_path) return workerHead;
@@ -2719,7 +2401,10 @@ async function integrateGjcWorkerCommits(
2719
2401
  detail: "Leader created a runtime merge commit to integrate worker history.",
2720
2402
  });
2721
2403
  } else {
2722
- integrationByWorker[worker.id] = { ...state, ...integrationNowState("integration_failed") };
2404
+ integrationByWorker[worker.id] = {
2405
+ ...state,
2406
+ ...integrationNowState("integration_failed"),
2407
+ };
2723
2408
  hygieneEntries.push({
2724
2409
  recorded_at: now(),
2725
2410
  operation: "integration_merge",
@@ -2838,7 +2523,10 @@ async function integrateGjcWorkerCommits(
2838
2523
  }
2839
2524
  const newLeaderHead = resolveHead(leaderCwd);
2840
2525
  if (!newLeaderHead || newLeaderHead === leaderHead) {
2841
- integrationByWorker[worker.id] = { ...state, ...integrationNowState("integration_failed") };
2526
+ integrationByWorker[worker.id] = {
2527
+ ...state,
2528
+ ...integrationNowState("integration_failed"),
2529
+ };
2842
2530
  hygieneEntries.push({
2843
2531
  recorded_at: now(),
2844
2532
  operation: "integration_cherry_pick",
@@ -3007,158 +2695,43 @@ async function integrateGjcWorkerCommits(
3007
2695
  return integrationByWorker;
3008
2696
  }
3009
2697
 
3010
- async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promise<void> {
3011
- // Empty mailbox directories are runtime state, so they must exist before messages arrive.
3012
- await fs.mkdir(path.join(dir, "mailbox"), { recursive: true });
3013
- for (const worker of workers) {
3014
- await fs.mkdir(mailboxDirPath(dir, worker.id), { recursive: true });
3015
- await writeJsonFile(mailboxPath(dir, worker.id), { messages: [] });
3016
- await writeJsonFile(path.join(workerDir(dir, worker.id), "status.json"), { state: "idle", updated_at: now() });
3017
- await writeJsonFile(workerLifecyclePath(dir, worker.id), {
3018
- worker: worker.id,
3019
- lifecycle_state: "starting",
3020
- worker_status_state: "idle",
3021
- updated_at: now(),
3022
- } satisfies GjcTeamWorkerLifecycle);
3023
- await writeJsonFile(path.join(workerDir(dir, worker.id), "heartbeat.json"), {
3024
- pid: 0,
3025
- last_turn_at: now(),
3026
- turn_count: 0,
3027
- alive: true,
3028
- });
3029
- }
3030
- // Empty leader mailbox directory is runtime state, so it must exist before messages arrive.
3031
- await fs.mkdir(mailboxDirPath(dir, "leader-fixed"), { recursive: true });
3032
- await writeJsonFile(mailboxPath(dir, "leader-fixed"), { messages: [] });
3033
- }
2698
+ const writeInitialGjcTeamTask = (dir: string, task: GjcTeamTask) =>
2699
+ withGjcTeamTaskMutation(taskStore(dir), capability => capability.writeRecovered(task));
3034
2700
 
3035
2701
  export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTeamSnapshot> {
3036
- const cwd = options.cwd ?? process.cwd();
3037
- const env = options.env ?? process.env;
3038
- if (options.mailboxDeliveryTransport) setGjcTeamMailboxDeliveryTransport(options.mailboxDeliveryTransport);
3039
- if (!Number.isInteger(options.workerCount) || options.workerCount < 1 || options.workerCount > GJC_TEAM_MAX_WORKERS)
3040
- throw new Error(`invalid_team_worker_count:${options.workerCount}:expected_1_${GJC_TEAM_MAX_WORKERS}`);
3041
- const workerCliPlan = resolveGjcTeamWorkerCliPlan(options.workerCount, env);
3042
- const stateRoot = resolveGjcTeamStateRoot(cwd, env);
3043
- const teamName = sanitizeName(options.teamName ?? makeTeamName(options.task, env));
3044
- const displayName = sanitizeName(options.teamName ?? options.task).slice(0, 30) || teamName;
3045
- const dir = teamDir(stateRoot, teamName);
3046
- const createdAt = now();
3047
- const worktreeMode = resolveDefaultWorktreeMode(options.worktreeMode);
3048
- const platform = options.platform ?? process.platform;
3049
- const tmuxBinary = resolveGjcTmuxBinary({ env, platform });
3050
- const tmuxCommand = tmuxBinary.command;
3051
- const tmuxContext = options.dryRun
3052
- ? { sessionName: "dry-run", windowIndex: "0", leaderPaneId: "%dry-run-leader", target: "dry-run:0" }
3053
- : readCurrentTmuxLeaderContext(tmuxCommand, env);
3054
- const initialWorkers = buildWorkers(options.workerCount, options.agentType, stateRoot);
3055
- const initialTasks = buildInitialTasks(options.task, initialWorkers);
3056
- const workers: GjcTeamWorker[] = [];
3057
- try {
3058
- for (const worker of initialWorkers)
3059
- workers.push(
3060
- options.dryRun
3061
- ? worker
3062
- : await ensureWorkerWorktree(cwd, dir, teamName, worker, worktreeMode, platform, tmuxBinary.isPsmux),
3063
- );
3064
- } catch (error) {
3065
- await rollbackCreatedWorktrees(workers);
3066
- throw error;
3067
- }
3068
- const config: GjcTeamConfig = {
3069
- team_name: teamName,
3070
- display_name: displayName,
3071
- requested_name: options.teamName ?? displayName,
3072
- task: options.task,
3073
- agent_type: options.agentType,
3074
- worker_count: options.workerCount,
3075
- max_workers: GJC_TEAM_MAX_WORKERS,
3076
- state_root: stateRoot,
3077
- worker_command: resolveGjcWorkerCommand(cwd, env),
3078
- worker_cli_plan: workerCliPlan,
3079
- tmux_command: tmuxCommand,
3080
- tmux_session: tmuxContext.sessionName,
3081
- tmux_session_name: tmuxContext.sessionName,
3082
- tmux_target: tmuxContext.target,
3083
- workspace_mode: worktreeMode.enabled ? "worktree" : "direct",
3084
- dry_run: options.dryRun ?? false,
3085
- leader: { session_id: env.GJC_SESSION_ID ?? env.CODEX_SESSION_ID ?? "", pane_id: tmuxContext.leaderPaneId, cwd },
3086
- leader_cwd: cwd,
3087
- team_state_root: stateRoot,
3088
- workers,
3089
- created_at: createdAt,
3090
- updated_at: createdAt,
3091
- };
3092
- await initializeStateDirs(dir, config.workers);
3093
- await writeJsonFile(path.join(dir, "config.json"), config);
3094
- await writeJsonFile(path.join(dir, "manifest.v2.json"), {
3095
- version: 2,
3096
- team_name: config.team_name,
3097
- display_name: config.display_name,
3098
- requested_name: config.requested_name,
3099
- tmux_session: config.tmux_session,
3100
- tmux_session_name: config.tmux_session_name,
3101
- tmux_target: config.tmux_target,
3102
- worker_command: config.worker_command,
3103
- worker_cli_plan: config.worker_cli_plan,
3104
- tmux_command: config.tmux_command,
3105
- leader: config.leader,
3106
- workers: config.workers,
3107
- workspace_mode: config.workspace_mode,
3108
- dry_run: config.dry_run,
3109
- created_at: createdAt,
3110
- updated_at: createdAt,
3111
- });
3112
- await writePhase(dir, "starting");
3113
- for (const task of initialTasks) await writeTask(dir, task);
3114
- await appendEvent(dir, {
3115
- type: "team_started",
3116
- message: options.dryRun
3117
- ? "Created native gjc team dry-run state without starting tmux workers"
3118
- : "Started native gjc team runtime",
3119
- data: {
3120
- worker_count: options.workerCount,
3121
- agent_type: options.agentType,
3122
- workspace_mode: config.workspace_mode,
3123
- dry_run: config.dry_run,
3124
- },
3125
- });
3126
- await appendTelemetry(dir, {
3127
- type: "team_runtime",
3128
- message: options.dryRun ? "Native gjc team dry-run state initialized" : "Native gjc team runtime initialized",
3129
- data: {
3130
- state_root: stateRoot,
3131
- worker_command: config.worker_command,
3132
- worker_cli_plan: workerCliPlan,
3133
- workspace_mode: config.workspace_mode,
3134
- dry_run: config.dry_run,
2702
+ return startGjcTeamLaunch(
2703
+ {
2704
+ maxWorkers: GJC_TEAM_MAX_WORKERS,
2705
+ resolveWorkerCliPlan: resolveGjcTeamWorkerCliPlan,
2706
+ resolveStateRoot: resolveGjcTeamStateRoot,
2707
+ sanitizeName,
2708
+ makeTeamName,
2709
+ teamDir,
2710
+ resolveDefaultWorktreeMode,
2711
+ resolveTmuxBinary: resolveGjcTmuxBinary,
2712
+ readTmuxLeaderContext: readCurrentTmuxLeaderContext,
2713
+ buildWorkers,
2714
+ buildInitialTasks,
2715
+ ensureWorkerWorktree,
2716
+ rollbackCreatedWorktrees,
2717
+ resolveWorkerCommand: resolveGjcWorkerCommand,
2718
+ mailboxDirPath,
2719
+ mailboxPath,
2720
+ workerDir,
2721
+ workerLifecyclePath,
2722
+ writeJson: writeJsonFile,
2723
+ now,
2724
+ writePhase,
2725
+ writeTask: writeInitialGjcTeamTask,
2726
+ appendEvent,
2727
+ appendTelemetry,
2728
+ startTmuxSession,
2729
+ killWorkerPanes,
2730
+ writeWorkerLifecycleForConfig,
2731
+ readSnapshot: readGjcTeamSnapshot,
3135
2732
  },
3136
- });
3137
- let tmuxWorkers: GjcTeamWorker[];
3138
- try {
3139
- tmuxWorkers = await startTmuxSession(config, dir, options.dryRun ?? false, env);
3140
- } catch (error) {
3141
- await writePhase(dir, "failed");
3142
- await appendEvent(dir, {
3143
- type: "team_start_failed",
3144
- message: error instanceof Error ? error.message : String(error),
3145
- });
3146
- killWorkerPanes(config);
3147
- await rollbackCreatedWorktrees(config.workers);
3148
- throw error;
3149
- }
3150
- const runningConfig = {
3151
- ...config,
3152
- workers: tmuxWorkers.map(worker => ({ ...worker, status: "idle" as const, last_heartbeat: now() })),
3153
- updated_at: now(),
3154
- };
3155
- await writeJsonFile(path.join(dir, "config.json"), runningConfig);
3156
- await writeWorkerLifecycleForConfig(dir, runningConfig, "starting", worker => ({
3157
- pane_id: worker.pane_id,
3158
- started_at: runningConfig.created_at,
3159
- }));
3160
- await writePhase(dir, "running");
3161
- return readGjcTeamSnapshot(teamName, cwd, env);
2733
+ options,
2734
+ );
3162
2735
  }
3163
2736
 
3164
2737
  export async function readGjcTeamSnapshot(
@@ -3183,8 +2756,8 @@ export async function readGjcTeamSnapshot(
3183
2756
  const workerLifecycleById = await readWorkerLifecycleById(dir, config);
3184
2757
  const notificationSummary =
3185
2758
  options.reconcileNotifications === true
3186
- ? await reconcileTeamNotifications(dir, config)
3187
- : summarizeNotifications(await listNotificationRecords(dir));
2759
+ ? await reconcileTeamNotifications(teamNotificationRuntime, dir, config)
2760
+ : summarizeTeamNotifications(await listNotificationRecords(dir));
3188
2761
  const phase = await resolveGjcTeamSnapshotPhase(dir, config, storedPhase, tasks, monitor);
3189
2762
  return {
3190
2763
  team_name: config.team_name,
@@ -3218,6 +2791,7 @@ function workerIntegrationFingerprint(head: string | null, classification: GjcWo
3218
2791
  export async function requestGjcWorkerIntegrationAttempt(
3219
2792
  cwd = process.cwd(),
3220
2793
  env: NodeJS.ProcessEnv = process.env,
2794
+ options: GjcWorkerIntegrationAttemptOptions = {},
3221
2795
  ): Promise<GjcWorkerIntegrationAttemptRequestResult> {
3222
2796
  const teamName = env.GJC_TEAM_NAME?.trim();
3223
2797
  const worker = env.GJC_TEAM_WORKER_ID?.trim() || env.GJC_TEAM_INTERNAL_WORKER?.split("/").pop()?.trim();
@@ -3227,19 +2801,63 @@ export async function requestGjcWorkerIntegrationAttempt(
3227
2801
  const configuredWorker = config.workers.find(candidate => candidate.id === worker);
3228
2802
  const worktreePath = env.GJC_TEAM_WORKTREE_PATH?.trim() || configuredWorker?.worktree_path;
3229
2803
  if (!worktreePath || !(await pathExists(worktreePath)))
3230
- return { requested: false, reason: "missing_worktree", worker, team_name: teamName };
2804
+ return {
2805
+ requested: false,
2806
+ reason: "missing_worktree",
2807
+ worker,
2808
+ team_name: teamName,
2809
+ };
3231
2810
  const [classification, head] = await Promise.all([
3232
- classifyWorkerCheckpointStatusAsync(worktreePath),
3233
- resolveHeadAsync(worktreePath),
2811
+ classifyWorkerCheckpointStatusAsync(worktreePath, options.signal),
2812
+ resolveHeadAsync(worktreePath, options.signal),
3234
2813
  ]);
3235
2814
  if (classification.kind === "git_error") {
2815
+ const reportWorker = configuredWorker ?? {
2816
+ id: worker,
2817
+ name: worker,
2818
+ index: 0,
2819
+ agent_type: "unknown",
2820
+ role: "unknown",
2821
+ status: "idle",
2822
+ last_heartbeat: now(),
2823
+ assigned_tasks: [],
2824
+ };
2825
+ const detail = classification.detail;
2826
+ await appendIntegrationEvent(dir, "worker_integration_attempt_failed", reportWorker, {
2827
+ worker_name: worker,
2828
+ worktree_path: worktreePath,
2829
+ detail,
2830
+ retryable: true,
2831
+ summary: `retryable worker integration request failure for ${worker}: ${detail}`,
2832
+ });
2833
+ await notifyLeader(
2834
+ config,
2835
+ reportWorker,
2836
+ `INTEGRATION RETRYABLE FAILURE: ${worker} git inspection failed (${detail}). A later turn can retry.`,
2837
+ cwd,
2838
+ env,
2839
+ );
3236
2840
  return { requested: false, reason: "git_error", worker, team_name: teamName, head, status: classification.kind };
3237
2841
  }
3238
2842
  if (classification.kind === "protected_only") {
3239
- return { requested: false, reason: "no_changes", worker, team_name: teamName, head, status: classification.kind };
2843
+ return {
2844
+ requested: false,
2845
+ reason: "no_changes",
2846
+ worker,
2847
+ team_name: teamName,
2848
+ head,
2849
+ status: classification.kind,
2850
+ };
3240
2851
  }
3241
2852
  if (classification.kind === "clean" && configuredWorker?.worktree_base_ref === head) {
3242
- return { requested: false, reason: "no_changes", worker, team_name: teamName, head, status: classification.kind };
2853
+ return {
2854
+ requested: false,
2855
+ reason: "no_changes",
2856
+ worker,
2857
+ team_name: teamName,
2858
+ head,
2859
+ status: classification.kind,
2860
+ };
3243
2861
  }
3244
2862
  const fingerprint = workerIntegrationFingerprint(head, classification);
3245
2863
  const dedupePath = workerIntegrationDedupePath(dir, worker);
@@ -3265,7 +2883,12 @@ export async function requestGjcWorkerIntegrationAttempt(
3265
2883
  type: "worker_integration_attempt_requested",
3266
2884
  worker,
3267
2885
  message: `Worker ${worker} requested leader integration attempt`,
3268
- data: { worker_name: worker, worker_head: head, status: classification.kind, files: classification.files },
2886
+ data: {
2887
+ worker_name: worker,
2888
+ worker_head: head,
2889
+ status: classification.kind,
2890
+ files: classification.files,
2891
+ },
3269
2892
  });
3270
2893
  await sendGjcTeamMessage(
3271
2894
  teamName,
@@ -3315,6 +2938,403 @@ export async function buildTeamHudSummary(
3315
2938
  });
3316
2939
  }
3317
2940
 
2941
+ const GJC_TEAM_AUTO_CONTINUE_STALLED_WORKERS_ENV = "GJC_TEAM_AUTO_CONTINUE_STALLED_WORKERS";
2942
+ const GJC_TEAM_CONTINUATION_DISPATCH_TIMEOUT_MS = 5_000;
2943
+
2944
+ async function validateGjcContinuationEligibility(
2945
+ dir: string,
2946
+ config: GjcTeamConfig,
2947
+ worker: GjcTeamWorker,
2948
+ task: GjcTeamTask,
2949
+ heartbeatAt: string,
2950
+ staleMs: number,
2951
+ env: NodeJS.ProcessEnv,
2952
+ reservedHoldUntil?: string,
2953
+ ): Promise<string | null> {
2954
+ const phase = await readContinuationJson<Record<string, unknown>>(path.join(dir, "phase.json"));
2955
+ if (
2956
+ phase?.current_phase !== "running" ||
2957
+ typeof phase.updated_at !== "string" ||
2958
+ !Number.isFinite(Date.parse(phase.updated_at))
2959
+ )
2960
+ return "invalid_or_absent_running_phase";
2961
+ if (
2962
+ !worker.pane_id ||
2963
+ !/^%\d+$/.test(worker.pane_id) ||
2964
+ worker.pane_id === config.leader.pane_id ||
2965
+ !paneBelongsToTeamTarget(config, worker.pane_id)
2966
+ )
2967
+ return "invalid_pane_authority";
2968
+ const shutdownAuthority = await readGjcShutdownAuthority(
2969
+ workerRuntime,
2970
+ path.join(workerDir(dir, worker.id), "shutdown-request.json"),
2971
+ );
2972
+ if (shutdownAuthority.state === "valid_present") return "shutdown_requested";
2973
+ if (shutdownAuthority.state === "invalid_or_unreadable") return "invalid_shutdown_authority";
2974
+ const heartbeat = await readWorkerHeartbeat(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
2975
+ if (!heartbeat || heartbeat.last_turn_at !== heartbeatAt || currentTimeMs() - Date.parse(heartbeatAt) < staleMs)
2976
+ return "heartbeat_changed";
2977
+ const lifecycle = (await readLifecycleById(workerRuntime, dir, config))[worker.id];
2978
+ const status = await readWorkerStatus(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
2979
+ if (
2980
+ (lifecycle?.lifecycle_state !== "ready" && lifecycle?.lifecycle_state !== "working") ||
2981
+ !["idle", "working", "blocked", "done"].includes(status.state) ||
2982
+ typeof status.updated_at !== "string" ||
2983
+ !Number.isFinite(Date.parse(status.updated_at))
2984
+ )
2985
+ return "invalid_worker_lifecycle_or_status";
2986
+ const inventory = await readGjcContinuationAuthorityInventory(dir);
2987
+ if (!inventory.valid) return "invalid_authority_inventory";
2988
+ const authority = selectGjcContinuationWorkerAuthority(inventory, worker.id);
2989
+ if (
2990
+ !authority.valid ||
2991
+ authority.task.id !== task.id ||
2992
+ authority.task.version !== task.version ||
2993
+ authority.task.claim?.owner !== task.claim?.owner ||
2994
+ authority.task.claim?.token !== task.claim?.token ||
2995
+ authority.task.claim?.leased_until !== task.claim?.leased_until
2996
+ )
2997
+ return "claim_changed";
2998
+ const leaseUntil = Date.parse(task.claim?.leased_until ?? "");
2999
+ const holdUntil = reservedHoldUntil === undefined ? undefined : Date.parse(reservedHoldUntil);
3000
+ if (!Number.isFinite(leaseUntil) || leaseUntil <= currentTimeMs()) return "invalid_or_expired_lease";
3001
+ if (holdUntil !== undefined && (!Number.isFinite(holdUntil) || leaseUntil < holdUntil))
3002
+ return "lease_does_not_cover_hold";
3003
+ if (reservedHoldUntil !== undefined && shouldDispatchWorkerWithSendKeys(config.tmux_command))
3004
+ return "unsupported_send_keys_transport";
3005
+ return null;
3006
+ }
3007
+ function normalizeGjcContinuationDispatchError(value: unknown, fallback: string, maxLength: number): string {
3008
+ try {
3009
+ const normalized = String(value ?? "")
3010
+ .replace(/(token|secret|password|authorization)\s*[:=]\s*\S+/gi, "$1=[redacted]")
3011
+ .trim()
3012
+ .slice(0, maxLength);
3013
+ return normalized || fallback;
3014
+ } catch {
3015
+ return fallback;
3016
+ }
3017
+ }
3018
+
3019
+ function readGjcContinuationDispatchErrorField(error: unknown, field: "name" | "code" | "message"): unknown {
3020
+ try {
3021
+ return typeof error === "object" && error !== null ? (error as Record<string, unknown>)[field] : undefined;
3022
+ } catch {
3023
+ return undefined;
3024
+ }
3025
+ }
3026
+ async function continueStalledGjcTeamWorkers(
3027
+ dir: string,
3028
+ config: GjcTeamConfig,
3029
+ env: NodeJS.ProcessEnv,
3030
+ ): Promise<void> {
3031
+ if (env[GJC_TEAM_AUTO_CONTINUE_STALLED_WORKERS_ENV] !== "1" || config.dry_run) return;
3032
+ const phase = await readContinuationJson<unknown>(path.join(dir, "phase.json"));
3033
+
3034
+ if (
3035
+ typeof phase !== "object" ||
3036
+ phase === null ||
3037
+ Array.isArray(phase) ||
3038
+ (phase as Record<string, unknown>).current_phase !== "running" ||
3039
+ typeof (phase as Record<string, unknown>).updated_at !== "string" ||
3040
+ !Number.isFinite(Date.parse(String((phase as Record<string, unknown>).updated_at)))
3041
+ ) {
3042
+ await appendEvent(dir, {
3043
+ type: "continuation_skipped",
3044
+ message: "Continuation requires a valid running team phase record",
3045
+ data: { reason: "invalid_or_absent_running_phase" },
3046
+ });
3047
+ return;
3048
+ }
3049
+ const staleMs = parseHeartbeatStaleMs(env);
3050
+ if (staleMs <= 0) return;
3051
+ const inventory = await readGjcContinuationAuthorityInventory(dir);
3052
+ if (!inventory.valid) {
3053
+ await appendEvent(dir, {
3054
+ type: "continuation_skipped",
3055
+ message: "Continuation requires a complete canonical task and claim authority inventory",
3056
+ data: { reason: "invalid_authority_inventory" },
3057
+ });
3058
+ return;
3059
+ }
3060
+ for (const worker of config.workers) {
3061
+ const heartbeat = await readWorkerHeartbeat(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
3062
+ const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? "");
3063
+ if (!heartbeat || !Number.isFinite(heartbeatAt) || currentTimeMs() - heartbeatAt < staleMs) continue;
3064
+ if (!worker.pane_id || !/^%\d+$/.test(worker.pane_id) || worker.pane_id === config.leader.pane_id) continue;
3065
+ if (!paneBelongsToTeamTarget(config, worker.pane_id)) continue;
3066
+ const shutdownAuthority = await readGjcShutdownAuthority(
3067
+ workerRuntime,
3068
+ path.join(workerDir(dir, worker.id), "shutdown-request.json"),
3069
+ );
3070
+ if (shutdownAuthority.state !== "proven_absent") {
3071
+ await appendEvent(dir, {
3072
+ type: "continuation_skipped",
3073
+ worker: worker.id,
3074
+ message: "Continuation requires a readable absent shutdown authority record",
3075
+ data: {
3076
+ reason:
3077
+ shutdownAuthority.state === "valid_present" ? "shutdown_requested" : "invalid_shutdown_authority",
3078
+ },
3079
+ });
3080
+ continue;
3081
+ }
3082
+
3083
+ const lifecycle = (await readLifecycleById(workerRuntime, dir, config))[worker.id];
3084
+ const status = await readWorkerStatus(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
3085
+ if (
3086
+ (lifecycle?.lifecycle_state !== "ready" && lifecycle?.lifecycle_state !== "working") ||
3087
+ !["idle", "working", "blocked", "done"].includes(status.state) ||
3088
+ typeof status.updated_at !== "string" ||
3089
+ !Number.isFinite(Date.parse(status.updated_at))
3090
+ ) {
3091
+ await appendEvent(dir, {
3092
+ type: "continuation_skipped",
3093
+ worker: worker.id,
3094
+ message: "Continuation requires valid worker lifecycle and status records",
3095
+ data: { reason: "invalid_worker_lifecycle_or_status" },
3096
+ });
3097
+ continue;
3098
+ }
3099
+
3100
+ const authority = selectGjcContinuationWorkerAuthority(inventory, worker.id);
3101
+ if (!authority.valid) {
3102
+ await appendEvent(dir, {
3103
+ type: "continuation_skipped",
3104
+ worker: worker.id,
3105
+ message: "Continuation requires exactly one canonical current in-progress claim",
3106
+ data: { reason: "invalid_claim_count", task_count: authority.taskCount, claim_count: authority.claimCount },
3107
+ });
3108
+ continue;
3109
+ }
3110
+ const task = authority.task;
3111
+ const eligibilityReason = await validateGjcContinuationEligibility(
3112
+ dir,
3113
+ config,
3114
+ worker,
3115
+ task,
3116
+ heartbeat.last_turn_at,
3117
+ staleMs,
3118
+ env,
3119
+ );
3120
+ if (eligibilityReason) {
3121
+ await appendEvent(dir, {
3122
+ type: "continuation_skipped",
3123
+ worker: worker.id,
3124
+ message: "Continuation eligibility changed or contains an invalid mutable record",
3125
+ data: { reason: eligibilityReason },
3126
+ });
3127
+ continue;
3128
+ }
3129
+
3130
+ const incident = stableHash(
3131
+ [
3132
+ config.team_name,
3133
+ worker.id,
3134
+ task.id,
3135
+ task.claim.owner,
3136
+ task.claim.token,
3137
+ task.version,
3138
+ heartbeat.last_turn_at,
3139
+ worker.pane_id,
3140
+ config.tmux_target,
3141
+ ].join(":"),
3142
+ );
3143
+ const journalDir = path.join(workerDir(dir, worker.id), "continuations", incident);
3144
+ const firstReservationPath = path.join(journalDir, "attempt-01.reservation.json");
3145
+ const firstOutcomePath = path.join(journalDir, "attempt-01.outcome.json");
3146
+ const firstReservation = await readContinuationJson<Record<string, unknown>>(firstReservationPath);
3147
+
3148
+ const firstOutcome = await readContinuationJson<Record<string, unknown>>(firstOutcomePath);
3149
+
3150
+ let attempt = 1;
3151
+ if (firstReservation) {
3152
+ if (
3153
+ !isValidGjcContinuationReservation(
3154
+ firstReservation,
3155
+ incident,
3156
+ 1,
3157
+ config,
3158
+ worker.id,
3159
+ task,
3160
+ task.claim,
3161
+ heartbeat.last_turn_at,
3162
+ worker.pane_id,
3163
+ )
3164
+ )
3165
+ continue;
3166
+ if (
3167
+ !isValidGjcContinuationOutcome(firstOutcome, firstReservation, incident, 1) ||
3168
+ firstOutcome.result !== "sent"
3169
+ )
3170
+ continue;
3171
+ const firstHold = Date.parse(String(firstOutcome.hold_until));
3172
+ const leaseUntil = Date.parse(task.claim.leased_until);
3173
+ if (
3174
+ !Number.isFinite(firstHold) ||
3175
+ !Number.isFinite(leaseUntil) ||
3176
+ leaseUntil < firstHold ||
3177
+ currentTimeMs() < firstHold
3178
+ )
3179
+ continue;
3180
+ if (await readContinuationJson(path.join(journalDir, "attempt-02.reservation.json"))) continue;
3181
+
3182
+ attempt = 2;
3183
+ }
3184
+ const holdMs = attempt === 1 ? 30_000 : 120_000;
3185
+ const reservedAtMs = currentTimeMs();
3186
+ const reservedAt = new Date(reservedAtMs).toISOString();
3187
+ const holdUntil = new Date(reservedAtMs + holdMs).toISOString();
3188
+ const leaseUntil = Date.parse(task.claim.leased_until);
3189
+ if (!Number.isFinite(leaseUntil) || leaseUntil < Date.parse(holdUntil)) continue;
3190
+ const reservation = {
3191
+ schema_version: 1,
3192
+ incident_hash: incident,
3193
+ team_name: config.team_name,
3194
+ worker: worker.id,
3195
+ task_id: task.id,
3196
+ owner: task.claim.owner,
3197
+ claim_token: task.claim.token,
3198
+ task_version: task.version,
3199
+ leased_until: task.claim.leased_until,
3200
+ heartbeat_at: heartbeat.last_turn_at,
3201
+ pane_id: worker.pane_id,
3202
+ tmux_target: config.tmux_target,
3203
+ attempt,
3204
+ reserved_at: reservedAt,
3205
+ hold_until: holdUntil,
3206
+ prompt_version: 1,
3207
+ prompt_sha256: createHash("sha256").update(GJC_TEAM_CONTINUATION_PROMPT).digest("hex"),
3208
+ dispatch_protocol: "tmux_command_sequence_v1",
3209
+ };
3210
+ const reservationPath = path.join(journalDir, `attempt-0${attempt}.reservation.json`);
3211
+ try {
3212
+ await createJsonNoClobber(
3213
+ reservationPath,
3214
+ reservation,
3215
+ stateWriterOptions(reservationPath, "state", "continuation-reservation"),
3216
+ );
3217
+ } catch (error) {
3218
+ if (error instanceof AlreadyExistsError) continue;
3219
+ throw error;
3220
+ }
3221
+ let result: "sent" | "unknown" = "unknown";
3222
+ let outcomeReason = "tmux_missing_exit_code";
3223
+ let tmuxExitCode: number | undefined;
3224
+ let tmuxError: { name: string; code?: string; message: string } | undefined;
3225
+ let dispatchedAt: string | undefined;
3226
+ let dispatchHoldUntil: string | undefined;
3227
+ if (gjcTeamRuntimeTestSeams?.continuationBeforeDispatch)
3228
+ await gjcTeamRuntimeTestSeams.continuationBeforeDispatch();
3229
+ const revalidationReason = await validateGjcContinuationEligibility(
3230
+ dir,
3231
+ config,
3232
+ worker,
3233
+ task,
3234
+ heartbeat.last_turn_at,
3235
+ staleMs,
3236
+ env,
3237
+ new Date(currentTimeMs() + GJC_TEAM_CONTINUATION_DISPATCH_TIMEOUT_MS + holdMs).toISOString(),
3238
+ );
3239
+ if (revalidationReason) {
3240
+ const skippedPath = path.join(journalDir, `attempt-0${attempt}.outcome.json`);
3241
+ await createJsonNoClobber(
3242
+ skippedPath,
3243
+ {
3244
+ schema_version: 1,
3245
+ incident_hash: incident,
3246
+ attempt,
3247
+ reservation_sha256: gjcContinuationReservationDigest(reservation),
3248
+ recorded_at: now(),
3249
+ result: "skipped",
3250
+ reason: revalidationReason,
3251
+ },
3252
+ stateWriterOptions(skippedPath, "state", "continuation-outcome"),
3253
+ );
3254
+ return;
3255
+ }
3256
+ try {
3257
+ const args = Object.freeze([
3258
+ "send-keys",
3259
+ "-l",
3260
+ "-t",
3261
+ worker.pane_id,
3262
+ GJC_TEAM_CONTINUATION_PROMPT,
3263
+ ";",
3264
+ "send-keys",
3265
+ "-t",
3266
+ worker.pane_id,
3267
+ "Enter",
3268
+ ]);
3269
+ const dispatch = gjcTeamRuntimeTestSeams?.continuationTmuxDispatch
3270
+ ? gjcTeamRuntimeTestSeams.continuationTmuxDispatch(config.tmux_command, args)
3271
+ : Bun.spawnSync([config.tmux_command, ...args], {
3272
+ stdout: "ignore",
3273
+ stderr: "ignore",
3274
+ timeout: GJC_TEAM_CONTINUATION_DISPATCH_TIMEOUT_MS,
3275
+ });
3276
+ const dispatchAtMs = currentTimeMs();
3277
+ tmuxExitCode = dispatch.exitCode;
3278
+ if (dispatch.exitCode === 0) {
3279
+ result = "sent";
3280
+ outcomeReason = "tmux_sent";
3281
+ dispatchedAt = new Date(dispatchAtMs).toISOString();
3282
+ dispatchHoldUntil = new Date(dispatchAtMs + holdMs).toISOString();
3283
+ } else if (typeof dispatch.exitCode === "number") {
3284
+ outcomeReason = "tmux_nonzero_exit";
3285
+ }
3286
+ } catch (error) {
3287
+ outcomeReason = "tmux_dispatch_threw";
3288
+ const name = normalizeGjcContinuationDispatchError(
3289
+ readGjcContinuationDispatchErrorField(error, "name"),
3290
+ "Error",
3291
+ 120,
3292
+ );
3293
+ const code = normalizeGjcContinuationDispatchError(
3294
+ readGjcContinuationDispatchErrorField(error, "code"),
3295
+ "",
3296
+ 120,
3297
+ );
3298
+ tmuxError = {
3299
+ name,
3300
+ ...(code ? { code } : {}),
3301
+ message: normalizeGjcContinuationDispatchError(
3302
+ readGjcContinuationDispatchErrorField(error, "message") ?? error,
3303
+ "tmux_dispatch_error",
3304
+ 400,
3305
+ ),
3306
+ };
3307
+ }
3308
+ const outcomePath = path.join(journalDir, `attempt-0${attempt}.outcome.json`);
3309
+ try {
3310
+ await createJsonNoClobber(
3311
+ outcomePath,
3312
+ {
3313
+ schema_version: 1,
3314
+ incident_hash: incident,
3315
+ attempt,
3316
+ reservation_sha256: gjcContinuationReservationDigest(reservation),
3317
+ recorded_at: now(),
3318
+ result,
3319
+ reason: outcomeReason,
3320
+ ...(tmuxExitCode === undefined ? {} : { tmux_exit_code: tmuxExitCode }),
3321
+ ...(dispatchedAt === undefined || dispatchHoldUntil === undefined
3322
+ ? {}
3323
+ : { dispatched_at: dispatchedAt, hold_until: dispatchHoldUntil }),
3324
+ ...(tmuxError ? { tmux_error: tmuxError } : {}),
3325
+ },
3326
+ stateWriterOptions(outcomePath, "state", "continuation-outcome"),
3327
+ );
3328
+ } catch (error) {
3329
+ if (!(error instanceof AlreadyExistsError)) throw error;
3330
+ const existing = await readContinuationJson<Record<string, unknown>>(outcomePath);
3331
+ if (!isValidGjcContinuationOutcome(existing, reservation, incident, attempt))
3332
+ throw new Error(`invalid_continuation_outcome:${incident}:${attempt}`);
3333
+ }
3334
+ return;
3335
+ }
3336
+ }
3337
+
3318
3338
  export async function monitorGjcTeam(
3319
3339
  teamName: string,
3320
3340
  cwd = process.cwd(),
@@ -3323,11 +3343,18 @@ export async function monitorGjcTeam(
3323
3343
  const dir = await findTeamDir(teamName, cwd, env);
3324
3344
  const config = await readConfig(dir);
3325
3345
  const previous = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
3326
- await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
3346
+ await withGjcTeamTaskMutation(taskStore(dir), async capability => {
3347
+ const config = await readConfig(dir);
3348
+ await continueStalledGjcTeamWorkers(dir, config, env);
3349
+ await reconcileGjcTeamStaleClaimsUnlocked(workerOrchestrationRuntime, teamName, dir, config, env, capability);
3350
+ await computeLifecycleNudges(config, dir, cwd, env);
3351
+ });
3327
3352
  const integrationByWorker = await integrateGjcWorkerCommits(config, dir, previous, cwd, env);
3328
- await writeJsonFile(monitorSnapshotPath(dir), { integration_by_worker: integrationByWorker, updated_at: now() });
3353
+ await writeJsonFile(monitorSnapshotPath(dir), {
3354
+ integration_by_worker: integrationByWorker,
3355
+ updated_at: now(),
3356
+ });
3329
3357
  await replayGjcTeamNotifications(teamName, cwd, env);
3330
- await computeLifecycleNudges(config, dir, cwd, env);
3331
3358
  return readGjcTeamSnapshot(teamName, cwd, env);
3332
3359
  }
3333
3360
  export async function listGjcTeams(
@@ -3361,23 +3388,7 @@ async function writeGjcWorkerStartupAck(
3361
3388
  input: Record<string, unknown>,
3362
3389
  ): Promise<Record<string, unknown>> {
3363
3390
  const dir = await findTeamDir(teamName, cwd, env);
3364
- const config = await readConfig(dir);
3365
- const teamWorker = findKnownWorker(config, worker);
3366
- const ack = {
3367
- worker,
3368
- pid: typeof input.pid === "number" ? input.pid : undefined,
3369
- session: typeof input.session === "string" ? input.session : undefined,
3370
- protocol_version: String(input.protocol_version ?? "1"),
3371
- ack_at: now(),
3372
- };
3373
- await writeJsonFile(path.join(workerDir(dir, worker), "startup-ack.json"), ack);
3374
- await writeWorkerLifecycleRecord(dir, teamWorker, "ready", {
3375
- pane_id: teamWorker.pane_id,
3376
- pid: typeof input.pid === "number" ? input.pid : undefined,
3377
- started_at: ack.ack_at,
3378
- });
3379
- await appendEvent(dir, { type: "worker_startup_ack", worker, message: `Worker ${worker} acknowledged startup` });
3380
- return ack;
3391
+ return withGjcTeamMutationFence(dir, () => writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input));
3381
3392
  }
3382
3393
  function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: number): number {
3383
3394
  const raw = env[name]?.trim();
@@ -3385,6 +3396,13 @@ function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: numb
3385
3396
  const parsed = Number(raw);
3386
3397
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallbackMs;
3387
3398
  }
3399
+
3400
+ function parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number {
3401
+ const raw = env.GJC_TEAM_HEARTBEAT_STALE_MS?.trim();
3402
+ if (!raw) return 120_000;
3403
+ const parsed = Number(raw);
3404
+ return Number.isFinite(parsed) ? parsed : 120_000;
3405
+ }
3388
3406
  async function writeLifecycleNudge(
3389
3407
  dir: string,
3390
3408
  worker: string,
@@ -3414,7 +3432,12 @@ async function writeLifecycleNudge(
3414
3432
  suggested_action: suggestedAction,
3415
3433
  auto_action_taken: false,
3416
3434
  };
3417
- await writeJsonFile(nudgePath, record);
3435
+ try {
3436
+ await writeJsonFile(nudgePath, record);
3437
+ } catch (error) {
3438
+ if (isEnoent(error)) return;
3439
+ throw error;
3440
+ }
3418
3441
  await appendEvent(dir, {
3419
3442
  type: "worker_lifecycle_nudge",
3420
3443
  worker,
@@ -3440,10 +3463,16 @@ async function computeLifecycleNudges(
3440
3463
  env: NodeJS.ProcessEnv,
3441
3464
  ): Promise<void> {
3442
3465
  const startupGraceMs = parseDurationEnv(env, "GJC_TEAM_STARTUP_GRACE_MS", 30_000);
3443
- const heartbeatStaleMs = parseDurationEnv(env, "GJC_TEAM_HEARTBEAT_STALE_MS", 120_000);
3466
+ const heartbeatStaleMs = parseHeartbeatStaleMs(env);
3444
3467
  const createdAt = Date.parse(config.created_at);
3445
3468
  const ageMs = Date.now() - (Number.isFinite(createdAt) ? createdAt : Date.now());
3446
3469
  for (const worker of config.workers) {
3470
+ try {
3471
+ await fs.access(workerDir(dir, worker.id));
3472
+ } catch (error) {
3473
+ if (isEnoent(error)) continue;
3474
+ throw error;
3475
+ }
3447
3476
  const ack = await readJsonFile<Record<string, unknown>>(path.join(workerDir(dir, worker.id), "startup-ack.json"));
3448
3477
  if (!ack && ageMs >= startupGraceMs) {
3449
3478
  await writeLifecycleNudge(
@@ -3460,7 +3489,7 @@ async function computeLifecycleNudges(
3460
3489
  GJC_TEAM_STATE_ROOT: config.state_root,
3461
3490
  });
3462
3491
  const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
3463
- if (Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= heartbeatStaleMs) {
3492
+ if (heartbeatStaleMs > 0 && Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= heartbeatStaleMs) {
3464
3493
  await writeLifecycleNudge(
3465
3494
  dir,
3466
3495
  worker.id,
@@ -3489,87 +3518,13 @@ export async function shutdownGjcTeam(
3489
3518
  env: NodeJS.ProcessEnv = process.env,
3490
3519
  ): Promise<GjcTeamSnapshot> {
3491
3520
  const dir = await findTeamDir(teamName, cwd, env);
3492
- const config = await readConfig(dir);
3493
- const tasks = await readTasks(dir);
3494
- const evidenceFailures = tasks
3495
- .map(task => {
3496
- const reason = task.status === "completed" ? getGjcTeamTaskCompletionEvidenceFailure(task) : null;
3497
- return reason ? { task_id: task.id, reason } : null;
3498
- })
3499
- .filter((failure): failure is { task_id: string; reason: string } => failure != null);
3500
- const shutdownRequestId = `shutdown-${stableHash([config.team_name, now(), randomUUID()].join(":"))}`;
3501
- const shutdownRequestedAt = now();
3502
- await Promise.all(
3503
- config.workers.map(worker =>
3504
- writeGjcShutdownRequest(
3505
- teamName,
3506
- worker.id,
3507
- "leader-fixed",
3508
- cwd,
3509
- env,
3510
- shutdownRequestId,
3511
- "graceful",
3512
- shutdownRequestedAt,
3513
- ),
3514
- ),
3515
- );
3516
- const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
3517
- const completionVerified = tasks.length === 0 || tasks.every(isGjcTeamTaskCompletionVerified);
3518
- const pendingIntegration = completionVerified ? await hasPendingGjcTeamIntegration(dir, config, monitor) : false;
3519
- killWorkerPanes(config);
3520
- await removeCleanCreatedWorktrees(config.workers);
3521
- const stopped = {
3522
- ...config,
3523
- workers: config.workers.map(worker => ({ ...worker, status: "stopped" as const, last_heartbeat: now() })),
3524
- updated_at: now(),
3525
- };
3526
- await writeJsonFile(path.join(dir, "config.json"), stopped);
3527
- await writeWorkerLifecycleForConfig(dir, stopped, "stopped", worker => ({
3528
- pane_id: worker.pane_id,
3529
- stopped_at: stopped.updated_at,
3530
- stop_reason: "graceful_shutdown",
3531
- shutdown_request_id: shutdownRequestId,
3532
- shutdown_requested_at: shutdownRequestedAt,
3533
- shutdown_mode: "graceful",
3534
- }));
3535
- const workerLifecycleById = await readWorkerLifecycleById(dir, stopped);
3536
- const gracefulShutdownComplete = stopped.workers.every(worker => {
3537
- const lifecycle = workerLifecycleById[worker.id];
3538
- return (
3539
- lifecycle?.lifecycle_state === "stopped" &&
3540
- lifecycle.shutdown_request_id === shutdownRequestId &&
3541
- lifecycle.shutdown_mode === "graceful"
3542
- );
3543
- });
3544
- const shutdownPhase: GjcTeamPhase =
3545
- completionVerified && gracefulShutdownComplete
3546
- ? pendingIntegration
3547
- ? "awaiting_integration"
3548
- : "complete"
3549
- : evidenceFailures.length > 0 || tasks.some(task => task.status === "failed" || task.status === "blocked")
3550
- ? "failed"
3551
- : "cancelled";
3552
- await writePhase(dir, shutdownPhase);
3553
- const shutdownData: Record<string, unknown> = {
3554
- phase: shutdownPhase,
3555
- shutdown_request_id: shutdownRequestId,
3556
- graceful_shutdown_complete: gracefulShutdownComplete,
3557
- };
3558
- if (evidenceFailures.length > 0) shutdownData.evidence_failures = evidenceFailures;
3559
- await appendEvent(dir, {
3560
- type: "team_shutdown",
3561
- message:
3562
- shutdownPhase === "complete"
3563
- ? "Shut down native gjc team runtime after completed tasks"
3564
- : "Shut down native gjc team runtime with incomplete tasks",
3565
- data: shutdownData,
3566
- });
3567
- await appendTelemetry(dir, {
3568
- type: "team_shutdown",
3569
- message: `Native gjc team runtime stopped with phase ${shutdownPhase}`,
3570
- data: { shutdown_request_id: shutdownRequestId, graceful_shutdown_complete: gracefulShutdownComplete },
3521
+ return withGjcTeamMutationFence(dir, () => shutdownGjcTeamWorkers(workerOrchestrationRuntime, teamName, cwd, env));
3522
+ }
3523
+
3524
+ function taskStore(dir: string): GjcTeamTaskStore {
3525
+ return new GjcTeamTaskStore(dir, async event => {
3526
+ await appendEvent(dir, event);
3571
3527
  });
3572
- return readGjcTeamSnapshot(config.team_name, cwd, env);
3573
3528
  }
3574
3529
 
3575
3530
  export async function listGjcTeamTasks(
@@ -3577,7 +3532,7 @@ export async function listGjcTeamTasks(
3577
3532
  cwd = process.cwd(),
3578
3533
  env: NodeJS.ProcessEnv = process.env,
3579
3534
  ): Promise<GjcTeamTask[]> {
3580
- return readTasks(await findTeamDir(teamName, cwd, env));
3535
+ return taskStore(await findTeamDir(teamName, cwd, env)).list();
3581
3536
  }
3582
3537
  export async function readGjcTeamTask(
3583
3538
  teamName: string,
@@ -3585,9 +3540,7 @@ export async function readGjcTeamTask(
3585
3540
  cwd = process.cwd(),
3586
3541
  env: NodeJS.ProcessEnv = process.env,
3587
3542
  ): Promise<GjcTeamTask> {
3588
- const task = (await listGjcTeamTasks(teamName, cwd, env)).find(candidate => candidate.id === taskId);
3589
- if (!task) throw new Error(`task_not_found:${taskId}`);
3590
- return task;
3543
+ return taskStore(await findTeamDir(teamName, cwd, env)).read(taskId);
3591
3544
  }
3592
3545
  export async function createGjcTeamTask(
3593
3546
  teamName: string,
@@ -3598,32 +3551,14 @@ export async function createGjcTeamTask(
3598
3551
  taskOptions: GjcTeamTaskMetadataInput = {},
3599
3552
  ): Promise<GjcTeamTask> {
3600
3553
  const dir = await findTeamDir(teamName, cwd, env);
3601
- const config = await readConfig(dir);
3602
- if (taskOptions.owner) assertKnownWorker(config, taskOptions.owner);
3603
- const tasks = await readTasks(dir);
3604
- const next = tasks.length + 1;
3605
- const task: GjcTeamTask = {
3606
- id: `task-${next}`,
3607
- subject,
3608
- description,
3609
- title: subject,
3610
- objective: description,
3611
- status: "pending",
3612
- ...(taskOptions.owner ? { owner: taskOptions.owner } : {}),
3613
- ...(taskOptions.lane ? { lane: taskOptions.lane } : {}),
3614
- ...(taskOptions.required_role ? { required_role: taskOptions.required_role } : {}),
3615
- ...(taskOptions.allowed_roles ? { allowed_roles: taskOptions.allowed_roles } : {}),
3616
- ...(taskOptions.depends_on ? { depends_on: taskOptions.depends_on } : {}),
3617
- ...(taskOptions.blocked_by ? { blocked_by: taskOptions.blocked_by } : {}),
3618
- version: 1,
3619
- created_at: now(),
3620
- updated_at: now(),
3621
- };
3622
- await writeTask(dir, task);
3623
- config.updated_at = now();
3624
- await writeJsonFile(path.join(dir, "config.json"), config);
3625
- await appendEvent(dir, { type: "task_created", task_id: task.id, message: subject });
3626
- return task;
3554
+ return withGjcTeamTaskMutation(taskStore(dir), async capability => {
3555
+ const config = await readConfig(dir);
3556
+ if (taskOptions.owner) assertKnownWorker(config, taskOptions.owner);
3557
+ const task = await capability.create(subject, description, taskOptions);
3558
+ config.updated_at = now();
3559
+ await writeJsonFile(path.join(dir, "config.json"), config);
3560
+ return task;
3561
+ });
3627
3562
  }
3628
3563
  export async function updateGjcTeamTask(
3629
3564
  teamName: string,
@@ -3638,18 +3573,7 @@ export async function updateGjcTeamTask(
3638
3573
  env: NodeJS.ProcessEnv = process.env,
3639
3574
  ): Promise<GjcTeamTask> {
3640
3575
  const dir = await findTeamDir(teamName, cwd, env);
3641
- const task = await readGjcTeamTask(teamName, taskId, cwd, env);
3642
- const updated = normalizeTask({
3643
- ...task,
3644
- ...updates,
3645
- title: updates.subject ?? task.title,
3646
- objective: updates.description ?? task.objective,
3647
- version: task.version + 1,
3648
- updated_at: now(),
3649
- });
3650
- await writeTask(dir, updated);
3651
- await appendEvent(dir, { type: "task_updated", task_id: taskId, message: updated.subject });
3652
- return updated;
3576
+ return taskStore(dir).update(taskId, updates);
3653
3577
  }
3654
3578
  export async function claimGjcTeamTask(
3655
3579
  teamName: string,
@@ -3659,68 +3583,28 @@ export async function claimGjcTeamTask(
3659
3583
  taskId?: string,
3660
3584
  ): Promise<GjcTeamApiClaimResult> {
3661
3585
  const dir = await findTeamDir(teamName, cwd, env);
3662
- const config = await readConfig(dir);
3663
- const teamWorker = findKnownWorker(config, workerId);
3664
- const livenessRecovery = await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
3665
- const staleWorkerReasons = livenessRecovery.stale_workers[workerId];
3666
- if (staleWorkerReasons?.length)
3667
- return { ok: false, reason: `worker_not_live:${workerId}:${staleWorkerReasons.join(",")}` };
3668
- const tasks = await readTasks(dir);
3669
- const task = taskId
3670
- ? tasks.find(candidate => candidate.id === taskId)
3671
- : tasks.find(candidate => getGjcTeamTaskClaimEligibilityReason(candidate, teamWorker, tasks) == null);
3672
- if (!task) return { ok: false, reason: taskId ? `task_not_found:${taskId}` : "no_pending_task" };
3673
- const eligibilityReason = getGjcTeamTaskClaimEligibilityReason(task, teamWorker, tasks);
3674
- if (eligibilityReason) return { ok: false, reason: eligibilityReason };
3675
- const activeClaimReason = await getActiveClaimReason(dir, task);
3676
- if (activeClaimReason) return { ok: false, reason: activeClaimReason };
3677
- const token = randomUUID();
3678
- const claim: GjcTeamTaskClaim = {
3679
- owner: workerId,
3680
- token,
3681
- leased_until: new Date(Date.now() + 30 * 60_000).toISOString(),
3682
- };
3683
- const claimPath = path.join(dir, "claims", `${task.id}.json`);
3684
- const created = await writeJsonFileNoClobber(claimPath, claim);
3685
- if (!created) return { ok: false, reason: `task_already_claimed:${task.id}` };
3686
- const current = await readGjcTeamTask(teamName, task.id, cwd, env);
3687
- const currentEligibilityReason = getGjcTeamTaskClaimEligibilityReason(current, teamWorker, await readTasks(dir));
3688
- if (currentEligibilityReason) {
3689
- await fs.rm(claimPath, { force: true });
3690
- return { ok: false, reason: currentEligibilityReason };
3691
- }
3692
- if (current.status !== "pending") {
3693
- await deleteIfOwned(claimPath, {
3694
- ...stateWriterOptions(claimPath, "prune", "rollback"),
3695
- predicate: current => (current as GjcTeamTaskClaim).token === token,
3696
- });
3697
- return { ok: false, reason: `task_not_pending:${task.id}` };
3698
- }
3699
- const updated: GjcTeamTask = {
3700
- ...current,
3701
- status: "in_progress",
3702
- assignee: workerId,
3703
- owner: workerId,
3704
- claim,
3705
- version: current.version + 1,
3706
- updated_at: now(),
3707
- };
3708
- try {
3709
- await writeTask(dir, updated);
3710
- } catch (error) {
3711
- await deleteIfOwned(claimPath, {
3712
- ...stateWriterOptions(claimPath, "prune", "rollback"),
3713
- predicate: current => (current as GjcTeamTaskClaim).token === token,
3714
- });
3715
- throw error;
3716
- }
3717
- await appendEvent(dir, {
3718
- type: "task_claimed",
3719
- task_id: updated.id,
3720
- worker: workerId,
3721
- message: "Worker claimed task",
3586
+ return withGjcTeamTaskMutation(taskStore(dir), async capability => {
3587
+ const config = await readConfig(dir);
3588
+ const worker = findKnownWorker(config, workerId);
3589
+ const recovered = await reconcileGjcTeamStaleClaimsUnlocked(
3590
+ workerOrchestrationRuntime,
3591
+ teamName,
3592
+ dir,
3593
+ config,
3594
+ env,
3595
+ capability,
3596
+ );
3597
+ const reasons = recovered.stale_workers[workerId];
3598
+ if (reasons?.length) return { ok: false, reason: `worker_not_live:${workerId}:${reasons.join(",")}` };
3599
+ const status = await readWorkerStatus(workerRuntime, teamName, workerId, cwd, env);
3600
+ if (
3601
+ !["idle", "working", "blocked", "done"].includes(status.state) ||
3602
+ typeof status.updated_at !== "string" ||
3603
+ !Number.isFinite(Date.parse(status.updated_at))
3604
+ )
3605
+ return { ok: false, reason: `worker_not_live:${workerId}:invalid_status` };
3606
+ return capability.claim(worker, taskId);
3722
3607
  });
3723
- return { ok: true, task: updated, worker_id: workerId, claim_token: token };
3724
3608
  }
3725
3609
  export async function transitionGjcTeamTaskStatus(
3726
3610
  teamName: string,
@@ -3733,51 +3617,10 @@ export async function transitionGjcTeamTaskStatus(
3733
3617
  completionEvidenceInput?: unknown,
3734
3618
  ): Promise<GjcTeamTask> {
3735
3619
  const dir = await findTeamDir(teamName, cwd, env);
3736
- const config = await readConfig(dir);
3737
- const task = await readGjcTeamTask(teamName, taskId, cwd, env);
3738
- if (workerId) assertKnownWorker(config, workerId);
3739
- if (status === "pending") throw new Error(`invalid_task_transition:${taskId}:pending_requires_release`);
3740
- if (task.status === "completed" || task.status === "failed") throw new Error(`task_terminal:${taskId}`);
3741
- if (!task.claim) throw new Error(`claim_token_required:${taskId}`);
3742
- if (!claimToken) throw new Error(`claim_token_required:${taskId}`);
3743
- if (task.claim.token !== claimToken) throw new Error(`claim_token_mismatch:${taskId}`);
3744
- if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${taskId}`);
3745
- const terminal = status === "completed" || status === "failed";
3746
- const transitionedAt = now();
3747
- const completionEvidence =
3748
- status === "completed"
3749
- ? normalizeGjcTeamTaskCompletionEvidence(taskId, task.claim.owner, completionEvidenceInput, transitionedAt)
3750
- : undefined;
3751
- const updated: GjcTeamTask = {
3752
- ...task,
3753
- status,
3754
- claim: terminal ? undefined : task.claim,
3755
- version: task.version + 1,
3756
- updated_at: transitionedAt,
3757
- ...(terminal ? { completed_at: transitionedAt } : {}),
3758
- ...(completionEvidence ? { completion_evidence: completionEvidence } : {}),
3759
- };
3760
- await writeTask(dir, updated);
3761
- if (terminal) {
3762
- const claimPath = path.join(dir, "claims", `${taskId}.json`);
3763
- await removeFileAudited(claimPath, stateWriterOptions(claimPath, "prune", "terminal"));
3764
- }
3765
- const eventData: Record<string, unknown> = { status };
3766
- if (completionEvidence) {
3767
- eventData.completion_evidence = {
3768
- recorded_by: completionEvidence.recorded_by,
3769
- item_count: completionEvidence.items.length,
3770
- verified_item_count: completionEvidence.items.filter(isGjcTeamCompletionEvidenceItemVerified).length,
3771
- files_count: completionEvidence.files?.length ?? 0,
3772
- };
3773
- }
3774
- await appendEvent(dir, {
3775
- type: "task_transitioned",
3776
- task_id: taskId,
3777
- message: "Task status changed",
3778
- data: eventData,
3620
+ return withGjcTeamTaskMutation(taskStore(dir), async capability => {
3621
+ if (workerId) assertKnownWorker(await readConfig(dir), workerId);
3622
+ return capability.transition(taskId, status, claimToken, workerId, completionEvidenceInput);
3779
3623
  });
3780
- return updated;
3781
3624
  }
3782
3625
  export async function transitionGjcTeamTask(
3783
3626
  teamName: string,
@@ -3808,59 +3651,9 @@ export async function releaseGjcTeamTaskClaim(
3808
3651
  env: NodeJS.ProcessEnv = process.env,
3809
3652
  ): Promise<GjcTeamTask> {
3810
3653
  const dir = await findTeamDir(teamName, cwd, env);
3811
- const task = await readGjcTeamTask(teamName, taskId, cwd, env);
3812
- if (!task.claim || task.claim.token !== claimToken || task.claim.owner !== workerId)
3813
- throw new Error(`claim_token_mismatch:${taskId}`);
3814
- const updated: GjcTeamTask = {
3815
- ...task,
3816
- status: "pending",
3817
- assignee: undefined,
3818
- claim: undefined,
3819
- version: task.version + 1,
3820
- updated_at: now(),
3821
- };
3822
- await writeTask(dir, updated);
3823
- const claimPath = path.join(dir, "claims", `${taskId}.json`);
3824
- await deleteIfOwned(claimPath, {
3825
- ...stateWriterOptions(claimPath, "prune", "release"),
3826
- predicate: current => (current as GjcTeamTaskClaim).token === claimToken,
3827
- });
3828
- await appendEvent(dir, {
3829
- type: "task_claim_released",
3830
- task_id: taskId,
3831
- worker: workerId,
3832
- message: "Task claim released",
3833
- });
3834
- return updated;
3654
+ return taskStore(dir).release(taskId, claimToken, workerId);
3835
3655
  }
3836
3656
 
3837
- function emptyNotificationSummary(): GjcTeamNotificationSummary {
3838
- return {
3839
- total: 0,
3840
- replay_eligible: 0,
3841
- by_state: {
3842
- pending: 0,
3843
- sent: 0,
3844
- queued: 0,
3845
- deferred: 0,
3846
- failed: 0,
3847
- delivered: 0,
3848
- acknowledged: 0,
3849
- },
3850
- };
3851
- }
3852
- function isReplayEligibleNotification(state: GjcTeamNotificationDeliveryState): boolean {
3853
- return state === "pending" || state === "queued" || state === "deferred" || state === "failed";
3854
- }
3855
- function summarizeNotifications(notifications: GjcTeamNotification[]): GjcTeamNotificationSummary {
3856
- const summary = emptyNotificationSummary();
3857
- for (const notification of notifications) {
3858
- summary.total += 1;
3859
- summary.by_state[notification.delivery_state] += 1;
3860
- if (isReplayEligibleNotification(notification.delivery_state)) summary.replay_eligible += 1;
3861
- }
3862
- return summary;
3863
- }
3864
3657
  async function listNotificationRecords(dir: string): Promise<GjcTeamNotification[]> {
3865
3658
  const notificationsDir = path.join(dir, "notifications");
3866
3659
  try {
@@ -3914,26 +3707,6 @@ async function writeNotificationRecord(dir: string, notification: GjcTeamNotific
3914
3707
  await writeJsonFile(notificationPath(dir, merged.id), merged);
3915
3708
  return merged;
3916
3709
  }
3917
- async function createMessageNotification(
3918
- dir: string,
3919
- teamName: string,
3920
- message: GjcTeamMailboxMessage,
3921
- state: GjcTeamNotificationDeliveryState = "pending",
3922
- ): Promise<GjcTeamNotification> {
3923
- const id = messageNotificationId(teamName, message.to_worker, message.message_id);
3924
- return writeNotificationRecord(dir, {
3925
- id,
3926
- kind: "mailbox_message",
3927
- team_name: teamName,
3928
- recipient: message.to_worker,
3929
- source: { type: "message", id: message.message_id },
3930
- idempotency_key: message.idempotency_key,
3931
- delivery_state: state,
3932
- created_at: message.created_at,
3933
- updated_at: now(),
3934
- replay_count: 0,
3935
- });
3936
- }
3937
3710
  async function readLegacyMailbox(dir: string, worker: string): Promise<{ messages: GjcTeamMailboxMessage[] }> {
3938
3711
  return (await readJsonFile<{ messages: GjcTeamMailboxMessage[] }>(mailboxPath(dir, worker))) ?? { messages: [] };
3939
3712
  }
@@ -3942,7 +3715,9 @@ async function readMailbox(dir: string, worker: string): Promise<{ messages: Gjc
3942
3715
  const byId = new Map<string, GjcTeamMailboxMessage>();
3943
3716
  for (const message of (await readLegacyMailbox(dir, worker)).messages ?? []) byId.set(message.message_id, message);
3944
3717
  try {
3945
- const entries = await fs.readdir(mailboxDirPath(dir, worker), { withFileTypes: true });
3718
+ const entries = await fs.readdir(mailboxDirPath(dir, worker), {
3719
+ withFileTypes: true,
3720
+ });
3946
3721
  const records = await Promise.all(
3947
3722
  entries
3948
3723
  .filter(entry => entry.isFile() && entry.name.endsWith(".json"))
@@ -3952,7 +3727,9 @@ async function readMailbox(dir: string, worker: string): Promise<{ messages: Gjc
3952
3727
  } catch (error) {
3953
3728
  if (!isEnoent(error)) throw error;
3954
3729
  }
3955
- return { messages: [...byId.values()].sort((a, b) => a.created_at.localeCompare(b.created_at)) };
3730
+ return {
3731
+ messages: [...byId.values()].sort((a, b) => a.created_at.localeCompare(b.created_at)),
3732
+ };
3956
3733
  }
3957
3734
  async function writeLegacyMailboxView(dir: string, worker: string): Promise<void> {
3958
3735
  const current = await readMailbox(dir, worker);
@@ -3989,121 +3766,36 @@ async function writeMailboxMessage(
3989
3766
  await writeLegacyMailboxView(dir, worker);
3990
3767
  return message;
3991
3768
  }
3992
- async function reconcileTeamNotifications(dir: string, config: GjcTeamConfig): Promise<GjcTeamNotificationSummary> {
3993
- for (const recipient of ["leader-fixed", ...config.workers.map(worker => worker.id)]) {
3994
- const mailbox = await readMailbox(dir, recipient);
3995
- for (const message of mailbox.messages) {
3996
- const state = message.delivered_at ? "acknowledged" : message.notified_at ? "delivered" : "pending";
3997
- await createMessageNotification(dir, config.team_name, message, state);
3998
- }
3999
- }
4000
- return summarizeNotifications(await listNotificationRecords(dir));
4001
- }
4002
- async function messageForNotification(
4003
- dir: string,
4004
- notification: GjcTeamNotification,
4005
- ): Promise<GjcTeamMailboxMessage | null> {
4006
- if (notification.kind !== "mailbox_message" || notification.source.type !== "message") return null;
4007
- const mailbox = await readMailbox(dir, notification.recipient);
4008
- return mailbox.messages.find(message => message.message_id === notification.source.id) ?? null;
4009
- }
3769
+ const teamNotificationRuntime: TeamNotificationRuntime = {
3770
+ findTeamDir,
3771
+ readConfig,
3772
+ assertKnownParticipant,
3773
+ messageId: messageIdFor,
3774
+ messageNotificationId,
3775
+ now,
3776
+ randomId: randomUUID,
3777
+ appendEvent: async (dir, event) => {
3778
+ await appendEvent(dir, event);
3779
+ },
3780
+ readMailbox,
3781
+ writeMailboxMessage,
3782
+ listNotifications: listNotificationRecords,
3783
+ readNotification: readNotificationRecord,
3784
+ writeNotification: writeNotificationRecord,
3785
+ };
4010
3786
 
4011
- async function attemptConfiguredMailboxTransport(
4012
- dir: string,
4013
- config: GjcTeamConfig,
4014
- notification: GjcTeamNotification,
4015
- cwd: string,
4016
- env: NodeJS.ProcessEnv,
4017
- ): Promise<GjcTeamNotification | null> {
4018
- if (!gjcTeamMailboxDeliveryTransport) return null;
4019
- const message = await messageForNotification(dir, notification);
4020
- if (!message) return null;
4021
- try {
4022
- const result = await gjcTeamMailboxDeliveryTransport.deliverMailboxMessage({
4023
- team_name: config.team_name,
4024
- state_dir: dir,
4025
- config,
4026
- notification,
4027
- message,
4028
- cwd,
4029
- env,
4030
- });
4031
- if (!result) return null;
4032
- if (result.transport === "sdk" && result.state === "failed") return null;
4033
- return writeNotificationRecord(dir, {
4034
- ...notification,
4035
- delivery_state: result.state,
4036
- pane_attempt_result: result.transport === "pane" ? result.state : undefined,
4037
- pane_attempt_reason: result.reason ?? result.transport,
4038
- pane_attempt_at: now(),
4039
- updated_at: now(),
4040
- });
4041
- } catch {
4042
- return null;
4043
- }
4044
- }
4045
-
4046
- async function attemptPaneNotification(
4047
- dir: string,
4048
- config: GjcTeamConfig,
4049
- notification: GjcTeamNotification,
4050
- env: NodeJS.ProcessEnv,
4051
- cwd = process.cwd(),
4052
- ): Promise<GjcTeamNotification> {
4053
- const transported = await attemptConfiguredMailboxTransport(dir, config, notification, cwd, env);
4054
- if (transported) return transported;
4055
- const paneId =
4056
- notification.recipient === "leader-fixed"
4057
- ? config.leader.pane_id
4058
- : config.workers.find(worker => worker.id === notification.recipient)?.pane_id;
4059
- let result: GjcTeamPaneAttemptResult = "deferred";
4060
- let reason = "pane_missing";
4061
- if (paneId) {
4062
- if (config.tmux_session === "dry-run" || env.GJC_TEAM_FAKE_PANE_ATTEMPT === "sent") {
4063
- result = "sent";
4064
- reason = "dry_run_or_fake_tmux";
4065
- } else {
4066
- result = "queued";
4067
- reason = "tmux_delivery_recorded_without_injection";
4068
- }
4069
- }
4070
- return writeNotificationRecord(dir, {
4071
- ...notification,
4072
- delivery_state: result,
4073
- pane_attempt_result: result,
4074
- pane_attempt_reason: reason,
4075
- pane_attempt_at: now(),
4076
- updated_at: now(),
4077
- });
4078
- }
4079
3787
  export async function replayGjcTeamNotifications(
4080
3788
  teamName: string,
4081
3789
  cwd = process.cwd(),
4082
3790
  env: NodeJS.ProcessEnv = process.env,
4083
- ): Promise<{ notifications: GjcTeamNotification[]; summary: GjcTeamNotificationSummary }> {
4084
- const dir = await findTeamDir(teamName, cwd, env);
4085
- const config = await readConfig(dir);
4086
- await reconcileTeamNotifications(dir, config);
4087
- const next: GjcTeamNotification[] = [];
4088
- for (const notification of await listNotificationRecords(dir)) {
4089
- if (!isReplayEligibleNotification(notification.delivery_state)) {
4090
- next.push(notification);
4091
- continue;
4092
- }
4093
- const attempted = await attemptPaneNotification(
4094
- dir,
4095
- config,
4096
- {
4097
- ...notification,
4098
- replay_count: (notification.replay_count ?? 0) + 1,
4099
- },
4100
- env,
4101
- cwd,
4102
- );
4103
- next.push(attempted);
4104
- }
4105
- return { notifications: next, summary: summarizeNotifications(next) };
3791
+ transport?: GjcTeamMailboxDeliveryTransport,
3792
+ ): Promise<{
3793
+ notifications: GjcTeamNotification[];
3794
+ summary: GjcTeamNotificationSummary;
3795
+ }> {
3796
+ return replayTeamNotifications(teamNotificationRuntime, teamName, cwd, env, transport);
4106
3797
  }
3798
+
4107
3799
  export async function sendGjcTeamMessage(
4108
3800
  teamName: string,
4109
3801
  fromWorker: string,
@@ -4112,36 +3804,21 @@ export async function sendGjcTeamMessage(
4112
3804
  cwd = process.cwd(),
4113
3805
  env: NodeJS.ProcessEnv = process.env,
4114
3806
  idempotencyKey?: string,
3807
+ transport?: GjcTeamMailboxDeliveryTransport,
4115
3808
  ): Promise<GjcTeamMailboxMessage> {
4116
- const dir = await findTeamDir(teamName, cwd, env);
4117
- const config = await readConfig(dir);
4118
- assertKnownParticipant(config, fromWorker);
4119
- assertKnownParticipant(config, toWorker);
4120
- const createdKey = idempotencyKey ?? randomUUID();
4121
- const message: GjcTeamMailboxMessage = {
4122
- message_id: messageIdFor({ teamName: config.team_name, fromWorker, toWorker, body, idempotencyKey, createdKey }),
4123
- from_worker: fromWorker,
4124
- to_worker: toWorker,
3809
+ return sendTeamMessage(
3810
+ teamNotificationRuntime,
3811
+ teamName,
3812
+ fromWorker,
3813
+ toWorker,
4125
3814
  body,
4126
- created_at: now(),
4127
- ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
4128
- };
4129
- const written = await writeMailboxMessage(dir, toWorker, message);
4130
- const existingNotification = await readJsonFile<GjcTeamNotification>(
4131
- notificationPath(dir, messageNotificationId(config.team_name, toWorker, written.message_id)),
3815
+ cwd,
3816
+ env,
3817
+ idempotencyKey,
3818
+ transport,
4132
3819
  );
4133
- const notification = await createMessageNotification(dir, config.team_name, written);
4134
- if (!existingNotification) {
4135
- await attemptPaneNotification(dir, config, notification, env, cwd);
4136
- }
4137
- await appendEvent(dir, {
4138
- type: "message_sent",
4139
- worker: fromWorker,
4140
- message: body,
4141
- data: { to_worker: toWorker, message_id: written.message_id },
4142
- });
4143
- return written;
4144
3820
  }
3821
+
4145
3822
  export async function broadcastGjcTeamMessage(
4146
3823
  teamName: string,
4147
3824
  fromWorker: string,
@@ -4165,17 +3842,16 @@ export async function broadcastGjcTeamMessage(
4165
3842
  ),
4166
3843
  );
4167
3844
  }
3845
+
4168
3846
  export async function listGjcTeamMailbox(
4169
3847
  teamName: string,
4170
3848
  worker: string,
4171
3849
  cwd = process.cwd(),
4172
3850
  env: NodeJS.ProcessEnv = process.env,
4173
3851
  ): Promise<GjcTeamMailboxMessage[]> {
4174
- const dir = await findTeamDir(teamName, cwd, env);
4175
- const config = await readConfig(dir);
4176
- assertKnownParticipant(config, worker);
4177
- return (await readMailbox(dir, worker)).messages;
3852
+ return listTeamMailbox(teamNotificationRuntime, teamName, worker, cwd, env);
4178
3853
  }
3854
+
4179
3855
  export async function markGjcTeamMailboxMessage(
4180
3856
  teamName: string,
4181
3857
  worker: string,
@@ -4185,40 +3861,14 @@ export async function markGjcTeamMailboxMessage(
4185
3861
  env: NodeJS.ProcessEnv = process.env,
4186
3862
  ): Promise<GjcTeamMailboxMessage> {
4187
3863
  assertSafeId("message_id", messageId);
4188
- const dir = await findTeamDir(teamName, cwd, env);
4189
- const config = await readConfig(dir);
4190
- assertKnownParticipant(config, worker);
4191
- const mailbox = await readMailbox(dir, worker);
4192
- const message = mailbox.messages.find(candidate => candidate.message_id === messageId);
4193
- if (!message) throw new Error(`message_not_found:${messageId}`);
4194
- const updated = { ...message, [field]: message[field] ?? now() };
4195
- const written = await writeMailboxMessage(dir, worker, updated);
4196
- const notificationId = messageNotificationId(config.team_name, worker, messageId);
4197
- const existing =
4198
- (await readJsonFile<GjcTeamNotification>(notificationPath(dir, notificationId))) ??
4199
- (await createMessageNotification(dir, config.team_name, written));
4200
- const nextState: GjcTeamNotificationDeliveryState = field === "delivered_at" ? "acknowledged" : "delivered";
4201
- const before = existing.delivery_state;
4202
- await writeNotificationRecord(dir, { ...existing, delivery_state: nextState, updated_at: now() });
4203
- if (mergeNotificationState(before, nextState) !== before)
4204
- await appendEvent(dir, {
4205
- type: `message_${field === "delivered_at" ? "acknowledged" : "notified"}`,
4206
- worker,
4207
- message: messageId,
4208
- });
4209
- return written;
3864
+ return markTeamMailboxMessage(teamNotificationRuntime, teamName, worker, messageId, field, cwd, env);
4210
3865
  }
4211
- export async function readGjcWorkerStatus(
3866
+ export const readGjcWorkerStatus = (
4212
3867
  teamName: string,
4213
3868
  worker: string,
4214
3869
  cwd = process.cwd(),
4215
3870
  env: NodeJS.ProcessEnv = process.env,
4216
- ): Promise<WorkerStatusFile> {
4217
- const dir = await findTeamDir(teamName, cwd, env);
4218
- const config = await readConfig(dir);
4219
- assertKnownWorker(config, worker);
4220
- return readWorkerStatusFile(dir, worker);
4221
- }
3871
+ ): Promise<WorkerStatusFile> => readWorkerStatus(workerRuntime, teamName, worker, cwd, env);
4222
3872
  export async function updateGjcWorkerStatus(
4223
3873
  teamName: string,
4224
3874
  worker: string,
@@ -4229,43 +3879,16 @@ export async function updateGjcWorkerStatus(
4229
3879
  reason?: string,
4230
3880
  ): Promise<WorkerStatusFile> {
4231
3881
  const dir = await findTeamDir(teamName, cwd, env);
4232
- const config = await readConfig(dir);
4233
- const teamWorker = findKnownWorker(config, worker);
4234
- if (currentTaskId) assertSafeId("task_id", currentTaskId);
4235
- const trimmedReason = reason?.trim();
4236
- const value: WorkerStatusFile = {
4237
- state: status,
4238
- ...(currentTaskId ? { current_task_id: currentTaskId } : {}),
4239
- ...(trimmedReason ? { reason: trimmedReason } : {}),
4240
- updated_at: now(),
4241
- };
4242
- await writeJsonFile(path.join(workerDir(dir, worker), "status.json"), value);
4243
- const currentLifecycle = await readWorkerLifecycleRecord(dir, teamWorker);
4244
- const lifecycleState =
4245
- currentLifecycle.lifecycle_state === "stopped" ? "stopped" : lifecycleStateForWorkerStatus(status);
4246
- await writeWorkerLifecycleRecord(dir, teamWorker, lifecycleState);
4247
- await appendEvent(dir, {
4248
- type: "worker_status_updated",
4249
- worker,
4250
- message: `Worker ${worker} reported ${status}`,
4251
- data: {
4252
- status,
4253
- current_task_id: currentTaskId,
4254
- },
4255
- });
4256
- return value;
3882
+ return withGjcTeamMutationFence(dir, () =>
3883
+ updateWorkerStatus(workerRuntime, teamName, worker, status, cwd, env, currentTaskId, reason),
3884
+ );
4257
3885
  }
4258
- export async function readGjcWorkerHeartbeat(
3886
+ export const readGjcWorkerHeartbeat = (
4259
3887
  teamName: string,
4260
3888
  worker: string,
4261
3889
  cwd = process.cwd(),
4262
3890
  env: NodeJS.ProcessEnv = process.env,
4263
- ): Promise<WorkerHeartbeatFile | null> {
4264
- const dir = await findTeamDir(teamName, cwd, env);
4265
- const config = await readConfig(dir);
4266
- assertKnownWorker(config, worker);
4267
- return readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker), "heartbeat.json"));
4268
- }
3891
+ ): Promise<WorkerHeartbeatFile | null> => readWorkerHeartbeat(workerRuntime, teamName, worker, cwd, env);
4269
3892
  export async function updateGjcWorkerHeartbeat(
4270
3893
  teamName: string,
4271
3894
  worker: string,
@@ -4274,11 +3897,9 @@ export async function updateGjcWorkerHeartbeat(
4274
3897
  env: NodeJS.ProcessEnv = process.env,
4275
3898
  ): Promise<WorkerHeartbeatFile> {
4276
3899
  const dir = await findTeamDir(teamName, cwd, env);
4277
- const config = await readConfig(dir);
4278
- assertKnownWorker(config, worker);
4279
- const value = { ...heartbeat, last_turn_at: heartbeat.last_turn_at || now() };
4280
- await writeJsonFile(path.join(workerDir(dir, worker), "heartbeat.json"), value);
4281
- return value;
3900
+ return withGjcTeamMutationFence(dir, () =>
3901
+ updateWorkerHeartbeat(workerRuntime, teamName, worker, heartbeat, cwd, env),
3902
+ );
4282
3903
  }
4283
3904
  export async function writeGjcWorkerInbox(
4284
3905
  teamName: string,
@@ -4408,35 +4029,16 @@ export async function writeGjcShutdownRequest(
4408
4029
  requestedAt = now(),
4409
4030
  ): Promise<Record<string, unknown>> {
4410
4031
  const dir = await findTeamDir(teamName, cwd, env);
4411
- const config = await readConfig(dir);
4412
- const teamWorker = findKnownWorker(config, worker);
4413
- assertKnownParticipant(config, requestedBy);
4414
- const value = { worker, requested_by: requestedBy, request_id: requestId, mode, requested_at: requestedAt };
4415
- await writeJsonFile(path.join(workerDir(dir, worker), "shutdown-request.json"), value);
4416
- await writeWorkerLifecycleRecord(dir, teamWorker, "draining", {
4417
- shutdown_request_id: requestId,
4418
- shutdown_requested_at: requestedAt,
4419
- shutdown_mode: mode,
4420
- });
4421
- await appendEvent(dir, {
4422
- type: "worker_shutdown_requested",
4423
- worker,
4424
- message: `Worker ${worker} shutdown requested`,
4425
- data: { requested_by: requestedBy, request_id: requestId, mode },
4426
- });
4427
- return value;
4032
+ return withGjcTeamMutationFence(dir, () =>
4033
+ writeShutdownRequest(workerRuntime, teamName, worker, requestedBy, cwd, env, requestId, mode, requestedAt),
4034
+ );
4428
4035
  }
4429
- export async function readGjcShutdownAck(
4036
+ export const readGjcShutdownAck = (
4430
4037
  teamName: string,
4431
4038
  worker: string,
4432
4039
  cwd = process.cwd(),
4433
4040
  env: NodeJS.ProcessEnv = process.env,
4434
- ): Promise<Record<string, unknown> | null> {
4435
- const dir = await findTeamDir(teamName, cwd, env);
4436
- const config = await readConfig(dir);
4437
- assertKnownWorker(config, worker);
4438
- return readJsonFile<Record<string, unknown>>(path.join(workerDir(dir, worker), "shutdown-ack.json"));
4439
- }
4041
+ ): Promise<Record<string, unknown> | null> => readShutdownAck(workerRuntime, teamName, worker, cwd, env);
4440
4042
 
4441
4043
  export async function executeGjcTeamApiOperation(
4442
4044
  operation: string,
@@ -4453,7 +4055,9 @@ export async function executeGjcTeamApiOperation(
4453
4055
  case "list-tasks":
4454
4056
  return { tasks: await listGjcTeamTasks(teamName, cwd, env) };
4455
4057
  case "read-task":
4456
- return { task: await readGjcTeamTask(teamName, String(input.task_id ?? input.taskId), cwd, env) };
4058
+ return {
4059
+ task: await readGjcTeamTask(teamName, String(input.task_id ?? input.taskId), cwd, env),
4060
+ };
4457
4061
  case "create-task": {
4458
4062
  const task = await createGjcTeamTask(
4459
4063
  teamName,
@@ -4524,7 +4128,11 @@ export async function executeGjcTeamApiOperation(
4524
4128
  cwd,
4525
4129
  env,
4526
4130
  );
4527
- return { ok: true, ...taskReceiptFields(teamName, task), worker_id: worker };
4131
+ return {
4132
+ ok: true,
4133
+ ...taskReceiptFields(teamName, task),
4134
+ worker_id: worker,
4135
+ };
4528
4136
  }
4529
4137
  case "send-message": {
4530
4138
  const message = await sendGjcTeamMessage(
@@ -4586,9 +4194,12 @@ export async function executeGjcTeamApiOperation(
4586
4194
  case "notification-list": {
4587
4195
  const dir = await findTeamDir(teamName, cwd, env);
4588
4196
  const config = await readConfig(dir);
4589
- await reconcileTeamNotifications(dir, config);
4197
+ await reconcileTeamNotifications(teamNotificationRuntime, dir, config);
4590
4198
  const notifications = await listNotificationRecords(dir);
4591
- const result = { notifications, summary: summarizeNotifications(notifications) };
4199
+ const result = {
4200
+ notifications,
4201
+ summary: summarizeTeamNotifications(notifications),
4202
+ };
4592
4203
  return notificationSummaryReceipt(teamName, result);
4593
4204
  }
4594
4205
  case "notification-read":
@@ -4729,5 +4340,11 @@ export function parseTeamLaunchArgs(argv: string[]): GjcTeamStartOptions {
4729
4340
  if (!task) throw new Error("missing_team_task");
4730
4341
  if (!Number.isInteger(workerCount) || workerCount < 1 || workerCount > GJC_TEAM_MAX_WORKERS)
4731
4342
  throw new Error(`invalid_team_worker_count:${workerCount}:expected_1_${GJC_TEAM_MAX_WORKERS}`);
4732
- return { workerCount, agentType, task, dryRun, worktreeMode: resolveDefaultWorktreeMode(parsedWorktree.mode) };
4343
+ return {
4344
+ workerCount,
4345
+ agentType,
4346
+ task,
4347
+ dryRun,
4348
+ worktreeMode: resolveDefaultWorktreeMode(parsedWorktree.mode),
4349
+ };
4733
4350
  }