@gajae-code/coding-agent 0.14.2 → 0.15.0

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 (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -1,878 +0,0 @@
1
- /** File-backed team task store, claims, leases, and completion evidence. */
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
- import { randomUUID } from "node:crypto";
4
- import * as fs from "node:fs/promises";
5
- import * as path from "node:path";
6
- import { sessionIdFromDirName } from "./session-layout";
7
- import {
8
- createJsonNoClobber,
9
- deleteIfOwned,
10
- removeFileAudited,
11
- withWorkflowStateLock,
12
- writeJsonAtomic,
13
- } from "./state-writer";
14
- import type { GjcTeamConfig, GjcTeamMailboxMessage } from "./team-runtime";
15
-
16
- export type GjcTeamNotificationDeliveryState =
17
- | "pending"
18
- | "sent"
19
- | "queued"
20
- | "deferred"
21
- | "failed"
22
- | "delivered"
23
- | "acknowledged";
24
- export type GjcTeamPaneAttemptResult = "sent" | "queued" | "deferred" | "failed";
25
- export type GjcTeamMailboxDeliveryTransportKind = "sdk" | "pane";
26
- export interface GjcTeamNotification {
27
- id: string;
28
- kind: "mailbox_message" | "worker_lifecycle" | "invalid_attempt";
29
- team_name: string;
30
- recipient: string;
31
- source: { type: "message" | "task" | "worker" | "event"; id: string };
32
- idempotency_key?: string;
33
- delivery_state: GjcTeamNotificationDeliveryState;
34
- pane_attempt_result?: GjcTeamPaneAttemptResult;
35
- pane_attempt_reason?: string;
36
- pane_attempt_at?: string;
37
- created_at: string;
38
- updated_at: string;
39
- replay_count: number;
40
- }
41
-
42
- export interface GjcTeamNotificationSummary {
43
- total: number;
44
- replay_eligible: number;
45
- by_state: Record<GjcTeamNotificationDeliveryState, number>;
46
- }
47
- export interface GjcTeamMailboxDeliveryInput {
48
- team_name: string;
49
- state_dir: string;
50
- config: GjcTeamConfig;
51
- notification: GjcTeamNotification;
52
- message: GjcTeamMailboxMessage;
53
- cwd: string;
54
- env: NodeJS.ProcessEnv;
55
- }
56
- export type GjcTeamMailboxDeliveryResult =
57
- | {
58
- transport: "sdk";
59
- state: GjcTeamNotificationDeliveryState;
60
- reason?: string;
61
- }
62
- | { transport: "pane"; state: GjcTeamPaneAttemptResult; reason?: string };
63
- export interface GjcTeamMailboxDeliveryTransport {
64
- deliverMailboxMessage(input: GjcTeamMailboxDeliveryInput): Promise<GjcTeamMailboxDeliveryResult | null>;
65
- }
66
-
67
- export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
68
- export interface GjcTeamTaskClaim {
69
- owner: string;
70
- token: string;
71
- leased_until: string;
72
- }
73
- export type GjcTeamTaskCompletionEvidenceKind = "command" | "inspection" | "artifact";
74
- export type GjcTeamTaskCompletionEvidenceStatus = "passed" | "failed" | "not_run" | "verified" | "rejected";
75
- export interface GjcTeamTaskCompletionEvidenceItem {
76
- kind: GjcTeamTaskCompletionEvidenceKind;
77
- status: GjcTeamTaskCompletionEvidenceStatus;
78
- summary: string;
79
- command?: string;
80
- artifact?: string;
81
- location?: string;
82
- output?: string;
83
- }
84
- export interface GjcTeamTaskCompletionEvidence {
85
- summary: string;
86
- items: GjcTeamTaskCompletionEvidenceItem[];
87
- files?: string[];
88
- notes?: string;
89
- recorded_by: string;
90
- recorded_at: string;
91
- }
92
- export interface GjcTeamTask {
93
- id: string;
94
- subject: string;
95
- description: string;
96
- title: string;
97
- objective: string;
98
- status: GjcTeamTaskStatus;
99
- assignee?: string;
100
- owner?: string;
101
- result?: string;
102
- completion_evidence?: GjcTeamTaskCompletionEvidence;
103
- error?: string;
104
- blocked_by?: string[];
105
- depends_on?: string[];
106
- lane?: string;
107
- required_role?: string;
108
- allowed_roles?: string[];
109
- version: number;
110
- claim?: GjcTeamTaskClaim;
111
- created_at: string;
112
- updated_at: string;
113
- completed_at?: string;
114
- }
115
- export type GjcTeamTaskMetadataInput = Partial<
116
- Pick<GjcTeamTask, "owner" | "lane" | "required_role" | "allowed_roles" | "depends_on" | "blocked_by">
117
- >;
118
- export interface GjcTeamTaskWorker {
119
- id: string;
120
- role: string;
121
- agent_type: string;
122
- }
123
- export interface GjcTeamApiClaimResult {
124
- ok: boolean;
125
- task?: GjcTeamTask;
126
- worker_id?: string;
127
- claim_token?: string;
128
- reason?: string;
129
- }
130
- export type GjcTeamWorkerClaimSelection =
131
- | { kind: "none" }
132
- | { kind: "exact"; task: GjcTeamTask; claim: GjcTeamTaskClaim }
133
- | { kind: "ambiguous"; tasks: GjcTeamTask[] };
134
-
135
- export function selectCurrentClaimedTaskForWorker(
136
- tasks: readonly GjcTeamTask[],
137
- workerId: string,
138
- ): GjcTeamWorkerClaimSelection {
139
- const claimed = tasks.filter(
140
- task =>
141
- task.status === "in_progress" &&
142
- task.assignee === workerId &&
143
- task.claim?.owner === workerId &&
144
- task.owner === workerId,
145
- );
146
- if (claimed.length === 0) return { kind: "none" };
147
- if (claimed.length === 1) {
148
- const task = claimed[0]!;
149
- return { kind: "exact", task, claim: task.claim! };
150
- }
151
- return { kind: "ambiguous", tasks: claimed };
152
- }
153
- export function findGjcTeamClaimedTaskForWorker(
154
- tasks: readonly GjcTeamTask[],
155
- workerId: string,
156
- ): GjcTeamTask | undefined {
157
- const active = selectCurrentClaimedTaskForWorker(tasks, workerId);
158
- if (active.kind === "exact") return active.task;
159
- if (active.kind === "ambiguous") return undefined;
160
- return tasks.find(task => task.status === "blocked" && task.assignee === workerId && task.claim?.owner === workerId);
161
- }
162
-
163
- type EventAppender = (event: {
164
- type: string;
165
- task_id?: string;
166
- worker?: string;
167
- message: string;
168
- data?: Record<string, unknown>;
169
- }) => Promise<void>;
170
- const now = () => new Date().toISOString();
171
- const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
172
- const isEnoent = (error: unknown): error is { code: string } => isRecord(error) && error.code === "ENOENT";
173
- const canonicalTaskKeys = new Set([
174
- "id",
175
- "subject",
176
- "description",
177
- "title",
178
- "objective",
179
- "status",
180
- "assignee",
181
- "owner",
182
- "result",
183
- "completion_evidence",
184
- "error",
185
- "blocked_by",
186
- "depends_on",
187
- "lane",
188
- "required_role",
189
- "allowed_roles",
190
- "version",
191
- "claim",
192
- "created_at",
193
- "updated_at",
194
- "completed_at",
195
- ]);
196
- const canonicalClaimKeys = new Set(["owner", "token", "leased_until"]);
197
- const canonicalEvidenceKeys = new Set(["summary", "items", "files", "notes", "recorded_by", "recorded_at"]);
198
- const canonicalEvidenceItemKeys = new Set(["kind", "status", "summary", "command", "artifact", "location", "output"]);
199
- const hasExactKeys = (value: Record<string, unknown>, keys: Set<string>) =>
200
- Object.keys(value).every(key => keys.has(key));
201
- const isNonEmptyString = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
202
- const isSafePersistedId = (value: unknown): value is string =>
203
- isNonEmptyString(value) && /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) && !value.includes("..");
204
- const isFiniteTimestamp = (value: unknown): value is string =>
205
- typeof value === "string" && Number.isFinite(Date.parse(value));
206
-
207
- /** The sole strict schema gate for persisted task and claim authority records. */
208
- export function isCanonicalPersistedGjcTeamTaskClaim(value: unknown): value is GjcTeamTaskClaim {
209
- if (!isRecord(value) || Array.isArray(value) || !hasExactKeys(value, canonicalClaimKeys)) return false;
210
- return isSafePersistedId(value.owner) && isNonEmptyString(value.token) && isFiniteTimestamp(value.leased_until);
211
- }
212
-
213
- function isCanonicalCompletionEvidence(value: unknown): value is GjcTeamTaskCompletionEvidence {
214
- if (!isRecord(value) || Array.isArray(value) || !hasExactKeys(value, canonicalEvidenceKeys)) return false;
215
- if (
216
- !isNonEmptyString(value.summary) ||
217
- !isSafePersistedId(value.recorded_by) ||
218
- !isFiniteTimestamp(value.recorded_at)
219
- )
220
- return false;
221
- if (!Array.isArray(value.items) || value.items.length === 0) return false;
222
- if (
223
- !value.items.every(item => {
224
- if (!isRecord(item) || Array.isArray(item) || !hasExactKeys(item, canonicalEvidenceItemKeys)) return false;
225
- if (!isNonEmptyString(item.summary) || !["command", "inspection", "artifact"].includes(String(item.kind)))
226
- return false;
227
- if (
228
- (item.kind === "command" && !["passed", "failed", "not_run"].includes(String(item.status))) ||
229
- (item.kind !== "command" && !["verified", "rejected"].includes(String(item.status)))
230
- )
231
- return false;
232
- if (item.command !== undefined && !isNonEmptyString(item.command)) return false;
233
- if (item.artifact !== undefined && !isNonEmptyString(item.artifact)) return false;
234
- if (item.location !== undefined && !isNonEmptyString(item.location)) return false;
235
- if (item.output !== undefined && typeof item.output !== "string") return false;
236
- return item.kind !== "command" || isNonEmptyString(item.command);
237
- })
238
- )
239
- return false;
240
- if (
241
- value.files !== undefined &&
242
- (!Array.isArray(value.files) || !value.files.every(file => typeof file === "string" && file.trim().length > 0))
243
- )
244
- return false;
245
- return value.notes === undefined || typeof value.notes === "string";
246
- }
247
-
248
- export function isCanonicalPersistedGjcTeamTask(value: unknown, fileId?: string): value is GjcTeamTask {
249
- if (!isRecord(value) || Array.isArray(value) || !hasExactKeys(value, canonicalTaskKeys)) return false;
250
- if (!isSafePersistedId(value.id) || (fileId !== undefined && value.id !== fileId)) return false;
251
- if (!["pending", "blocked", "in_progress", "completed", "failed"].includes(String(value.status))) return false;
252
- if (!["subject", "description", "title", "objective"].every(key => typeof value[key] === "string")) return false;
253
- if (!Number.isInteger(value.version) || (value.version as number) <= 0) return false;
254
- if (!isFiniteTimestamp(value.created_at) || !isFiniteTimestamp(value.updated_at)) return false;
255
- if (value.completed_at !== undefined && !isFiniteTimestamp(value.completed_at)) return false;
256
- if (value.owner !== undefined && !isSafePersistedId(value.owner)) return false;
257
- if (value.assignee !== undefined && !isSafePersistedId(value.assignee)) return false;
258
- if (value.result !== undefined && typeof value.result !== "string") return false;
259
- if (value.error !== undefined && typeof value.error !== "string") return false;
260
- if (value.lane !== undefined && !isNonEmptyString(value.lane)) return false;
261
- if (value.required_role !== undefined && !isNonEmptyString(value.required_role)) return false;
262
- if (
263
- value.allowed_roles !== undefined &&
264
- (!Array.isArray(value.allowed_roles) || !value.allowed_roles.every(isNonEmptyString))
265
- )
266
- return false;
267
- if (
268
- value.depends_on !== undefined &&
269
- (!Array.isArray(value.depends_on) || !value.depends_on.every(isSafePersistedId))
270
- )
271
- return false;
272
- if (
273
- value.blocked_by !== undefined &&
274
- (!Array.isArray(value.blocked_by) || !value.blocked_by.every(isSafePersistedId))
275
- )
276
- return false;
277
- if (value.completion_evidence !== undefined && !isCanonicalCompletionEvidence(value.completion_evidence))
278
- return false;
279
- if (value.claim !== undefined) {
280
- if (!isCanonicalPersistedGjcTeamTaskClaim(value.claim)) return false;
281
- if (value.status !== "in_progress" || value.owner !== value.claim.owner || value.assignee !== value.claim.owner)
282
- return false;
283
- }
284
- return true;
285
- }
286
- const safeId = (kind: string, value: string) => {
287
- if (
288
- !/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) ||
289
- value.includes("..") ||
290
- value.includes("/") ||
291
- value.includes("\\")
292
- )
293
- throw new Error(`invalid_${kind}:${value}`);
294
- return value;
295
- };
296
- const taskPath = (dir: string, id: string) => path.join(dir, "tasks", `${safeId("task_id", id)}.json`);
297
- const claimPath = (dir: string, id: string) => path.join(dir, "claims", `${safeId("task_id", id)}.json`);
298
- const writerOptions = (filePath: string, category: "state" | "prune", verb: string) => {
299
- const resolved = path.resolve(filePath);
300
- const marker = `${path.sep}.gjc${path.sep}`;
301
- const markerIndex = resolved.indexOf(marker);
302
- const cwd = markerIndex >= 0 ? resolved.slice(0, markerIndex) : process.cwd();
303
- const sessionId =
304
- resolved
305
- .split(path.sep)
306
- .map(segment => sessionIdFromDirName(segment))
307
- .find((value): value is string => Boolean(value)) ?? process.env.GJC_SESSION_ID?.trim();
308
- return sessionId
309
- ? {
310
- cwd,
311
- audit: { category, verb, owner: "gjc-runtime" as const, sessionId },
312
- }
313
- : { cwd };
314
- };
315
- async function readJson<T>(filePath: string): Promise<T | null> {
316
- try {
317
- return (await Bun.file(filePath).json()) as T;
318
- } catch (error) {
319
- if (isEnoent(error)) return null;
320
- throw error;
321
- }
322
- }
323
- async function writeJson(filePath: string, value: unknown): Promise<void> {
324
- await writeJsonAtomic(filePath, value, writerOptions(filePath, "state", "write"));
325
- }
326
-
327
- /**
328
- * Serializes mutable team facts across processes. Nested operations inherit the
329
- * outer filesystem lock and execute directly; independent callers queue locally
330
- * before contending for the cross-process lock.
331
- */
332
- const teamMutationFenceTails = new Map<string, Promise<void>>();
333
- const activeTeamMutationFences = new AsyncLocalStorage<ReadonlySet<string>>();
334
-
335
- export async function withGjcTeamMutationFence<T>(dir: string, fn: () => Promise<T>): Promise<T> {
336
- const lockPath = path.join(dir, "operations", "team-mutation.json");
337
- const active = activeTeamMutationFences.getStore();
338
- if (active?.has(lockPath)) return await fn();
339
- const previous = teamMutationFenceTails.get(lockPath) ?? Promise.resolve();
340
- let releaseQueue!: () => void;
341
- const queued = new Promise<void>(resolve => {
342
- releaseQueue = resolve;
343
- });
344
- teamMutationFenceTails.set(lockPath, queued);
345
- await previous;
346
- try {
347
- return await activeTeamMutationFences.run(new Set([...(active ?? []), lockPath]), () =>
348
- withWorkflowStateLock(lockPath, fn, writerOptions(lockPath, "state", "mutation-fence")),
349
- );
350
- } finally {
351
- releaseQueue();
352
- if (teamMutationFenceTails.get(lockPath) === queued) teamMutationFenceTails.delete(lockPath);
353
- }
354
- }
355
- function optionalString(value: unknown): string | undefined {
356
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
357
- }
358
- function optionalStringArray(value: unknown): string[] | undefined {
359
- if (!Array.isArray(value)) return undefined;
360
- const values = [...new Set(value.map(optionalString).filter((value): value is string => Boolean(value)))].sort();
361
- return values.length ? values : undefined;
362
- }
363
- export function taskMetadataFromInput(input: Record<string, unknown>, includeOwner = false): GjcTeamTaskMetadataInput {
364
- const result: GjcTeamTaskMetadataInput = {};
365
- const owner = optionalString(input.owner);
366
- if (includeOwner && owner) result.owner = owner;
367
- for (const [key, value] of [
368
- ["lane", optionalString(input.lane)],
369
- ["required_role", optionalString(input.required_role ?? input.requiredRole)],
370
- ["allowed_roles", optionalStringArray(input.allowed_roles ?? input.allowedRoles)],
371
- ["depends_on", optionalStringArray(input.depends_on ?? input.dependsOn)],
372
- ["blocked_by", optionalStringArray(input.blocked_by ?? input.blockedBy)],
373
- ] as const)
374
- if (value) Object.assign(result, { [key]: value });
375
- return result;
376
- }
377
- export function normalizeGjcTeamTask(raw: GjcTeamTask): GjcTeamTask {
378
- return {
379
- ...raw,
380
- status: raw.status === ("complete" as GjcTeamTaskStatus) ? "completed" : raw.status,
381
- subject: raw.subject ?? raw.title,
382
- description: raw.description ?? raw.objective,
383
- title: raw.title ?? raw.subject,
384
- objective: raw.objective ?? raw.description,
385
- version: raw.version ?? 1,
386
- lane: optionalString(raw.lane),
387
- required_role: optionalString(raw.required_role),
388
- allowed_roles: optionalStringArray(raw.allowed_roles),
389
- };
390
- }
391
- function evidenceError(id: string, field: string) {
392
- return new Error(`invalid_completion_evidence:${id}:${field}`);
393
- }
394
- function required(id: string, field: string, value: unknown, max = 4000): string {
395
- if (typeof value !== "string" || !value.trim() || value.trim().length > max) throw evidenceError(id, field);
396
- return value.trim();
397
- }
398
- function optional(id: string, field: string, value: unknown, max = 8000): string | undefined {
399
- if (value == null) return undefined;
400
- if (typeof value !== "string" || value.trim().length > max) throw evidenceError(id, field);
401
- return value.trim() || undefined;
402
- }
403
- function verified(item: GjcTeamTaskCompletionEvidenceItem) {
404
- return (
405
- (item.kind === "command" && item.status === "passed") || (item.kind !== "command" && item.status === "verified")
406
- );
407
- }
408
- export function normalizeGjcTeamTaskCompletionEvidence(
409
- id: string,
410
- owner: string,
411
- input: unknown,
412
- recordedAt = now(),
413
- ): GjcTeamTaskCompletionEvidence {
414
- if (!isRecord(input) || Array.isArray(input)) throw new Error(`completion_evidence_required:${id}`);
415
- if (!Array.isArray(input.items) || !input.items.length) throw evidenceError(id, "items");
416
- const items = input.items.map(value => {
417
- if (!isRecord(value) || Array.isArray(value)) throw evidenceError(id, "items");
418
- const kind = required(id, "items.kind", value.kind);
419
- if (kind !== "command" && kind !== "inspection" && kind !== "artifact") throw evidenceError(id, "items.kind");
420
- const status = required(id, "items.status", value.status) as GjcTeamTaskCompletionEvidenceStatus;
421
- if (
422
- (kind === "command" && !["passed", "failed", "not_run"].includes(status)) ||
423
- (kind !== "command" && !["verified", "rejected"].includes(status))
424
- )
425
- throw evidenceError(id, "items.status");
426
- const item: GjcTeamTaskCompletionEvidenceItem = {
427
- kind,
428
- status,
429
- summary: required(id, "items.summary", value.summary),
430
- };
431
- for (const [key, entry] of [
432
- ["command", optional(id, "items.command", value.command)],
433
- ["artifact", optional(id, "items.artifact", value.artifact)],
434
- ["location", optional(id, "items.location", value.location)],
435
- ["output", optional(id, "items.output", value.output)],
436
- ] as const)
437
- if (entry) Object.assign(item, { [key]: entry });
438
- if (kind === "command" && !item.command) throw evidenceError(id, "items.command");
439
- return item;
440
- });
441
- if (!items.some(verified)) throw new Error(`completion_evidence_no_verified_item:${id}`);
442
- let files: string[] | undefined;
443
- if (input.files != null) {
444
- if (!Array.isArray(input.files)) throw evidenceError(id, "files");
445
- files = [
446
- ...new Set(
447
- input.files.map(file => {
448
- if (typeof file !== "string") throw evidenceError(id, "files");
449
- const normalized = file.trim().replace(/\\/g, "/");
450
- if (
451
- !normalized ||
452
- normalized.includes("\0") ||
453
- path.isAbsolute(normalized) ||
454
- normalized.split("/").includes("..")
455
- )
456
- throw evidenceError(id, "files");
457
- return normalized;
458
- }),
459
- ),
460
- ].sort();
461
- if (!files.length) files = undefined;
462
- }
463
- const evidence: GjcTeamTaskCompletionEvidence = {
464
- summary: required(id, "summary", input.summary),
465
- items,
466
- recorded_by: owner,
467
- recorded_at: recordedAt,
468
- };
469
- if (files) evidence.files = files;
470
- const notes = optional(id, "notes", input.notes);
471
- if (notes) evidence.notes = notes;
472
- return evidence;
473
- }
474
- export function getGjcTeamTaskCompletionEvidenceFailure(task: GjcTeamTask): string | null {
475
- if (task.status !== "completed") return `task_not_completed:${task.id}`;
476
- const evidence = task.completion_evidence;
477
- if (!isRecord(evidence) || Array.isArray(evidence)) return `completion_evidence_required:${task.id}`;
478
- if (typeof evidence.recorded_by !== "string" || !evidence.recorded_by.trim())
479
- return `invalid_completion_evidence:${task.id}:recorded_by`;
480
- if (typeof evidence.recorded_at !== "string" || !evidence.recorded_at.trim())
481
- return `invalid_completion_evidence:${task.id}:recorded_at`;
482
- try {
483
- normalizeGjcTeamTaskCompletionEvidence(task.id, evidence.recorded_by.trim(), evidence, evidence.recorded_at);
484
- return null;
485
- } catch (error) {
486
- return error instanceof Error ? error.message : `invalid_completion_evidence:${task.id}:unknown`;
487
- }
488
- }
489
- export const isGjcTeamTaskCompletionVerified = (task: GjcTeamTask) =>
490
- getGjcTeamTaskCompletionEvidenceFailure(task) === null;
491
- function taskRecord(value: unknown): value is GjcTeamTask {
492
- return (
493
- isRecord(value) &&
494
- typeof value.id === "string" &&
495
- typeof value.status === "string" &&
496
- ["pending", "blocked", "in_progress", "completed", "failed", "complete"].includes(value.status) &&
497
- (typeof value.subject === "string" || typeof value.title === "string") &&
498
- (typeof value.description === "string" || typeof value.objective === "string")
499
- );
500
- }
501
- export async function readGjcTeamTasksFromDir(dir: string): Promise<GjcTeamTask[]> {
502
- try {
503
- const entries = await fs.readdir(path.join(dir, "tasks"), {
504
- withFileTypes: true,
505
- });
506
- const records = await Promise.all(
507
- entries
508
- .filter(entry => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".evidence.json"))
509
- .map(entry => readJson<unknown>(path.join(dir, "tasks", entry.name))),
510
- );
511
- return records
512
- .filter(taskRecord)
513
- .map(normalizeGjcTeamTask)
514
- .sort((a, b) => a.id.localeCompare(b.id));
515
- } catch (error) {
516
- if (isEnoent(error)) return [];
517
- throw error;
518
- }
519
- }
520
- async function writeGjcTeamTaskToDir(dir: string, task: GjcTeamTask): Promise<void> {
521
- await writeJson(taskPath(dir, task.id), normalizeGjcTeamTask(task));
522
- }
523
-
524
- function eligibility(task: GjcTeamTask, worker: GjcTeamTaskWorker, tasks: GjcTeamTask[]): string | null {
525
- if (task.status !== "pending") return `task_not_pending:${task.id}`;
526
- if (task.owner && task.owner !== worker.id) return `task_owner_mismatch:${task.id}:${task.owner}`;
527
- if (task.assignee && task.assignee !== worker.id) return `task_assignee_mismatch:${task.id}:${task.assignee}`;
528
- const roles = new Set([worker.role, worker.agent_type].map(value => value.trim()).filter(Boolean));
529
- if (task.required_role && !roles.has(task.required_role))
530
- return `task_role_mismatch:${task.id}:${task.required_role}`;
531
- if (task.allowed_roles?.length && !task.allowed_roles.some(role => roles.has(role)))
532
- return `task_role_mismatch:${task.id}:${task.allowed_roles.join(",")}`;
533
- if (task.blocked_by?.length) return `task_blocked:${task.id}:${task.blocked_by.join(",")}`;
534
- for (const dependency of task.depends_on ?? [])
535
- if (!tasks.find(candidate => candidate.id === dependency && isGjcTeamTaskCompletionVerified(candidate)))
536
- return `task_dependency_incomplete:${task.id}:${dependency}`;
537
- return null;
538
- }
539
- function claimRecord(value: unknown): GjcTeamTaskClaim | undefined {
540
- if (
541
- !isRecord(value) ||
542
- typeof value.owner !== "string" ||
543
- typeof value.token !== "string" ||
544
- typeof value.leased_until !== "string" ||
545
- !value.owner ||
546
- !value.token ||
547
- !value.leased_until
548
- )
549
- return undefined;
550
- return {
551
- owner: value.owner,
552
- token: value.token,
553
- leased_until: value.leased_until,
554
- };
555
- }
556
- const expired = (value: string | undefined) =>
557
- Boolean(value && Number.isFinite(Date.parse(value)) && Date.parse(value) <= Date.now());
558
-
559
- const teamTaskMutationCapability = Symbol("team-task-mutation-capability");
560
-
561
- /**
562
- * This is intentionally structural only: callers can invoke methods supplied by
563
- * `withGjcTeamTaskMutation`, but cannot create an active capability. Each method
564
- * closes over a private brand and is revoked when that callback settles.
565
- */
566
- export interface GjcTeamTaskMutationCapability {
567
- create(subject: string, description: string, options: GjcTeamTaskMetadataInput): Promise<GjcTeamTask>;
568
- update(
569
- id: string,
570
- updates: Partial<
571
- Pick<
572
- GjcTeamTask,
573
- "subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
574
- >
575
- >,
576
- ): Promise<GjcTeamTask>;
577
- claim(worker: GjcTeamTaskWorker, id?: string): Promise<GjcTeamApiClaimResult>;
578
- transition(
579
- id: string,
580
- status: GjcTeamTaskStatus,
581
- token?: string,
582
- workerId?: string,
583
- evidenceInput?: unknown,
584
- ): Promise<GjcTeamTask>;
585
- release(id: string, token: string, workerId: string): Promise<GjcTeamTask>;
586
- writeRecovered(task: GjcTeamTask): Promise<void>;
587
- }
588
-
589
- export async function withGjcTeamTaskMutation<T>(
590
- store: GjcTeamTaskStore,
591
- fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>,
592
- ): Promise<T> {
593
- return withGjcTeamMutationFence(store.dir, () => store.withMutationCapability(teamTaskMutationCapability, fn));
594
- }
595
-
596
- export class GjcTeamTaskStore {
597
- constructor(
598
- readonly dir: string,
599
- readonly appendEvent: EventAppender,
600
- ) {}
601
- async withMutationCapability<T>(
602
- capabilityBrand: typeof teamTaskMutationCapability,
603
- fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>,
604
- ): Promise<T> {
605
- if (capabilityBrand !== teamTaskMutationCapability) throw new Error("team_mutation_capability_required");
606
- const brand = Symbol("active-team-task-mutation");
607
- let active = true;
608
- const mutations: Promise<unknown>[] = [];
609
- const assertActive = () => {
610
- if (!active || brand.description !== "active-team-task-mutation")
611
- throw new Error("team_mutation_capability_revoked");
612
- };
613
- const track = <T>(mutation: () => Promise<T>): Promise<T> => {
614
- assertActive();
615
- const pending = mutation();
616
- mutations.push(pending);
617
- void pending.catch(() => undefined);
618
- return pending;
619
- };
620
- const capability = Object.freeze<GjcTeamTaskMutationCapability>({
621
- create: (subject, description, options) => track(() => this.#createUnlocked(subject, description, options)),
622
- update: (id, updates) => track(() => this.#updateUnlocked(id, updates)),
623
- claim: (worker, id) => track(() => this.#claimUnlocked(worker, id)),
624
- transition: (id, status, token, workerId, evidenceInput) =>
625
- track(() => this.#transitionUnlocked(id, status, token, workerId, evidenceInput)),
626
- release: (id, token, workerId) => track(() => this.#releaseUnlocked(id, token, workerId)),
627
- writeRecovered: task => track(() => writeGjcTeamTaskToDir(this.dir, task)),
628
- });
629
- let callbackResult: T | undefined;
630
- let callbackError: unknown;
631
- let callbackFailed = false;
632
- try {
633
- callbackResult = await fn(capability);
634
- } catch (error) {
635
- callbackFailed = true;
636
- callbackError = error;
637
- }
638
- // Callback settlement is the admission boundary. Drain only mutations that
639
- // were admitted before it; an escaped capability cannot add work while draining.
640
- active = false;
641
- const settled: PromiseSettledResult<unknown>[] = [];
642
- let drained = 0;
643
- while (drained < mutations.length) {
644
- const pending = mutations.slice(drained);
645
- drained = mutations.length;
646
- settled.push(...(await Promise.allSettled(pending)));
647
- }
648
- if (callbackFailed) throw callbackError;
649
- const failed = settled.find((result): result is PromiseRejectedResult => result.status === "rejected");
650
- if (failed) throw failed.reason;
651
- return callbackResult as T;
652
- }
653
- async list() {
654
- return readGjcTeamTasksFromDir(this.dir);
655
- }
656
- async read(id: string) {
657
- const task = (await this.list()).find(candidate => candidate.id === id);
658
- if (!task) throw new Error(`task_not_found:${id}`);
659
- return task;
660
- }
661
- async create(subject: string, description: string, options: GjcTeamTaskMetadataInput) {
662
- return withGjcTeamMutationFence(this.dir, () => this.#createUnlocked(subject, description, options));
663
- }
664
- async #createUnlocked(subject: string, description: string, options: GjcTeamTaskMetadataInput) {
665
- const task: GjcTeamTask = {
666
- id: `task-${(await this.list()).length + 1}`,
667
- subject,
668
- description,
669
- title: subject,
670
- objective: description,
671
- status: "pending",
672
- ...options,
673
- version: 1,
674
- created_at: now(),
675
- updated_at: now(),
676
- };
677
- await writeGjcTeamTaskToDir(this.dir, task);
678
- await this.appendEvent({
679
- type: "task_created",
680
- task_id: task.id,
681
- message: subject,
682
- });
683
- return task;
684
- }
685
- async update(
686
- id: string,
687
- updates: Partial<
688
- Pick<
689
- GjcTeamTask,
690
- "subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
691
- >
692
- >,
693
- ) {
694
- return withGjcTeamMutationFence(this.dir, () => this.#updateUnlocked(id, updates));
695
- }
696
- async #updateUnlocked(
697
- id: string,
698
- updates: Partial<
699
- Pick<
700
- GjcTeamTask,
701
- "subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
702
- >
703
- >,
704
- ) {
705
- const task = await this.read(id);
706
- const updated = normalizeGjcTeamTask({
707
- ...task,
708
- ...updates,
709
- title: updates.subject ?? task.title,
710
- objective: updates.description ?? task.objective,
711
- version: task.version + 1,
712
- updated_at: now(),
713
- });
714
- await writeGjcTeamTaskToDir(this.dir, updated);
715
- await this.appendEvent({
716
- type: "task_updated",
717
- task_id: id,
718
- message: updated.subject,
719
- });
720
- return updated;
721
- }
722
- async claim(worker: GjcTeamTaskWorker, id?: string): Promise<GjcTeamApiClaimResult> {
723
- return withGjcTeamMutationFence(this.dir, () => this.#claimUnlocked(worker, id));
724
- }
725
- async #claimUnlocked(worker: GjcTeamTaskWorker, id?: string): Promise<GjcTeamApiClaimResult> {
726
- const tasks = await this.list();
727
- const task = id
728
- ? tasks.find(candidate => candidate.id === id)
729
- : tasks.find(candidate => eligibility(candidate, worker, tasks) === null);
730
- if (!task)
731
- return {
732
- ok: false,
733
- reason: id ? `task_not_found:${id}` : "no_pending_task",
734
- };
735
- const reason = eligibility(task, worker, tasks);
736
- if (reason) return { ok: false, reason };
737
- const existing = task.claim ?? claimRecord(await readJson<unknown>(claimPath(this.dir, task.id)));
738
- if (existing && !expired(existing.leased_until)) return { ok: false, reason: `task_already_claimed:${task.id}` };
739
- const claim: GjcTeamTaskClaim = {
740
- owner: worker.id,
741
- token: randomUUID(),
742
- leased_until: new Date(Date.now() + 30 * 60_000).toISOString(),
743
- };
744
- try {
745
- await createJsonNoClobber(
746
- claimPath(this.dir, task.id),
747
- claim,
748
- writerOptions(claimPath(this.dir, task.id), "state", "claim"),
749
- );
750
- } catch (error) {
751
- if (isRecord(error) && error.code === "EEXIST")
752
- return { ok: false, reason: `task_already_claimed:${task.id}` };
753
- throw error;
754
- }
755
- const current = await this.read(task.id);
756
- const currentReason = eligibility(current, worker, await this.list());
757
- if (currentReason || current.status !== "pending") {
758
- await deleteIfOwned(claimPath(this.dir, task.id), {
759
- ...writerOptions(claimPath(this.dir, task.id), "prune", "rollback"),
760
- predicate: current => (current as GjcTeamTaskClaim).token === claim.token,
761
- });
762
- return {
763
- ok: false,
764
- reason: currentReason ?? `task_not_pending:${task.id}`,
765
- };
766
- }
767
- const updated = {
768
- ...current,
769
- status: "in_progress" as const,
770
- assignee: worker.id,
771
- owner: worker.id,
772
- claim,
773
- version: current.version + 1,
774
- updated_at: now(),
775
- };
776
- try {
777
- await writeGjcTeamTaskToDir(this.dir, updated);
778
- } catch (error) {
779
- await deleteIfOwned(claimPath(this.dir, task.id), {
780
- ...writerOptions(claimPath(this.dir, task.id), "prune", "rollback"),
781
- predicate: current => (current as GjcTeamTaskClaim).token === claim.token,
782
- });
783
- throw error;
784
- }
785
- await this.appendEvent({
786
- type: "task_claimed",
787
- task_id: updated.id,
788
- worker: worker.id,
789
- message: "Worker claimed task",
790
- });
791
- return {
792
- ok: true,
793
- task: updated,
794
- worker_id: worker.id,
795
- claim_token: claim.token,
796
- };
797
- }
798
- async transition(id: string, status: GjcTeamTaskStatus, token?: string, workerId?: string, evidenceInput?: unknown) {
799
- return withGjcTeamMutationFence(this.dir, () =>
800
- this.#transitionUnlocked(id, status, token, workerId, evidenceInput),
801
- );
802
- }
803
- async #transitionUnlocked(
804
- id: string,
805
- status: GjcTeamTaskStatus,
806
- token?: string,
807
- workerId?: string,
808
- evidenceInput?: unknown,
809
- ) {
810
- const task = await this.read(id);
811
- if (status === "pending") throw new Error(`invalid_task_transition:${id}:pending_requires_release`);
812
- if (task.status === "completed" || task.status === "failed") throw new Error(`task_terminal:${id}`);
813
- if (!task.claim || !token) throw new Error(`claim_token_required:${id}`);
814
- if (task.claim.token !== token) throw new Error(`claim_token_mismatch:${id}`);
815
- if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${id}`);
816
- const terminal = status === "completed" || status === "failed";
817
- const transitionedAt = now();
818
- const evidence =
819
- status === "completed"
820
- ? normalizeGjcTeamTaskCompletionEvidence(id, task.claim.owner, evidenceInput, transitionedAt)
821
- : undefined;
822
- const updated: GjcTeamTask = {
823
- ...task,
824
- status,
825
- claim: terminal ? undefined : task.claim,
826
- version: task.version + 1,
827
- updated_at: transitionedAt,
828
- ...(terminal ? { completed_at: transitionedAt } : {}),
829
- ...(evidence ? { completion_evidence: evidence } : {}),
830
- };
831
- await writeGjcTeamTaskToDir(this.dir, updated);
832
- if (terminal)
833
- await removeFileAudited(claimPath(this.dir, id), writerOptions(claimPath(this.dir, id), "prune", "terminal"));
834
- const data: Record<string, unknown> = { status };
835
- if (evidence)
836
- data.completion_evidence = {
837
- recorded_by: evidence.recorded_by,
838
- item_count: evidence.items.length,
839
- verified_item_count: evidence.items.filter(verified).length,
840
- files_count: evidence.files?.length ?? 0,
841
- };
842
- await this.appendEvent({
843
- type: "task_transitioned",
844
- task_id: id,
845
- message: "Task status changed",
846
- data,
847
- });
848
- return updated;
849
- }
850
- async release(id: string, token: string, workerId: string) {
851
- return withGjcTeamMutationFence(this.dir, () => this.#releaseUnlocked(id, token, workerId));
852
- }
853
- async #releaseUnlocked(id: string, token: string, workerId: string) {
854
- const task = await this.read(id);
855
- if (!task.claim || task.claim.token !== token || task.claim.owner !== workerId)
856
- throw new Error(`claim_token_mismatch:${id}`);
857
- const updated: GjcTeamTask = {
858
- ...task,
859
- status: "pending",
860
- assignee: undefined,
861
- claim: undefined,
862
- version: task.version + 1,
863
- updated_at: now(),
864
- };
865
- await writeGjcTeamTaskToDir(this.dir, updated);
866
- await deleteIfOwned(claimPath(this.dir, id), {
867
- ...writerOptions(claimPath(this.dir, id), "prune", "release"),
868
- predicate: current => (current as GjcTeamTaskClaim).token === token,
869
- });
870
- await this.appendEvent({
871
- type: "task_claim_released",
872
- task_id: id,
873
- worker: workerId,
874
- message: "Task claim released",
875
- });
876
- return updated;
877
- }
878
- }