@gajae-code/coding-agent 0.11.11 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -43,6 +43,7 @@ import type {
43
43
  GjcTeamTaskStatus,
44
44
  } from "./team-store";
45
45
  import {
46
+ findGjcTeamClaimedTaskForWorker,
46
47
  GjcTeamTaskStore,
47
48
  isCanonicalPersistedGjcTeamTask,
48
49
  isCanonicalPersistedGjcTeamTaskClaim,
@@ -52,7 +53,19 @@ import {
52
53
  withGjcTeamMutationFence,
53
54
  withGjcTeamTaskMutation,
54
55
  } from "./team-store";
55
-
56
+ import {
57
+ appendTeamWorkerMemoryGuardLedgerEntry,
58
+ createInitialGjcTeamWorkerMemoryGuardLedger,
59
+ type GjcTeamWorkerMemoryGuardCheckpoint,
60
+ type GjcTeamWorkerMemoryGuardLedger,
61
+ type GjcTeamWorkerMemoryGuardSelectionCandidate,
62
+ isCanonicalGjcTeamWorkerMemoryGuardLedger,
63
+ nextTeamWorkerMemoryGuardAttempt,
64
+ normalizeGjcTeamWorkerMemoryGuardPidProbe,
65
+ readTeamWorkerMemoryGuardLedger,
66
+ selectGjcTeamWorkerMemoryGuardCandidate,
67
+ workerMemoryGuardLedgerPath,
68
+ } from "./team-worker-memory-guard";
56
69
  import {
57
70
  GJC_TEAM_CONTINUATION_PROMPT,
58
71
  type GjcTeamWorkerOrchestrationRuntime,
@@ -70,10 +83,10 @@ import {
70
83
  updateGjcWorkerHeartbeat as updateWorkerHeartbeat,
71
84
  updateGjcWorkerStatus as updateWorkerStatus,
72
85
  writeWorkerLifecycleForConfig as writeLifecycleForConfig,
86
+ writeWorkerLifecycleRecord as writeLifecycleRecord,
73
87
  writeGjcShutdownRequest as writeShutdownRequest,
74
88
  writeGjcWorkerStartupAck as writeWorkerStartupAck,
75
89
  } from "./team-workers";
76
-
77
90
  import {
78
91
  buildGjcTmuxExactOptionTarget,
79
92
  buildGjcTmuxUntaggedSessionHint,
@@ -435,6 +448,7 @@ export interface WorkerHeartbeatFile {
435
448
  last_turn_at: string;
436
449
  turn_count: number;
437
450
  alive: boolean;
451
+ process_start_time?: string;
438
452
  }
439
453
  interface GitResult {
440
454
  ok: boolean;
@@ -520,6 +534,9 @@ export const GJC_TEAM_API_OPERATIONS = [
520
534
  "read-worker-heartbeat",
521
535
  "recover-stale-claims",
522
536
  "update-worker-heartbeat",
537
+ "read-worker-memory-guard",
538
+ "update-worker-memory-guard",
539
+ "apply-worker-memory-guard",
523
540
  "write-worker-inbox",
524
541
  "write-worker-identity",
525
542
  "append-event",
@@ -534,6 +551,40 @@ export const GJC_TEAM_API_OPERATIONS = [
534
551
  "write-task-approval",
535
552
  ] as const;
536
553
 
554
+ export type GjcTeamApiOperation = (typeof GJC_TEAM_API_OPERATIONS)[number];
555
+
556
+ export class UnknownGjcTeamApiOperationError extends Error {
557
+ readonly code = "unknown_team_api_operation";
558
+ readonly operation: string;
559
+ readonly suggestions: readonly string[];
560
+
561
+ constructor(operation: string, suggestions: readonly string[]) {
562
+ const guidance =
563
+ suggestions.length > 0
564
+ ? `did you mean ${suggestions.join(" or ")}?`
565
+ : "run gjc team api --help for supported operations";
566
+ super(`unknown_team_api_operation:${operation}; ${guidance}`);
567
+ this.name = "UnknownGjcTeamApiOperationError";
568
+ this.operation = operation;
569
+ this.suggestions = suggestions;
570
+ }
571
+ }
572
+
573
+ function isGjcTeamApiOperation(operation: string): operation is GjcTeamApiOperation {
574
+ return (GJC_TEAM_API_OPERATIONS as readonly string[]).includes(operation);
575
+ }
576
+
577
+ function unknownGjcTeamApiOperationSuggestions(operation: string): readonly string[] {
578
+ if (operation === "heartbeat") return ["read-worker-heartbeat", "update-worker-heartbeat"];
579
+ if (operation === "get-task") return ["read-task"];
580
+ return [];
581
+ }
582
+
583
+ function resolveGjcTeamApiOperation(operation: string): GjcTeamApiOperation {
584
+ if (isGjcTeamApiOperation(operation)) return operation;
585
+ throw new UnknownGjcTeamApiOperationError(operation, unknownGjcTeamApiOperationSuggestions(operation));
586
+ }
587
+
537
588
  function currentTimeMs(): number {
538
589
  return gjcTeamRuntimeTestSeams?.nowMs?.() ?? Date.now();
539
590
  }
@@ -1218,6 +1269,838 @@ async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfi
1218
1269
 
1219
1270
  return config;
1220
1271
  }
1272
+ function manifestPath(dir: string): string {
1273
+ return path.join(dir, "manifest.v2.json");
1274
+ }
1275
+
1276
+ function manifestRecordFromConfig(config: GjcTeamConfig): Record<string, unknown> {
1277
+ return {
1278
+ version: 2,
1279
+ team_name: config.team_name,
1280
+ display_name: config.display_name,
1281
+ requested_name: config.requested_name,
1282
+ tmux_session: config.tmux_session,
1283
+ tmux_session_name: config.tmux_session_name,
1284
+ tmux_target: config.tmux_target,
1285
+ worker_command: config.worker_command,
1286
+ worker_cli_plan: config.worker_cli_plan,
1287
+ tmux_command: config.tmux_command,
1288
+ leader: config.leader,
1289
+ workers: config.workers,
1290
+ workspace_mode: config.workspace_mode,
1291
+ dry_run: config.dry_run,
1292
+ created_at: config.created_at,
1293
+ updated_at: config.updated_at,
1294
+ };
1295
+ }
1296
+
1297
+ async function syncTeamConfigAndManifest(dir: string, config: GjcTeamConfig): Promise<void> {
1298
+ await writeJsonFile(path.join(dir, "config.json"), config);
1299
+ await writeJsonFile(manifestPath(dir), manifestRecordFromConfig(config));
1300
+ }
1301
+
1302
+ async function readWorkerMemoryGuardLedger(
1303
+ dir: string,
1304
+ workerId: string,
1305
+ platform: string,
1306
+ ): Promise<GjcTeamWorkerMemoryGuardLedger> {
1307
+ const ledgerPath = workerMemoryGuardLedgerPath(dir, workerId);
1308
+ const existing = await readJsonFile<unknown>(ledgerPath);
1309
+ if (isCanonicalGjcTeamWorkerMemoryGuardLedger(existing) && existing.worker_id === workerId) return existing;
1310
+ const ledger = createInitialGjcTeamWorkerMemoryGuardLedger({ workerId, platform, now: now() });
1311
+ await writeJsonFile(ledgerPath, ledger);
1312
+ return ledger;
1313
+ }
1314
+
1315
+ async function writeWorkerMemoryGuardLedger(
1316
+ dir: string,
1317
+ ledger: GjcTeamWorkerMemoryGuardLedger,
1318
+ ): Promise<GjcTeamWorkerMemoryGuardLedger> {
1319
+ await writeJsonFile(workerMemoryGuardLedgerPath(dir, ledger.worker_id), ledger);
1320
+ return ledger;
1321
+ }
1322
+
1323
+ async function appendWorkerMemoryGuardAction(input: {
1324
+ dir: string;
1325
+ teamName: string;
1326
+ cwd: string;
1327
+ workerId: string;
1328
+ task?: GjcTeamTask;
1329
+ incidentId: string;
1330
+ action: "advisory" | "replace" | "blocked";
1331
+ result: "noop" | "scheduled" | "succeeded" | "failed" | "blocked";
1332
+ reason: string;
1333
+ }): Promise<void> {
1334
+ const workerPath = path.join(input.dir, "workers", safePathSegment("worker_id", input.workerId));
1335
+ const entries = await readTeamWorkerMemoryGuardLedger(workerPath);
1336
+ await appendTeamWorkerMemoryGuardLedgerEntry(
1337
+ workerPath,
1338
+ {
1339
+ schema_version: 1,
1340
+ recorded_at: now(),
1341
+ incident_id: input.incidentId,
1342
+ team_name: input.teamName,
1343
+ worker_id: input.workerId,
1344
+ task_id: input.task?.id ?? "unclaimed",
1345
+ claim_token: input.task?.claim?.token ?? "unclaimed",
1346
+ attempt: nextTeamWorkerMemoryGuardAttempt(entries, input.incidentId),
1347
+ platform: process.platform,
1348
+ action: input.action,
1349
+ result: input.result,
1350
+ reason: input.reason,
1351
+ },
1352
+ { cwd: input.cwd },
1353
+ );
1354
+ }
1355
+
1356
+ function normalizeWorkerMemoryGuardPlatform(value: unknown): string {
1357
+ return typeof value === "string" && value.trim() ? value.trim() : process.platform;
1358
+ }
1359
+
1360
+ async function readLinuxProcessStartTime(pid: number): Promise<string | undefined> {
1361
+ if (process.platform !== "linux" || !Number.isSafeInteger(pid) || pid <= 0) return undefined;
1362
+ try {
1363
+ const stat = await Bun.file(`/proc/${pid}/stat`).text();
1364
+ const commandEnd = stat.lastIndexOf(")");
1365
+ if (commandEnd < 0) return undefined;
1366
+ const fields = stat
1367
+ .slice(commandEnd + 2)
1368
+ .trim()
1369
+ .split(/\s+/);
1370
+ const startTime = fields[19];
1371
+ return startTime && /^\d+$/.test(startTime) ? startTime : undefined;
1372
+ } catch {
1373
+ return undefined;
1374
+ }
1375
+ }
1376
+
1377
+ function normalizeWorkerMemoryGuardSelectionCandidates(
1378
+ value: unknown,
1379
+ ledgers: Map<string, GjcTeamWorkerMemoryGuardLedger>,
1380
+ ): GjcTeamWorkerMemoryGuardSelectionCandidate[] {
1381
+ if (!Array.isArray(value)) return [];
1382
+ const candidates: GjcTeamWorkerMemoryGuardSelectionCandidate[] = [];
1383
+ for (const entry of value) {
1384
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1385
+ const record = entry as Record<string, unknown>;
1386
+ const workerId =
1387
+ typeof record.worker_id === "string"
1388
+ ? record.worker_id.trim()
1389
+ : typeof record.workerId === "string"
1390
+ ? record.workerId.trim()
1391
+ : "";
1392
+ if (!workerId) continue;
1393
+ const ledger = ledgers.get(workerId);
1394
+ const excessBytes = Number(record.excess_bytes ?? record.excessBytes ?? 0);
1395
+ if (!Number.isFinite(excessBytes)) continue;
1396
+ candidates.push({
1397
+ worker_id: workerId,
1398
+ platform:
1399
+ typeof record.platform === "string" && record.platform.trim()
1400
+ ? record.platform.trim()
1401
+ : (ledger?.platform ?? process.platform),
1402
+ excess_bytes: excessBytes,
1403
+ retry_count:
1404
+ typeof record.retry_count === "number" && Number.isInteger(record.retry_count)
1405
+ ? record.retry_count
1406
+ : (ledger?.retry_count ?? 0),
1407
+ retry_limit:
1408
+ typeof record.retry_limit === "number" && Number.isInteger(record.retry_limit) && record.retry_limit > 0
1409
+ ? record.retry_limit
1410
+ : (ledger?.retry_limit ?? 2),
1411
+ blocked: typeof record.blocked === "boolean" ? record.blocked : ledger?.state === "blocked",
1412
+ current_task_id:
1413
+ typeof record.current_task_id === "string"
1414
+ ? record.current_task_id
1415
+ : typeof record.currentTaskId === "string"
1416
+ ? record.currentTaskId
1417
+ : ledger?.current_task_id,
1418
+ });
1419
+ }
1420
+ return candidates;
1421
+ }
1422
+
1423
+ function withMemoryGuardRetry(
1424
+ ledger: GjcTeamWorkerMemoryGuardLedger,
1425
+ input: {
1426
+ platform: string;
1427
+ reason: string;
1428
+ incidentId: string;
1429
+ currentTaskId?: string;
1430
+ pidProbe?: GjcTeamWorkerMemoryGuardLedger["last_pid_probe"];
1431
+ nowIso: string;
1432
+ },
1433
+ ): { ledger: GjcTeamWorkerMemoryGuardLedger; finalBlocked: boolean } {
1434
+ const retryCount = Math.min(ledger.retry_count + 1, ledger.retry_limit);
1435
+ const finalBlocked = retryCount >= ledger.retry_limit;
1436
+ return {
1437
+ ledger: {
1438
+ ...ledger,
1439
+ platform: input.platform,
1440
+ state: finalBlocked ? "blocked" : "retrying",
1441
+ retry_count: retryCount,
1442
+ current_task_id: input.currentTaskId,
1443
+ last_incident_id: input.incidentId,
1444
+ last_reason: input.reason,
1445
+ last_pid_probe: input.pidProbe,
1446
+ updated_at: input.nowIso,
1447
+ },
1448
+ finalBlocked,
1449
+ };
1450
+ }
1451
+
1452
+ async function finalizeWorkerMemoryGuardBlockedState(input: {
1453
+ teamName: string;
1454
+ dir: string;
1455
+ worker: GjcTeamWorker;
1456
+ task: GjcTeamTask | undefined;
1457
+ taskMutation: GjcTeamTaskMutationCapability;
1458
+ reason: string;
1459
+ cwd: string;
1460
+ env: NodeJS.ProcessEnv;
1461
+ }): Promise<void> {
1462
+ await updateWorkerStatus(
1463
+ workerRuntime,
1464
+ input.teamName,
1465
+ input.worker.id,
1466
+ "blocked",
1467
+ input.cwd,
1468
+ input.env,
1469
+ input.task?.id,
1470
+ input.reason,
1471
+ );
1472
+ await writeLifecycleRecord(workerRuntime, input.dir, input.worker, "failed", { stop_reason: input.reason });
1473
+ const task = input.task;
1474
+ if (!task?.claim) return;
1475
+ await input.taskMutation.transition(task.id, "blocked", task.claim.token, input.worker.id);
1476
+ }
1477
+
1478
+ async function checkpointWorkerForMemoryGuard(
1479
+ worker: GjcTeamWorker,
1480
+ taskId: string | undefined,
1481
+ ): Promise<{ ok: true; checkpoint: GjcTeamWorkerMemoryGuardCheckpoint } | { ok: false; reason: string }> {
1482
+ if (!worker.worktree_path) return { ok: false, reason: "worker_worktree_missing" };
1483
+ let classification: GjcWorkerCheckpointClassification;
1484
+ try {
1485
+ classification = classifyWorkerCheckpointStatus(worker.worktree_path);
1486
+ } catch {
1487
+ return { ok: false, reason: "checkpoint_git_error" };
1488
+ }
1489
+ if (
1490
+ classification.kind !== "clean" &&
1491
+ classification.kind !== "eligible" &&
1492
+ classification.kind !== "protected_only"
1493
+ )
1494
+ return { ok: false, reason: `checkpoint_${classification.kind}` };
1495
+ let commit: string | null = null;
1496
+ if (classification.kind === "eligible") {
1497
+ const added = runGitResult(worker.worktree_path, ["add", "--", ...classification.files]);
1498
+ if (!added.ok) return { ok: false, reason: "checkpoint_git_add_failed" };
1499
+ const message = `gjc(team): memory-guard checkpoint ${worker.id} [${taskId ?? "unknown"}]`;
1500
+ const committed = runGitResult(worker.worktree_path, [
1501
+ "commit",
1502
+ "--no-verify",
1503
+ "--only",
1504
+ "-m",
1505
+ message,
1506
+ "--",
1507
+ ...classification.files,
1508
+ ]);
1509
+ if (!committed.ok) return { ok: false, reason: "checkpoint_git_commit_failed" };
1510
+ commit = resolveHead(worker.worktree_path);
1511
+ }
1512
+ return {
1513
+ ok: true,
1514
+ checkpoint: {
1515
+ kind: classification.kind,
1516
+ files: classification.files,
1517
+ head: resolveHead(worker.worktree_path),
1518
+ commit,
1519
+ recorded_at: now(),
1520
+ },
1521
+ };
1522
+ }
1523
+
1524
+ async function relaunchWorkerPaneForMemoryGuard(input: {
1525
+ config: GjcTeamConfig;
1526
+ worker: GjcTeamWorker;
1527
+ platform: NodeJS.Platform;
1528
+ startupAckPath: string;
1529
+ replacementToken: string;
1530
+ startupAckTimeoutMs: number;
1531
+ env: NodeJS.ProcessEnv;
1532
+ }): Promise<string> {
1533
+ if (input.config.dry_run)
1534
+ return `%memory-guard-${input.worker.id}-${stableHash(`${input.worker.id}:${now()}`).slice(0, 8)}`;
1535
+ const workerCommand = buildWorkerCommand(
1536
+ input.config,
1537
+ input.worker,
1538
+ input.platform,
1539
+ `Send startup ACK before resuming: gjc team api worker-startup-ack --input '{"team_name":"${input.config.team_name}","worker_id":"${input.worker.id}","protocol_version":"1","replacement_token":"${input.replacementToken}"}' --json. ${GJC_TEAM_CONTINUATION_PROMPT}`,
1540
+ input.env,
1541
+ );
1542
+ const workerCwd = input.worker.worktree_path ?? input.config.leader.cwd;
1543
+ const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(input.config.tmux_command, input.platform);
1544
+ const splitTarget =
1545
+ input.worker.pane_id && paneBelongsToTeamTarget(input.config, input.worker.pane_id)
1546
+ ? input.worker.pane_id
1547
+ : input.config.tmux_target;
1548
+ const split = Bun.spawnSync(
1549
+ [
1550
+ input.config.tmux_command,
1551
+ "split-window",
1552
+ "-v",
1553
+ "-t",
1554
+ splitTarget,
1555
+ "-d",
1556
+ "-P",
1557
+ "-F",
1558
+ "#{pane_id}",
1559
+ "-c",
1560
+ workerCwd,
1561
+ ...(useSendKeysFallback ? [] : [workerCommand]),
1562
+ ],
1563
+ { stdout: "pipe", stderr: "pipe" },
1564
+ );
1565
+ if (split.exitCode !== 0)
1566
+ throw new Error(split.stderr.toString().trim() || `memory_guard_split_failed:${input.worker.id}`);
1567
+ const newPaneId = split.stdout.toString().trim().split(/\r?\n/)[0]?.trim() ?? "";
1568
+ if (!newPaneId.startsWith("%")) throw new Error(`memory_guard_split_missing_pane:${input.worker.id}`);
1569
+ if (useSendKeysFallback) {
1570
+ Bun.spawnSync([input.config.tmux_command, "send-keys", "-l", "-t", newPaneId, workerCommand], {
1571
+ stdout: "ignore",
1572
+ stderr: "ignore",
1573
+ });
1574
+ Bun.spawnSync([input.config.tmux_command, "send-keys", "-t", newPaneId, "Enter"], {
1575
+ stdout: "ignore",
1576
+ stderr: "ignore",
1577
+ });
1578
+ }
1579
+ const startupDeadline = Date.now() + input.startupAckTimeoutMs;
1580
+ while (true) {
1581
+ try {
1582
+ const ack = await Bun.file(input.startupAckPath).json();
1583
+ if (
1584
+ ack &&
1585
+ typeof ack === "object" &&
1586
+ "replacement_token" in ack &&
1587
+ ack.replacement_token === input.replacementToken
1588
+ )
1589
+ break;
1590
+ } catch {
1591
+ // The successor may still be publishing its generation-bound ACK.
1592
+ }
1593
+ if (Date.now() >= startupDeadline) {
1594
+ Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
1595
+ stdout: "ignore",
1596
+ stderr: "ignore",
1597
+ });
1598
+ throw new Error(`memory_guard_successor_startup_timeout:${input.worker.id}`);
1599
+ }
1600
+ await Bun.sleep(50);
1601
+ }
1602
+ const successorPane = probePaneTeamTarget(input.config, newPaneId);
1603
+ if (!successorPane.exists || !successorPane.belongsToTeamTarget) {
1604
+ Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
1605
+ stdout: "ignore",
1606
+ stderr: "ignore",
1607
+ });
1608
+ throw new Error(`memory_guard_successor_pane_unavailable:${input.worker.id}`);
1609
+ }
1610
+ if (input.worker.pane_id) {
1611
+ const oldPane = probePaneTeamTarget(input.config, input.worker.pane_id);
1612
+ if (oldPane.exists && !oldPane.belongsToTeamTarget) {
1613
+ Bun.spawnSync([input.config.tmux_command, "kill-pane", "-t", newPaneId], {
1614
+ stdout: "ignore",
1615
+ stderr: "ignore",
1616
+ });
1617
+ throw new Error(`memory_guard_old_pane_outside_team_target:${input.worker.id}`);
1618
+ }
1619
+ }
1620
+ Bun.spawnSync([input.config.tmux_command, "select-layout", "-t", input.config.tmux_target, "main-vertical"], {
1621
+ stdout: "ignore",
1622
+ stderr: "ignore",
1623
+ });
1624
+ return newPaneId;
1625
+ }
1626
+
1627
+ async function applyWorkerMemoryGuardUnlocked(input: {
1628
+ teamName: string;
1629
+ workerId?: string;
1630
+ requestedWorkerId: string;
1631
+ reason?: string;
1632
+ incidentId?: string;
1633
+ platform: string;
1634
+ pidProbe?: GjcTeamWorkerMemoryGuardLedger["last_pid_probe"];
1635
+ candidates?: unknown;
1636
+ cwd: string;
1637
+ env: NodeJS.ProcessEnv;
1638
+ allowAutomaticAction?: boolean;
1639
+ replacementToken?: string;
1640
+ dir: string;
1641
+ /**
1642
+ * Acquire the team task-mutation fence only for the blocked-state transition.
1643
+ * Must not be held across the successor startup-ack wait: concurrent
1644
+ * `worker-startup-ack` must publish while replacement is in flight.
1645
+ */
1646
+ withTaskMutation: <T>(fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>) => Promise<T>;
1647
+ }): Promise<Record<string, unknown>> {
1648
+ const dir = input.dir;
1649
+ const config = await readConfig(dir);
1650
+ const ledgers = new Map<string, GjcTeamWorkerMemoryGuardLedger>();
1651
+ for (const candidate of config.workers)
1652
+ ledgers.set(candidate.id, await readWorkerMemoryGuardLedger(dir, candidate.id, input.platform));
1653
+ let workerId = input.workerId ?? input.requestedWorkerId;
1654
+ const parsedCandidates = normalizeWorkerMemoryGuardSelectionCandidates(input.candidates, ledgers);
1655
+ if (!input.workerId && Array.isArray(input.candidates)) {
1656
+ const selected = selectGjcTeamWorkerMemoryGuardCandidate(parsedCandidates);
1657
+ if (!selected)
1658
+ return {
1659
+ ok: true,
1660
+ result: "advisory",
1661
+ lifecycle_mutated: false,
1662
+ reason: "no_eligible_worker_memory_guard_candidate",
1663
+ };
1664
+ workerId = selected.worker_id;
1665
+ }
1666
+ const worker = findKnownWorker(config, workerId);
1667
+ const authorityInventory = await readGjcContinuationAuthorityInventory(dir);
1668
+ const authority = authorityInventory.valid
1669
+ ? selectGjcContinuationWorkerAuthority(authorityInventory, worker.id)
1670
+ : { valid: false as const, taskCount: 0, claimCount: 0 };
1671
+ const task = authority.valid ? authority.task : undefined;
1672
+ const leaseExpiresAt = authority.valid ? Date.parse(authority.claim.leased_until) : Number.NaN;
1673
+ let ledger = ledgers.get(worker.id) ?? (await readWorkerMemoryGuardLedger(dir, worker.id, input.platform));
1674
+ const nowIso = now();
1675
+ const currentTaskId = task?.id;
1676
+ const incidentId = input.incidentId ?? stableHash(`${worker.id}:${currentTaskId ?? "none"}:${nowIso}`).slice(0, 16);
1677
+ const baseReason = input.reason?.trim() || "memory_guard_requested";
1678
+ if (!authority.valid || !Number.isFinite(leaseExpiresAt) || leaseExpiresAt <= currentTimeMs()) {
1679
+ const noClaimReason =
1680
+ process.platform !== "linux" || input.platform !== "linux"
1681
+ ? `unsupported_platform:${input.platform}:host:${process.platform}:${baseReason}`
1682
+ : "worker_has_no_exact_active_claim";
1683
+ ledger = {
1684
+ ...ledger,
1685
+ platform: input.platform,
1686
+ state: "advisory",
1687
+ current_task_id: undefined,
1688
+ last_incident_id: incidentId,
1689
+ last_reason: noClaimReason,
1690
+ last_pid_probe: input.pidProbe,
1691
+ updated_at: nowIso,
1692
+ };
1693
+ await writeWorkerMemoryGuardLedger(dir, ledger);
1694
+ await appendWorkerMemoryGuardAction({
1695
+ dir,
1696
+ teamName: input.teamName,
1697
+ cwd: input.cwd,
1698
+ workerId: worker.id,
1699
+ task,
1700
+ incidentId,
1701
+ action: "advisory",
1702
+ result: "noop",
1703
+ reason: noClaimReason,
1704
+ });
1705
+ return {
1706
+ ok: true,
1707
+ result: "advisory",
1708
+ lifecycle_mutated: false,
1709
+ ledger,
1710
+ reason: noClaimReason,
1711
+ };
1712
+ }
1713
+ if (ledger.state === "blocked" || ledger.retry_count >= ledger.retry_limit) {
1714
+ await appendWorkerMemoryGuardAction({
1715
+ dir,
1716
+ teamName: input.teamName,
1717
+ cwd: input.cwd,
1718
+ workerId: worker.id,
1719
+ task,
1720
+ incidentId,
1721
+ action: "blocked",
1722
+ result: "blocked",
1723
+ reason: ledger.last_reason ?? "retry_limit",
1724
+ });
1725
+ return { ok: true, result: "blocked", lifecycle_mutated: false, ledger };
1726
+ }
1727
+ if (input.allowAutomaticAction !== undefined)
1728
+ ledger = {
1729
+ ...ledger,
1730
+ automatic_action_allowed: input.allowAutomaticAction,
1731
+ updated_at: nowIso,
1732
+ };
1733
+ if (process.platform !== "linux" || input.platform !== "linux") {
1734
+ ledger = {
1735
+ ...ledger,
1736
+ platform: input.platform,
1737
+ state: "advisory",
1738
+ current_task_id: currentTaskId,
1739
+ last_incident_id: incidentId,
1740
+ last_reason: `unsupported_platform:${input.platform}:host:${process.platform}:${baseReason}`,
1741
+ last_pid_probe: input.pidProbe,
1742
+ updated_at: nowIso,
1743
+ };
1744
+ await writeWorkerMemoryGuardLedger(dir, ledger);
1745
+ await appendWorkerMemoryGuardAction({
1746
+ dir,
1747
+ teamName: input.teamName,
1748
+ cwd: input.cwd,
1749
+ workerId: worker.id,
1750
+ task,
1751
+ incidentId,
1752
+ action: "advisory",
1753
+ result: "noop",
1754
+ reason: ledger.last_reason ?? "unsupported_platform",
1755
+ });
1756
+ return { ok: true, result: "advisory", lifecycle_mutated: false, ledger };
1757
+ }
1758
+ if (!ledger.automatic_action_allowed) {
1759
+ ledger = {
1760
+ ...ledger,
1761
+ platform: input.platform,
1762
+ state: "advisory",
1763
+ current_task_id: currentTaskId,
1764
+ last_incident_id: incidentId,
1765
+ last_reason: "automatic_linux_action_disabled",
1766
+ last_pid_probe: input.pidProbe,
1767
+ updated_at: nowIso,
1768
+ };
1769
+ await writeWorkerMemoryGuardLedger(dir, ledger);
1770
+ await appendWorkerMemoryGuardAction({
1771
+ dir,
1772
+ teamName: input.teamName,
1773
+ cwd: input.cwd,
1774
+ workerId: worker.id,
1775
+ task,
1776
+ incidentId,
1777
+ action: "advisory",
1778
+ result: "noop",
1779
+ reason: "automatic_linux_action_disabled",
1780
+ });
1781
+ return { ok: true, result: "advisory", lifecycle_mutated: false, ledger };
1782
+ }
1783
+ const controllerPath = path.join(dir, "memory-guard-controller.json");
1784
+ const controllerProcessStart = await readLinuxProcessStartTime(process.pid);
1785
+ const controllerId = `pid:${process.pid}:start:${controllerProcessStart ?? "unknown"}`;
1786
+ const controllerNow = currentTimeMs();
1787
+ const controllerCooldownMs = parseDurationEnv(input.env, "GJC_TEAM_MEMORY_GUARD_ACTION_COOLDOWN_MS", 120_000);
1788
+ const existingController = await readJsonFile<{
1789
+ controller_id?: string;
1790
+ cooldown_until?: string;
1791
+ }>(controllerPath);
1792
+ const existingCooldownUntil = Date.parse(existingController?.cooldown_until ?? "");
1793
+ if (
1794
+ existingController?.controller_id &&
1795
+ existingController.controller_id !== controllerId &&
1796
+ Number.isFinite(existingCooldownUntil) &&
1797
+ existingCooldownUntil > controllerNow
1798
+ )
1799
+ return {
1800
+ ok: true,
1801
+ result: "advisory",
1802
+ lifecycle_mutated: false,
1803
+ reason: "team_memory_guard_controller_active",
1804
+ };
1805
+ await writeJsonFile(controllerPath, {
1806
+ schema_version: 1,
1807
+ controller_id: controllerId,
1808
+ worker_id: worker.id,
1809
+ incident_id: incidentId,
1810
+ reserved_at: new Date(controllerNow).toISOString(),
1811
+ cooldown_until: new Date(controllerNow + controllerCooldownMs).toISOString(),
1812
+ });
1813
+ const checkpoint = await checkpointWorkerForMemoryGuard(worker, currentTaskId);
1814
+ if (!checkpoint.ok) {
1815
+ const retried = withMemoryGuardRetry(ledger, {
1816
+ platform: input.platform,
1817
+ reason: checkpoint.reason,
1818
+ incidentId,
1819
+ currentTaskId,
1820
+ pidProbe: input.pidProbe,
1821
+ nowIso,
1822
+ });
1823
+ ledger = retried.ledger;
1824
+ await writeWorkerMemoryGuardLedger(dir, ledger);
1825
+ if (retried.finalBlocked)
1826
+ await input.withTaskMutation(taskMutation =>
1827
+ finalizeWorkerMemoryGuardBlockedState({
1828
+ teamName: input.teamName,
1829
+ dir,
1830
+ worker,
1831
+ task,
1832
+ taskMutation,
1833
+ reason: checkpoint.reason,
1834
+ cwd: input.cwd,
1835
+ env: input.env,
1836
+ }),
1837
+ );
1838
+ await appendWorkerMemoryGuardAction({
1839
+ dir,
1840
+ teamName: input.teamName,
1841
+ cwd: input.cwd,
1842
+ workerId: worker.id,
1843
+ task,
1844
+ incidentId,
1845
+ action: retried.finalBlocked ? "blocked" : "replace",
1846
+ result: retried.finalBlocked ? "blocked" : "failed",
1847
+ reason: checkpoint.reason,
1848
+ });
1849
+ return {
1850
+ ok: true,
1851
+ result: retried.finalBlocked ? "blocked" : "retrying",
1852
+ lifecycle_mutated: retried.finalBlocked,
1853
+ ledger,
1854
+ };
1855
+ }
1856
+ const refreshedInventory = await readGjcContinuationAuthorityInventory(dir);
1857
+ const refreshedAuthority = refreshedInventory.valid
1858
+ ? selectGjcContinuationWorkerAuthority(refreshedInventory, worker.id)
1859
+ : { valid: false as const, taskCount: 0, claimCount: 0 };
1860
+ if (
1861
+ !refreshedAuthority.valid ||
1862
+ refreshedAuthority.task.id !== task?.id ||
1863
+ refreshedAuthority.claim.token !== task?.claim?.token ||
1864
+ refreshedAuthority.claim.leased_until !== task?.claim?.leased_until ||
1865
+ Date.parse(refreshedAuthority.claim.leased_until) <= currentTimeMs()
1866
+ )
1867
+ return {
1868
+ ok: true,
1869
+ result: "advisory",
1870
+ lifecycle_mutated: false,
1871
+ reason: "worker_claim_authority_changed",
1872
+ };
1873
+ const startupAckPath = path.join(dir, "workers", safePathSegment("worker_id", worker.id), "startup-ack.json");
1874
+ const previousStartupAck = (await Bun.file(startupAckPath).exists())
1875
+ ? await Bun.file(startupAckPath).text()
1876
+ : undefined;
1877
+ const lifecyclePath = workerLifecyclePath(dir, worker.id);
1878
+ const previousLifecycle = (await Bun.file(lifecyclePath).exists())
1879
+ ? await Bun.file(lifecyclePath).text()
1880
+ : undefined;
1881
+ const restorePredecessorStartupState = async (): Promise<void> => {
1882
+ if (previousLifecycle === undefined) await fs.rm(lifecyclePath, { force: true });
1883
+ else await Bun.write(lifecyclePath, previousLifecycle);
1884
+ if (previousStartupAck === undefined) await fs.rm(startupAckPath, { force: true });
1885
+ else await Bun.write(startupAckPath, previousStartupAck);
1886
+ };
1887
+ await fs.rm(startupAckPath, { force: true });
1888
+ const replacementToken = input.replacementToken ?? randomUUID();
1889
+ const startupAckTimeoutMs = parseDurationEnv(input.env, "GJC_TEAM_MEMORY_GUARD_STARTUP_TIMEOUT_MS", 120_000);
1890
+ let newPaneId: string;
1891
+ try {
1892
+ newPaneId = await relaunchWorkerPaneForMemoryGuard({
1893
+ config,
1894
+ worker,
1895
+ platform: process.platform,
1896
+ startupAckPath,
1897
+ replacementToken,
1898
+ startupAckTimeoutMs,
1899
+ env: input.env,
1900
+ });
1901
+ } catch (error) {
1902
+ await restorePredecessorStartupState();
1903
+ const reason = error instanceof Error && error.message ? `relaunch_failed:${error.message}` : "relaunch_failed";
1904
+ const retried = withMemoryGuardRetry(ledger, {
1905
+ platform: input.platform,
1906
+ reason,
1907
+ incidentId,
1908
+ currentTaskId,
1909
+ pidProbe: input.pidProbe,
1910
+ nowIso,
1911
+ });
1912
+ ledger = retried.ledger;
1913
+ await writeWorkerMemoryGuardLedger(dir, ledger);
1914
+ if (retried.finalBlocked)
1915
+ await input.withTaskMutation(taskMutation =>
1916
+ finalizeWorkerMemoryGuardBlockedState({
1917
+ teamName: input.teamName,
1918
+ dir,
1919
+ worker,
1920
+ task,
1921
+ taskMutation,
1922
+ reason,
1923
+ cwd: input.cwd,
1924
+ env: input.env,
1925
+ }),
1926
+ );
1927
+ await appendWorkerMemoryGuardAction({
1928
+ dir,
1929
+ teamName: input.teamName,
1930
+ cwd: input.cwd,
1931
+ workerId: worker.id,
1932
+ task,
1933
+ incidentId,
1934
+ action: retried.finalBlocked ? "blocked" : "replace",
1935
+ result: retried.finalBlocked ? "blocked" : "failed",
1936
+ reason,
1937
+ });
1938
+ return {
1939
+ ok: true,
1940
+ result: retried.finalBlocked ? "blocked" : "retrying",
1941
+ lifecycle_mutated: retried.finalBlocked,
1942
+ ledger,
1943
+ };
1944
+ }
1945
+ const postAckInventory = await readGjcContinuationAuthorityInventory(dir);
1946
+ const postAckAuthority = postAckInventory.valid
1947
+ ? selectGjcContinuationWorkerAuthority(postAckInventory, worker.id)
1948
+ : { valid: false as const, taskCount: 0, claimCount: 0 };
1949
+ if (
1950
+ !postAckAuthority.valid ||
1951
+ postAckAuthority.task.id !== task?.id ||
1952
+ postAckAuthority.claim.token !== task?.claim?.token ||
1953
+ postAckAuthority.claim.leased_until !== task?.claim?.leased_until ||
1954
+ Date.parse(postAckAuthority.claim.leased_until) <= currentTimeMs()
1955
+ ) {
1956
+ Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
1957
+ stdout: "ignore",
1958
+ stderr: "ignore",
1959
+ });
1960
+ await restorePredecessorStartupState();
1961
+ return {
1962
+ ok: true,
1963
+ result: "advisory",
1964
+ lifecycle_mutated: false,
1965
+ reason: "worker_claim_authority_changed_after_startup",
1966
+ };
1967
+ }
1968
+ const successorPane = probePaneTeamTarget(config, newPaneId);
1969
+ if (!successorPane.exists || !successorPane.belongsToTeamTarget || !successorPane.pid) {
1970
+ Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
1971
+ stdout: "ignore",
1972
+ stderr: "ignore",
1973
+ });
1974
+ await restorePredecessorStartupState();
1975
+ return {
1976
+ ok: true,
1977
+ result: "advisory",
1978
+ lifecycle_mutated: false,
1979
+ reason: "successor_pane_unavailable_after_startup",
1980
+ };
1981
+ }
1982
+ const heartbeatPath = path.join(dir, "workers", safePathSegment("worker_id", worker.id), "heartbeat.json");
1983
+ const previousHeartbeat = (await Bun.file(heartbeatPath).exists())
1984
+ ? await Bun.file(heartbeatPath).text()
1985
+ : undefined;
1986
+ const successorHeartbeat: WorkerHeartbeatFile = {
1987
+ pid: successorPane.pid,
1988
+ last_turn_at: now(),
1989
+ turn_count: 0,
1990
+ alive: true,
1991
+ process_start_time: await readLinuxProcessStartTime(successorPane.pid),
1992
+ };
1993
+ const nextConfig: GjcTeamConfig = {
1994
+ ...config,
1995
+ workers: config.workers.map(candidate =>
1996
+ candidate.id === worker.id
1997
+ ? { ...candidate, pane_id: newPaneId, last_heartbeat: nowIso, status: "idle" }
1998
+ : candidate,
1999
+ ),
2000
+ updated_at: nowIso,
2001
+ };
2002
+ const rollbackReplacement = async (): Promise<void> => {
2003
+ Bun.spawnSync([config.tmux_command, "kill-pane", "-t", newPaneId], {
2004
+ stdout: "ignore",
2005
+ stderr: "ignore",
2006
+ });
2007
+ if (previousHeartbeat === undefined) await fs.rm(heartbeatPath, { force: true });
2008
+ else await Bun.write(heartbeatPath, previousHeartbeat);
2009
+ await syncTeamConfigAndManifest(dir, config);
2010
+ await restorePredecessorStartupState();
2011
+ };
2012
+ try {
2013
+ await writeJsonFile(heartbeatPath, successorHeartbeat);
2014
+ await syncTeamConfigAndManifest(dir, nextConfig);
2015
+ await writeLifecycleRecord(workerRuntime, dir, { ...worker, pane_id: newPaneId }, "ready", {
2016
+ pane_id: newPaneId,
2017
+ started_at: nowIso,
2018
+ stop_reason: undefined,
2019
+ stopped_at: undefined,
2020
+ });
2021
+ const cutoverPane = probePaneTeamTarget(config, newPaneId);
2022
+ if (!cutoverPane.exists || !cutoverPane.belongsToTeamTarget || cutoverPane.pid !== successorHeartbeat.pid)
2023
+ throw new Error(`memory_guard_successor_pane_changed:${worker.id}`);
2024
+ if (worker.pane_id && !config.dry_run) {
2025
+ const killed = Bun.spawnSync([config.tmux_command, "kill-pane", "-t", worker.pane_id], {
2026
+ stdout: "ignore",
2027
+ stderr: "pipe",
2028
+ });
2029
+ if (killed.exitCode !== 0 && probePaneTeamTarget(config, worker.pane_id).exists)
2030
+ throw new Error(killed.stderr.toString().trim() || `memory_guard_kill_failed:${worker.id}`);
2031
+ }
2032
+ } catch (error) {
2033
+ await rollbackReplacement();
2034
+ throw new Error(
2035
+ error instanceof Error && error.message
2036
+ ? `memory_guard_replacement_commit_failed:${error.message}`
2037
+ : "memory_guard_replacement_commit_failed",
2038
+ );
2039
+ }
2040
+ ledger = {
2041
+ ...ledger,
2042
+ platform: input.platform,
2043
+ state: "replaced",
2044
+ retry_count: 0,
2045
+ current_task_id: currentTaskId,
2046
+ last_incident_id: incidentId,
2047
+ last_reason: baseReason,
2048
+ last_pid_probe: input.pidProbe,
2049
+ last_checkpoint: checkpoint.checkpoint,
2050
+ last_replacement: {
2051
+ old_pane_id: worker.pane_id,
2052
+ new_pane_id: newPaneId,
2053
+ recorded_at: nowIso,
2054
+ },
2055
+ updated_at: nowIso,
2056
+ };
2057
+ await writeWorkerMemoryGuardLedger(dir, ledger);
2058
+ await appendWorkerMemoryGuardAction({
2059
+ dir,
2060
+ teamName: input.teamName,
2061
+ cwd: input.cwd,
2062
+ workerId: worker.id,
2063
+ task,
2064
+ incidentId,
2065
+ action: "replace",
2066
+ result: "succeeded",
2067
+ reason: baseReason,
2068
+ });
2069
+ await appendEvent(dir, {
2070
+ type: "worker_memory_guard_replaced",
2071
+ worker: worker.id,
2072
+ task_id: currentTaskId,
2073
+ message: `Replaced ${worker.id} after memory-guard checkpoint`,
2074
+ data: {
2075
+ incident_id: incidentId,
2076
+ checkpoint_kind: checkpoint.checkpoint.kind,
2077
+ checkpoint_commit: checkpoint.checkpoint.commit,
2078
+ old_pane_id: worker.pane_id,
2079
+ new_pane_id: newPaneId,
2080
+ },
2081
+ });
2082
+ return {
2083
+ ok: true,
2084
+ result: "replaced",
2085
+ lifecycle_mutated: true,
2086
+ ledger,
2087
+ checkpoint: checkpoint.checkpoint,
2088
+ };
2089
+ }
2090
+
2091
+ async function applyWorkerMemoryGuard(
2092
+ input: Omit<Parameters<typeof applyWorkerMemoryGuardUnlocked>[0], "dir" | "withTaskMutation">,
2093
+ ): Promise<Record<string, unknown>> {
2094
+ const dir = await findTeamDir(input.teamName, input.cwd, input.env);
2095
+ // Do not hold withGjcTeamTaskMutation across relaunchWorkerPaneForMemoryGuard's
2096
+ // startup-ack poll (default 120s). That fence would serialize concurrent
2097
+ // worker-startup-ack publication and hang selector-replacement under CI load.
2098
+ return applyWorkerMemoryGuardUnlocked({
2099
+ ...input,
2100
+ dir,
2101
+ withTaskMutation: fn => withGjcTeamTaskMutation(taskStore(dir), fn),
2102
+ });
2103
+ }
1221
2104
  async function readPhase(dir: string): Promise<GjcTeamPhase> {
1222
2105
  try {
1223
2106
  const phase = await readJsonFile<{ current_phase?: GjcTeamPhase }>(path.join(dir, "phase.json"));
@@ -1652,7 +2535,11 @@ function tagTmuxSessionAsGjcLeader(tmuxCommand: string, sessionName: string): bo
1652
2535
  return result.exitCode === 0;
1653
2536
  }
1654
2537
 
1655
- function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): GjcTmuxLeaderContext {
2538
+ function readCurrentTmuxLeaderContext(
2539
+ tmuxCommand: string,
2540
+ env: NodeJS.ProcessEnv,
2541
+ adoptUnmanagedSession = true,
2542
+ ): GjcTmuxLeaderContext {
1656
2543
  if (Bun.which(tmuxCommand) === null)
1657
2544
  throw new Error(buildTeamTmuxLeaderRequirementMessage(`tmux_not_installed:${tmuxCommand}`));
1658
2545
  // Prefer the explicit GJC-managed session name propagated by `gjc --tmux`
@@ -1685,7 +2572,7 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
1685
2572
  const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
1686
2573
  if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
1687
2574
  throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
1688
- if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE) {
2575
+ if (adoptUnmanagedSession && readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE) {
1689
2576
  // Adopt any real tmux leader as a GJC team leader — including a session
1690
2577
  // the user created outside `gjc --tmux` — by writing GJC's @gjc-profile
1691
2578
  // ownership tag and reading it back. A provider that round-trips tmux
@@ -1708,6 +2595,20 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
1708
2595
  target: `${sessionName}:${windowIndex}`,
1709
2596
  };
1710
2597
  }
2598
+ /**
2599
+ * Check whether the current process can launch a team without changing tmux state.
2600
+ * Unlike the launch path, this never adopts or tags an unmanaged tmux session.
2601
+ */
2602
+ export function probeGjcTeamAvailability(
2603
+ env: NodeJS.ProcessEnv = process.env,
2604
+ ): { available: true } | { available: false; reason: string } {
2605
+ try {
2606
+ readCurrentTmuxLeaderContext(resolveGjcTmuxCommand(env), env, false);
2607
+ return { available: true };
2608
+ } catch (error) {
2609
+ return { available: false, reason: error instanceof Error ? error.message : String(error) };
2610
+ }
2611
+ }
1711
2612
  function isBunVirtualPath(candidate: string | undefined): boolean {
1712
2613
  const normalized = candidate?.trim().replace(/\\/g, "/").toLowerCase();
1713
2614
  return (
@@ -1886,7 +2787,7 @@ async function startTmuxSession(
1886
2787
  const splitDirection: string = worker.index === 1 ? "-h" : "-v";
1887
2788
  const splitTarget: string =
1888
2789
  worker.index === 1 ? config.tmux_target : (rightStackRootPaneId ?? config.tmux_target);
1889
- const workerCommand = buildWorkerCommand(config, worker);
2790
+ const workerCommand = buildWorkerCommand(config, worker, process.platform, undefined, env);
1890
2791
  const workerCwd = worker.worktree_path ?? config.leader.cwd;
1891
2792
  const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(config.tmux_command);
1892
2793
  const splitArgs = [
@@ -1998,15 +2899,27 @@ async function startTmuxSession(
1998
2899
  throw error;
1999
2900
  }
2000
2901
  }
2902
+ function probePaneTeamTarget(
2903
+ config: GjcTeamConfig,
2904
+ paneId: string,
2905
+ ): { exists: boolean; belongsToTeamTarget: boolean; pid?: number } {
2906
+ if (paneId === config.leader.pane_id) return { exists: true, belongsToTeamTarget: false };
2907
+ const result = Bun.spawnSync(
2908
+ [config.tmux_command, "display-message", "-p", "-t", paneId, "#S:#I #{pane_id} #{pane_pid}"],
2909
+ { stdout: "pipe", stderr: "ignore" },
2910
+ );
2911
+ if (result.exitCode !== 0) return { exists: false, belongsToTeamTarget: false };
2912
+ const [target = "", detectedPaneId = "", rawPid = ""] = result.stdout.toString().trim().split(/\s+/);
2913
+ const pid = Number(rawPid);
2914
+ return {
2915
+ exists: true,
2916
+ belongsToTeamTarget: target === config.tmux_target && detectedPaneId === paneId,
2917
+ pid: Number.isInteger(pid) && pid > 0 ? pid : undefined,
2918
+ };
2919
+ }
2920
+
2001
2921
  function paneBelongsToTeamTarget(config: GjcTeamConfig, paneId: string): boolean {
2002
- if (paneId === config.leader.pane_id) return false;
2003
- const result = Bun.spawnSync([config.tmux_command, "display-message", "-p", "-t", paneId, "#S:#I #{pane_id}"], {
2004
- stdout: "pipe",
2005
- stderr: "ignore",
2006
- });
2007
- if (result.exitCode !== 0) return false;
2008
- const [target = "", detectedPaneId = ""] = result.stdout.toString().trim().split(/\s+/);
2009
- return target === config.tmux_target && detectedPaneId === paneId;
2922
+ return probePaneTeamTarget(config, paneId).belongsToTeamTarget;
2010
2923
  }
2011
2924
  function killWorkerPanes(config: GjcTeamConfig): void {
2012
2925
  for (const worker of config.workers)
@@ -2147,13 +3060,24 @@ const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "
2147
3060
  // into the leader branch on projects that do not gitignore `.gjc/_session-*/`.
2148
3061
  const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [".gjc/_session-*/"];
2149
3062
 
2150
- function parsePorcelainStatusFiles(stdout: string): string[] {
2151
- return stdout
2152
- .split(/\r?\n/)
2153
- .map(line => line.trimEnd())
2154
- .filter(Boolean)
2155
- .map(line => line.slice(3).trim())
2156
- .filter(Boolean);
3063
+ function parsePorcelainStatus(stdout: string): { files: string[]; statusCodes: string[] } {
3064
+ const records = stdout.split("\0");
3065
+ const files: string[] = [];
3066
+ const statusCodes: string[] = [];
3067
+ for (let index = 0; index < records.length; index++) {
3068
+ const record = records[index];
3069
+ if (!record) continue;
3070
+ const statusCode = record.slice(0, 2);
3071
+ const file = record.slice(3);
3072
+ if (!file) continue;
3073
+ statusCodes.push(statusCode);
3074
+ files.push(file);
3075
+ if (statusCode.includes("R") || statusCode.includes("C")) {
3076
+ const source = records[++index];
3077
+ if (source) files.push(source);
3078
+ }
3079
+ }
3080
+ return { files: [...new Set(files)], statusCodes };
2157
3081
  }
2158
3082
 
2159
3083
  function normalizeGitStatusPath(filePath: string): string {
@@ -2180,7 +3104,7 @@ export function classifyGjcTeamCheckpointFiles(files: string[]): {
2180
3104
  }
2181
3105
 
2182
3106
  export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpointClassification {
2183
- const status = runGitResult(cwd, ["status", "--porcelain", "-uall"]);
3107
+ const status = runGitResult(cwd, ["status", "--porcelain=v1", "-z", "-uall"]);
2184
3108
  if (!status.ok) {
2185
3109
  return {
2186
3110
  kind: "git_error",
@@ -2189,11 +3113,9 @@ export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpoint
2189
3113
  };
2190
3114
  }
2191
3115
  if (!status.stdout.trim()) return { kind: "clean", files: [] };
2192
- const files = parsePorcelainStatusFiles(status.stdout);
2193
- const hasUnmergedStatus = status.stdout
2194
- .split(/\r?\n/)
2195
- .filter(Boolean)
2196
- .some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
3116
+ const parsed = parsePorcelainStatus(status.stdout);
3117
+ const files = parsed.files;
3118
+ const hasUnmergedStatus = parsed.statusCodes.some(code => UNMERGED_GIT_STATUS_CODES.has(code));
2197
3119
  const conflictFiles = listConflictFiles(cwd);
2198
3120
  if (hasUnmergedStatus || conflictFiles.length > 0) {
2199
3121
  return {
@@ -2210,7 +3132,7 @@ export async function classifyWorkerCheckpointStatusAsync(
2210
3132
  cwd: string,
2211
3133
  signal?: AbortSignal,
2212
3134
  ): Promise<GjcWorkerCheckpointClassification> {
2213
- const status = await runGitResultAsync(cwd, ["status", "--porcelain", "-uall"], signal);
3135
+ const status = await runGitResultAsync(cwd, ["status", "--porcelain=v1", "-z", "-uall"], signal);
2214
3136
  if (!status.ok) {
2215
3137
  return {
2216
3138
  kind: "git_error",
@@ -2219,11 +3141,9 @@ export async function classifyWorkerCheckpointStatusAsync(
2219
3141
  };
2220
3142
  }
2221
3143
  if (!status.stdout.trim()) return { kind: "clean", files: [] };
2222
- const files = parsePorcelainStatusFiles(status.stdout);
2223
- const hasUnmergedStatus = status.stdout
2224
- .split(/\r?\n/)
2225
- .filter(Boolean)
2226
- .some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
3144
+ const parsed = parsePorcelainStatus(status.stdout);
3145
+ const files = parsed.files;
3146
+ const hasUnmergedStatus = parsed.statusCodes.some(code => UNMERGED_GIT_STATUS_CODES.has(code));
2227
3147
  const conflictFiles = await listConflictFilesAsync(cwd, signal);
2228
3148
  if (hasUnmergedStatus || conflictFiles.length > 0) {
2229
3149
  return {
@@ -3387,8 +4307,7 @@ async function writeGjcWorkerStartupAck(
3387
4307
  env: NodeJS.ProcessEnv,
3388
4308
  input: Record<string, unknown>,
3389
4309
  ): Promise<Record<string, unknown>> {
3390
- const dir = await findTeamDir(teamName, cwd, env);
3391
- return withGjcTeamMutationFence(dir, () => writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input));
4310
+ return writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input);
3392
4311
  }
3393
4312
  function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: number): number {
3394
4313
  const raw = env[name]?.trim();
@@ -3397,11 +4316,13 @@ function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: numb
3397
4316
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallbackMs;
3398
4317
  }
3399
4318
 
3400
- function parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number {
4319
+ /** Positive stale windows are clamped so a worker can publish strictly before expiry. */
4320
+ export function parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number {
3401
4321
  const raw = env.GJC_TEAM_HEARTBEAT_STALE_MS?.trim();
3402
4322
  if (!raw) return 120_000;
3403
4323
  const parsed = Number(raw);
3404
- return Number.isFinite(parsed) ? parsed : 120_000;
4324
+ if (!Number.isFinite(parsed)) return 120_000;
4325
+ return parsed > 0 ? Math.max(3, parsed) : parsed;
3405
4326
  }
3406
4327
  async function writeLifecycleNudge(
3407
4328
  dir: string,
@@ -3901,6 +4822,41 @@ export async function updateGjcWorkerHeartbeat(
3901
4822
  updateWorkerHeartbeat(workerRuntime, teamName, worker, heartbeat, cwd, env),
3902
4823
  );
3903
4824
  }
4825
+ /**
4826
+ * Refresh liveness without discarding metadata written by other heartbeat
4827
+ * producers. The read/merge/write sequence shares the team mutation fence with
4828
+ * CLI heartbeat updates, so a runtime tick cannot overwrite a newer turn count.
4829
+ */
4830
+ export async function refreshGjcWorkerHeartbeat(
4831
+ teamName: string,
4832
+ worker: string,
4833
+ pid: number,
4834
+ cwd = process.cwd(),
4835
+ env: NodeJS.ProcessEnv = process.env,
4836
+ ): Promise<WorkerHeartbeatFile> {
4837
+ const dir = await findTeamDir(teamName, cwd, env);
4838
+ return withGjcTeamMutationFence(dir, async () => {
4839
+ const current = await readWorkerHeartbeat(workerRuntime, teamName, worker, cwd, env);
4840
+ const processStartTime = await readLinuxProcessStartTime(pid);
4841
+ const turnCount = current?.turn_count;
4842
+ return updateWorkerHeartbeat(
4843
+ workerRuntime,
4844
+ teamName,
4845
+ worker,
4846
+ {
4847
+ ...current,
4848
+ pid,
4849
+ last_turn_at: now(),
4850
+ turn_count: Number.isSafeInteger(turnCount) && (turnCount ?? -1) >= 0 ? (turnCount ?? 0) : 0,
4851
+ alive: true,
4852
+ // Never carry another process's incarnation across a pid change.
4853
+ process_start_time: processStartTime ?? (current?.pid === pid ? current.process_start_time : undefined),
4854
+ },
4855
+ cwd,
4856
+ env,
4857
+ );
4858
+ });
4859
+ }
3904
4860
  export async function writeGjcWorkerInbox(
3905
4861
  teamName: string,
3906
4862
  worker: string,
@@ -4046,12 +5002,13 @@ export async function executeGjcTeamApiOperation(
4046
5002
  cwd = process.cwd(),
4047
5003
  env: NodeJS.ProcessEnv = process.env,
4048
5004
  ): Promise<unknown> {
5005
+ const resolvedOperation = resolveGjcTeamApiOperation(operation);
4049
5006
  const teamName = String(input.team_name ?? input.teamName ?? "").trim();
4050
5007
  if (!teamName) throw new Error("missing_team_name");
4051
5008
  const workerInput = input.worker ?? input.worker_id ?? input.workerId;
4052
5009
  const worker = String(workerInput ?? "worker-1");
4053
5010
  const explicitWorker = workerInput == null ? undefined : String(workerInput);
4054
- switch (operation) {
5011
+ switch (resolvedOperation) {
4055
5012
  case "list-tasks":
4056
5013
  return { tasks: await listGjcTeamTasks(teamName, cwd, env) };
4057
5014
  case "read-task":
@@ -4248,19 +5205,106 @@ export async function executeGjcTeamApiOperation(
4248
5205
  return readGjcWorkerHeartbeat(teamName, worker, cwd, env);
4249
5206
  case "recover-stale-claims":
4250
5207
  return recoverGjcTeamStaleClaims(teamName, cwd, env);
4251
- case "update-worker-heartbeat":
5208
+ case "update-worker-heartbeat": {
5209
+ const pid = Number(input.pid ?? 0);
4252
5210
  return updateGjcWorkerHeartbeat(
4253
5211
  teamName,
4254
5212
  worker,
4255
5213
  {
4256
- pid: Number(input.pid ?? 0),
5214
+ pid,
4257
5215
  last_turn_at: now(),
4258
5216
  turn_count: Number(input.turn_count ?? 0),
4259
5217
  alive: Boolean(input.alive ?? true),
5218
+ process_start_time: await readLinuxProcessStartTime(pid),
4260
5219
  },
4261
5220
  cwd,
4262
5221
  env,
4263
5222
  );
5223
+ }
5224
+ case "read-worker-memory-guard": {
5225
+ const dir = await findTeamDir(teamName, cwd, env);
5226
+ const config = await readConfig(dir);
5227
+ assertKnownWorker(config, worker);
5228
+ return readWorkerMemoryGuardLedger(dir, worker, normalizeWorkerMemoryGuardPlatform(input.platform));
5229
+ }
5230
+ case "update-worker-memory-guard": {
5231
+ const dir = await findTeamDir(teamName, cwd, env);
5232
+ return withGjcTeamMutationFence(dir, async () => {
5233
+ const config = await readConfig(dir);
5234
+ assertKnownWorker(config, worker);
5235
+ const platform = normalizeWorkerMemoryGuardPlatform(input.platform);
5236
+ const tasks = await readTasks(dir);
5237
+ const currentTaskIdInput = input.current_task_id ?? input.currentTaskId;
5238
+ if (typeof currentTaskIdInput === "string" && !currentTaskIdInput.trim())
5239
+ throw new Error("invalid_worker_memory_guard_task_id");
5240
+ const currentTaskId =
5241
+ typeof currentTaskIdInput === "string"
5242
+ ? currentTaskIdInput.trim()
5243
+ : findGjcTeamClaimedTaskForWorker(tasks, worker)?.id;
5244
+ const existing = await readWorkerMemoryGuardLedger(dir, worker, platform);
5245
+ const retryLimitInput = Number(input.retry_limit ?? input.retryLimit ?? existing.retry_limit);
5246
+ if (!Number.isInteger(retryLimitInput) || retryLimitInput <= 0)
5247
+ throw new Error(`invalid_worker_memory_guard_retry_limit:${retryLimitInput}`);
5248
+ const pidProbe = normalizeGjcTeamWorkerMemoryGuardPidProbe(input.pid_probe ?? input.pidProbe);
5249
+ const stateInput = typeof input.state === "string" ? input.state.trim() : existing.state;
5250
+ if (!["idle", "advisory", "retrying", "checkpointed", "replaced", "blocked"].includes(stateInput))
5251
+ throw new Error(`invalid_worker_memory_guard_state:${stateInput}`);
5252
+ const updated: GjcTeamWorkerMemoryGuardLedger = {
5253
+ ...existing,
5254
+ platform,
5255
+ state: stateInput as GjcTeamWorkerMemoryGuardLedger["state"],
5256
+ automatic_action_allowed:
5257
+ typeof input.automatic_action_allowed === "boolean"
5258
+ ? input.automatic_action_allowed
5259
+ : typeof input.automaticActionAllowed === "boolean"
5260
+ ? input.automaticActionAllowed
5261
+ : existing.automatic_action_allowed,
5262
+ retry_limit: retryLimitInput,
5263
+ current_task_id: currentTaskId,
5264
+ last_incident_id:
5265
+ typeof input.incident_id === "string"
5266
+ ? input.incident_id
5267
+ : typeof input.incidentId === "string"
5268
+ ? input.incidentId
5269
+ : existing.last_incident_id,
5270
+ last_reason:
5271
+ typeof input.reason === "string" && input.reason.trim() ? input.reason.trim() : existing.last_reason,
5272
+ last_pid_probe: pidProbe ?? existing.last_pid_probe,
5273
+ updated_at: now(),
5274
+ };
5275
+ return writeWorkerMemoryGuardLedger(dir, updated);
5276
+ });
5277
+ }
5278
+ case "apply-worker-memory-guard":
5279
+ return applyWorkerMemoryGuard({
5280
+ teamName,
5281
+ workerId: explicitWorker,
5282
+ requestedWorkerId: worker,
5283
+ reason: typeof input.reason === "string" ? input.reason : undefined,
5284
+ incidentId:
5285
+ typeof input.incident_id === "string"
5286
+ ? input.incident_id
5287
+ : typeof input.incidentId === "string"
5288
+ ? input.incidentId
5289
+ : undefined,
5290
+ platform: normalizeWorkerMemoryGuardPlatform(input.platform),
5291
+ pidProbe: normalizeGjcTeamWorkerMemoryGuardPidProbe(input.pid_probe ?? input.pidProbe),
5292
+ candidates: input.candidates,
5293
+ cwd,
5294
+ env,
5295
+ allowAutomaticAction:
5296
+ typeof input.automatic_action_allowed === "boolean"
5297
+ ? input.automatic_action_allowed
5298
+ : typeof input.automaticActionAllowed === "boolean"
5299
+ ? input.automaticActionAllowed
5300
+ : undefined,
5301
+ replacementToken:
5302
+ typeof input.replacement_token === "string"
5303
+ ? input.replacement_token
5304
+ : typeof input.replacementToken === "string"
5305
+ ? input.replacementToken
5306
+ : undefined,
5307
+ });
4264
5308
  case "write-worker-inbox":
4265
5309
  return writeGjcWorkerInbox(teamName, worker, String(input.content ?? ""), cwd, env);
4266
5310
  case "write-worker-identity":
@@ -4310,7 +5354,8 @@ export async function executeGjcTeamApiOperation(
4310
5354
  case "read-shutdown-ack":
4311
5355
  return readGjcShutdownAck(teamName, worker, cwd, env);
4312
5356
  default:
4313
- throw new Error(`unknown_team_api_operation:${operation}`);
5357
+ resolvedOperation satisfies never;
5358
+ throw new UnknownGjcTeamApiOperationError(operation, []);
4314
5359
  }
4315
5360
  }
4316
5361