@cuongtran001/kanna 1.2.0 → 1.3.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 (196) hide show
  1. package/dist/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
  5. package/dist/client/assets/channel-BEYTrUAm.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
  27. package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
  28. package/dist/client/assets/index-CLsOWXff.css +32 -0
  29. package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
  30. package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
  31. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
  34. package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
  35. package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
  36. package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
  44. package/dist/client/assets/{timeline-definition-PNZ67QCA-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
  45. package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
  46. package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
  47. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
  48. package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
  49. package/dist/client/index.html +2 -2
  50. package/package.json +1 -1
  51. package/src/server/agent-coordinator-types.ts +158 -0
  52. package/src/server/agent-coordinator.ts +919 -0
  53. package/src/server/agent-deps-builders.ts +425 -0
  54. package/src/server/agent.oauth-rotation.test.ts +101 -0
  55. package/src/server/agent.test.ts +24 -8
  56. package/src/server/agent.ts +35 -4963
  57. package/src/server/auto-continue/read-model.test.ts +2 -2
  58. package/src/server/auto-continue/read-model.ts +4 -1
  59. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  60. package/src/server/claude-autocontinue-commands.ts +256 -0
  61. package/src/server/claude-cancel-handler.test.ts +472 -0
  62. package/src/server/claude-cancel-handler.ts +241 -0
  63. package/src/server/claude-chat-management.test.ts +395 -0
  64. package/src/server/claude-chat-management.ts +251 -0
  65. package/src/server/claude-harness-stream.test.ts +273 -0
  66. package/src/server/claude-harness-stream.ts +158 -0
  67. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  68. package/src/server/claude-loop-orch-commands.ts +466 -0
  69. package/src/server/claude-message-normalizer.test.ts +213 -0
  70. package/src/server/claude-message-normalizer.ts +436 -0
  71. package/src/server/claude-prompt-helpers.test.ts +248 -0
  72. package/src/server/claude-prompt-helpers.ts +139 -0
  73. package/src/server/claude-sdk-queue.test.ts +169 -0
  74. package/src/server/claude-sdk-queue.ts +82 -0
  75. package/src/server/claude-send-command.test.ts +414 -0
  76. package/src/server/claude-send-command.ts +420 -0
  77. package/src/server/claude-session-config-helpers.test.ts +279 -0
  78. package/src/server/claude-session-config-helpers.ts +151 -0
  79. package/src/server/claude-session-config.test.ts +250 -0
  80. package/src/server/claude-session-config.ts +159 -0
  81. package/src/server/claude-session-error-handler.test.ts +498 -0
  82. package/src/server/claude-session-error-handler.ts +361 -0
  83. package/src/server/claude-session-lifecycle.test.ts +669 -0
  84. package/src/server/claude-session-lifecycle.ts +283 -0
  85. package/src/server/claude-session-rebuild.test.ts +224 -0
  86. package/src/server/claude-session-rebuild.ts +101 -0
  87. package/src/server/claude-session-runner.test.ts +523 -0
  88. package/src/server/claude-session-runner.ts +521 -0
  89. package/src/server/claude-session-spawner.test.ts +401 -0
  90. package/src/server/claude-session-spawner.ts +401 -0
  91. package/src/server/claude-session-start.test.ts +205 -0
  92. package/src/server/claude-session-start.ts +277 -0
  93. package/src/server/claude-session-state-queries.test.ts +344 -0
  94. package/src/server/claude-session-state-queries.ts +160 -0
  95. package/src/server/claude-session-state.ts +104 -0
  96. package/src/server/claude-slash-commands.test.ts +277 -0
  97. package/src/server/claude-slash-commands.ts +234 -0
  98. package/src/server/claude-spawn-helpers.test.ts +221 -0
  99. package/src/server/claude-spawn-helpers.ts +179 -0
  100. package/src/server/claude-steer-log.test.ts +243 -0
  101. package/src/server/claude-steer-log.ts +58 -0
  102. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  103. package/src/server/claude-subagent-tool-response.ts +165 -0
  104. package/src/server/claude-subagent-wiring.test.ts +263 -0
  105. package/src/server/claude-subagent-wiring.ts +324 -0
  106. package/src/server/claude-tool-respond.test.ts +321 -0
  107. package/src/server/claude-tool-respond.ts +142 -0
  108. package/src/server/claude-turn-runner.test.ts +310 -0
  109. package/src/server/claude-turn-runner.ts +228 -0
  110. package/src/server/claude-turn-starter.test.ts +266 -0
  111. package/src/server/claude-turn-starter.ts +566 -0
  112. package/src/server/claude-usage-math.test.ts +169 -0
  113. package/src/server/claude-usage-math.ts +132 -0
  114. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  115. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  116. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  117. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  118. package/src/server/diff-store-errors.ts +85 -0
  119. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  120. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  121. package/src/server/diff-store-io.adapter.ts +27 -0
  122. package/src/server/diff-store-parse.test.ts +212 -0
  123. package/src/server/diff-store-parse.ts +122 -0
  124. package/src/server/diff-store-state.ts +82 -0
  125. package/src/server/diff-store.ts +76 -1854
  126. package/src/server/event-store-apply.test.ts +280 -0
  127. package/src/server/event-store-apply.ts +132 -0
  128. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  129. package/src/server/event-store-chat-lifecycle.ts +441 -0
  130. package/src/server/event-store-entity-write.ts +356 -0
  131. package/src/server/event-store-helpers.test.ts +230 -0
  132. package/src/server/event-store-helpers.ts +235 -0
  133. package/src/server/event-store-init.ts +276 -0
  134. package/src/server/event-store-messages.adapter.test.ts +365 -0
  135. package/src/server/event-store-messages.adapter.ts +213 -0
  136. package/src/server/event-store-orch-subscription.ts +124 -0
  137. package/src/server/event-store-orch.test.ts +462 -0
  138. package/src/server/event-store-orch.ts +312 -0
  139. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  140. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  141. package/src/server/event-store-snapshot.test.ts +597 -0
  142. package/src/server/event-store-snapshot.ts +658 -0
  143. package/src/server/event-store-subagent.test.ts +408 -0
  144. package/src/server/event-store-subagent.ts +198 -0
  145. package/src/server/event-store-tool-requests.test.ts +210 -0
  146. package/src/server/event-store-tool-requests.ts +131 -0
  147. package/src/server/event-store-transcript-write.adapter.ts +285 -0
  148. package/src/server/event-store-write-ops.test.ts +554 -0
  149. package/src/server/event-store-write-ops.ts +529 -0
  150. package/src/server/event-store.test.ts +35 -0
  151. package/src/server/event-store.ts +251 -2284
  152. package/src/server/events.ts +3 -0
  153. package/src/server/harness-types.ts +15 -1
  154. package/src/server/oauth-errors.ts +19 -0
  155. package/src/server/read-models.ts +26 -1
  156. package/src/server/subagent-orchestrator.ts +3 -0
  157. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  158. package/src/server/ws-router-agent-ctrl.ts +137 -0
  159. package/src/server/ws-router-broadcast.ts +488 -0
  160. package/src/server/ws-router-chat.test.ts +461 -0
  161. package/src/server/ws-router-chat.ts +279 -0
  162. package/src/server/ws-router-defaults.test.ts +283 -0
  163. package/src/server/ws-router-defaults.ts +434 -0
  164. package/src/server/ws-router-diff.test.ts +167 -0
  165. package/src/server/ws-router-diff.ts +250 -0
  166. package/src/server/ws-router-envelope.ts +329 -0
  167. package/src/server/ws-router-misc.test.ts +422 -0
  168. package/src/server/ws-router-misc.ts +265 -0
  169. package/src/server/ws-router-orch.test.ts +190 -0
  170. package/src/server/ws-router-orch.ts +93 -0
  171. package/src/server/ws-router-project.test.ts +403 -0
  172. package/src/server/ws-router-project.ts +256 -0
  173. package/src/server/ws-router-push.test.ts +193 -0
  174. package/src/server/ws-router-push.ts +120 -0
  175. package/src/server/ws-router-settings.test.ts +173 -0
  176. package/src/server/ws-router-settings.ts +416 -0
  177. package/src/server/ws-router-skills.test.ts +194 -0
  178. package/src/server/ws-router-skills.ts +204 -0
  179. package/src/server/ws-router-utils.ts +224 -0
  180. package/src/server/ws-router.ts +339 -2254
  181. package/src/shared/app-settings-types.ts +365 -0
  182. package/src/shared/core-types.ts +27 -0
  183. package/src/shared/git-diff-types.ts +165 -0
  184. package/src/shared/loop-progress.test.ts +106 -0
  185. package/src/shared/loop-progress.ts +88 -0
  186. package/src/shared/mcp-types.ts +105 -0
  187. package/src/shared/provider-model-types.ts +437 -0
  188. package/src/shared/subagent-types.ts +198 -0
  189. package/src/shared/tool-call-types.ts +314 -0
  190. package/src/shared/transcript-types.ts +270 -0
  191. package/src/shared/types.ts +27 -1668
  192. package/dist/client/assets/channel-BMkhjiLI.js +0 -1
  193. package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
  194. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
  195. package/dist/client/assets/index-H6C-f2Vp.css +0 -32
  196. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
@@ -0,0 +1,462 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import type { OrchRunConfig, OrchTaskSpec } from "../shared/orchestration-types"
3
+ import type { OrchestrationEvent, OrchRunRecord } from "./events"
4
+ import {
5
+ applyOrchEvent,
6
+ gatedOrchTasks,
7
+ getAllOrchRunSnapshots,
8
+ getOrchLastPhaseOutput,
9
+ getOrchRunEvents,
10
+ getOrchRunSnapshot,
11
+ getOrchTaskSpec,
12
+ nonTerminalOrchTasks,
13
+ toOrchRunSnapshot,
14
+ } from "./event-store-orch"
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Fixtures
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const T = 1_000_000
21
+
22
+ const MINIMAL_CONFIG: OrchRunConfig = {
23
+ title: "test run",
24
+ repoRoot: "/repo",
25
+ baseBranch: "main",
26
+ maxParallelTasks: 2,
27
+ worktreePoolSize: 2,
28
+ maxAttempts: 3,
29
+ contextPrompt: null,
30
+ init: null,
31
+ workerSubagentId: "sub-1",
32
+ originChatId: "chat-1",
33
+ phases: [{ name: "implement", kind: "implement", parallel: 1, promptTemplate: "{{TASK}}" }],
34
+ gates: [],
35
+ verify: null,
36
+ }
37
+
38
+ const TASKS: OrchTaskSpec[] = [
39
+ { id: "t1", title: "Task 1", prompt: "do task 1", scopePaths: ["src/"] },
40
+ { id: "t2", title: "Task 2", prompt: "do task 2", scopePaths: [] },
41
+ ]
42
+
43
+ function makeCreatedEvent(runId = "run-1"): Extract<OrchestrationEvent, { type: "orch_run_created" }> {
44
+ return { v: 3, type: "orch_run_created", timestamp: T, runId, config: MINIMAL_CONFIG, tasks: TASKS }
45
+ }
46
+
47
+ function makeMap(): Map<string, OrchRunRecord> {
48
+ return new Map()
49
+ }
50
+
51
+ function seedRun(map: Map<string, OrchRunRecord>, runId = "run-1"): void {
52
+ applyOrchEvent(map, makeCreatedEvent(runId))
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // applyOrchEvent — run lifecycle
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe("applyOrchEvent", () => {
60
+ it("creates a run on orch_run_created", () => {
61
+ const m = makeMap()
62
+ seedRun(m)
63
+ expect(m.size).toBe(1)
64
+ const run = m.get("run-1")!
65
+ expect(run.status).toBe("running")
66
+ expect(run.tasksById.size).toBe(2)
67
+ expect(run.taskOrder).toEqual(["t1", "t2"])
68
+ })
69
+
70
+ it("initialises tasks in queued state with correct fields", () => {
71
+ const m = makeMap()
72
+ seedRun(m)
73
+ const t1 = m.get("run-1")!.tasksById.get("t1")!
74
+ expect(t1.state).toBe("queued")
75
+ expect(t1.prompt).toBe("do task 1")
76
+ expect(t1.scopePaths).toEqual(["src/"])
77
+ expect(t1.attempts).toBe(0)
78
+ expect(t1.ownerWorkerId).toBeNull()
79
+ })
80
+
81
+ it("records the creation event in the event log", () => {
82
+ const m = makeMap()
83
+ seedRun(m)
84
+ expect(m.get("run-1")!.eventLog).toHaveLength(1)
85
+ expect(m.get("run-1")!.eventLog[0]?.type).toBe("orch_run_created")
86
+ })
87
+
88
+ it("ignores events for unknown runId", () => {
89
+ const m = makeMap()
90
+ applyOrchEvent(m, {
91
+ v: 3, type: "orch_run_completed", timestamp: T + 1, runId: "ghost",
92
+ })
93
+ expect(m.size).toBe(0)
94
+ })
95
+
96
+ it("transitions run to completed on orch_run_completed", () => {
97
+ const m = makeMap()
98
+ seedRun(m)
99
+ applyOrchEvent(m, { v: 3, type: "orch_run_completed", timestamp: T + 10, runId: "run-1" })
100
+ expect(m.get("run-1")!.status).toBe("completed")
101
+ })
102
+
103
+ it("transitions run to cancelled on orch_run_cancelled", () => {
104
+ const m = makeMap()
105
+ seedRun(m)
106
+ applyOrchEvent(m, { v: 3, type: "orch_run_cancelled", timestamp: T + 10, runId: "run-1" })
107
+ expect(m.get("run-1")!.status).toBe("cancelled")
108
+ })
109
+
110
+ it("drops illegal run transition (completed → cancelled) without corrupting state", () => {
111
+ const m = makeMap()
112
+ seedRun(m)
113
+ applyOrchEvent(m, { v: 3, type: "orch_run_completed", timestamp: T + 5, runId: "run-1" })
114
+ applyOrchEvent(m, { v: 3, type: "orch_run_cancelled", timestamp: T + 6, runId: "run-1" })
115
+ // status stays completed — illegal transition was dropped
116
+ expect(m.get("run-1")!.status).toBe("completed")
117
+ })
118
+
119
+ it("is a no-op for observability-only events (scope_overlap, config_warning)", () => {
120
+ const m = makeMap()
121
+ seedRun(m)
122
+ const before = m.get("run-1")!.status
123
+ applyOrchEvent(m, {
124
+ v: 3, type: "orch_scope_overlap_flagged", timestamp: T + 1,
125
+ runId: "run-1", taskIds: ["t1"], paths: ["src/"],
126
+ })
127
+ applyOrchEvent(m, {
128
+ v: 3, type: "orch_config_warning", timestamp: T + 2,
129
+ runId: "run-1", message: "warning text",
130
+ })
131
+ expect(m.get("run-1")!.status).toBe(before)
132
+ })
133
+ })
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // applyOrchEvent — task lifecycle
137
+ // ---------------------------------------------------------------------------
138
+
139
+ describe("applyOrchEvent – task lifecycle", () => {
140
+ function claimedMap(): Map<string, OrchRunRecord> {
141
+ const m = makeMap()
142
+ seedRun(m)
143
+ applyOrchEvent(m, {
144
+ v: 3, type: "orch_task_claimed", timestamp: T + 1,
145
+ runId: "run-1", taskId: "t1",
146
+ workerId: "w1", baseSha: "abc", worktreePath: "/wt/0", branch: "orch/run-1/wt-0",
147
+ })
148
+ return m
149
+ }
150
+
151
+ /** Extends claimedMap — task is in `running` state after phase_started. */
152
+ function runningMap(): Map<string, OrchRunRecord> {
153
+ const m = claimedMap()
154
+ applyOrchEvent(m, {
155
+ v: 3, type: "orch_phase_started", timestamp: T + 2,
156
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", workerIds: ["w1"],
157
+ })
158
+ return m
159
+ }
160
+
161
+ it("moves task to claimed state with owner and increments attempts", () => {
162
+ const m = claimedMap()
163
+ const t1 = m.get("run-1")!.tasksById.get("t1")!
164
+ expect(t1.state).toBe("claimed")
165
+ expect(t1.ownerWorkerId).toBe("w1")
166
+ expect(t1.attempts).toBe(1)
167
+ expect(t1.worktreePath).toBe("/wt/0")
168
+ })
169
+
170
+ it("moves task to running on orch_phase_started", () => {
171
+ const m = claimedMap()
172
+ applyOrchEvent(m, {
173
+ v: 3, type: "orch_phase_started", timestamp: T + 2,
174
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", workerIds: ["w1"],
175
+ })
176
+ expect(m.get("run-1")!.tasksById.get("t1")!.state).toBe("running")
177
+ })
178
+
179
+ it("stores last phase output on orch_phase_completed", () => {
180
+ const m = claimedMap()
181
+ applyOrchEvent(m, {
182
+ v: 3, type: "orch_phase_started", timestamp: T + 2,
183
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", workerIds: ["w1"],
184
+ })
185
+ applyOrchEvent(m, {
186
+ v: 3, type: "orch_phase_completed", timestamp: T + 3,
187
+ runId: "run-1", taskId: "t1", phaseIndex: 0,
188
+ output: "phase output text", outputChars: 17, workers: [],
189
+ })
190
+ expect(m.get("run-1")!.tasksById.get("t1")!.lastPhaseOutput).toBe("phase output text")
191
+ })
192
+
193
+ it("moves task to gated on orch_gate_opened (must be running first)", () => {
194
+ const m = runningMap() // claimed → running via phase_started
195
+ applyOrchEvent(m, {
196
+ v: 3, type: "orch_gate_opened", timestamp: T + 3,
197
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", gateKind: "hard",
198
+ })
199
+ expect(m.get("run-1")!.tasksById.get("t1")!.state).toBe("gated")
200
+ })
201
+
202
+ it("moves task back to running on approved gate resolution", () => {
203
+ const m = runningMap()
204
+ applyOrchEvent(m, {
205
+ v: 3, type: "orch_gate_opened", timestamp: T + 3,
206
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", gateKind: "hard",
207
+ })
208
+ applyOrchEvent(m, {
209
+ v: 3, type: "orch_gate_resolved", timestamp: T + 4,
210
+ runId: "run-1", taskId: "t1", phaseIndex: 0, decision: "approve",
211
+ })
212
+ expect(m.get("run-1")!.tasksById.get("t1")!.state).toBe("running")
213
+ })
214
+
215
+ it("keeps task gated on rejected gate resolution", () => {
216
+ const m = runningMap()
217
+ applyOrchEvent(m, {
218
+ v: 3, type: "orch_gate_opened", timestamp: T + 3,
219
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", gateKind: "hard",
220
+ })
221
+ applyOrchEvent(m, {
222
+ v: 3, type: "orch_gate_resolved", timestamp: T + 4,
223
+ runId: "run-1", taskId: "t1", phaseIndex: 0, decision: "reject",
224
+ })
225
+ expect(m.get("run-1")!.tasksById.get("t1")!.state).toBe("gated")
226
+ })
227
+
228
+ it("moves task to committed and clears owner (must be running first)", () => {
229
+ const m = runningMap()
230
+ applyOrchEvent(m, {
231
+ v: 3, type: "orch_task_committed", timestamp: T + 3,
232
+ runId: "run-1", taskId: "t1", commitSha: "sha123",
233
+ })
234
+ const t1 = m.get("run-1")!.tasksById.get("t1")!
235
+ expect(t1.state).toBe("committed")
236
+ expect(t1.ownerWorkerId).toBeNull()
237
+ expect(t1.commitSha).toBe("sha123")
238
+ })
239
+
240
+ it("moves task to failed and clears owner", () => {
241
+ const m = claimedMap()
242
+ applyOrchEvent(m, {
243
+ v: 3, type: "orch_task_failed", timestamp: T + 2,
244
+ runId: "run-1", taskId: "t1", error: "boom",
245
+ })
246
+ const t1 = m.get("run-1")!.tasksById.get("t1")!
247
+ expect(t1.state).toBe("failed")
248
+ expect(t1.ownerWorkerId).toBeNull()
249
+ expect(t1.error).toBe("boom")
250
+ })
251
+
252
+ it("requeues a task back to queued and clears owner", () => {
253
+ const m = claimedMap()
254
+ applyOrchEvent(m, {
255
+ v: 3, type: "orch_task_requeued", timestamp: T + 2,
256
+ runId: "run-1", taskId: "t1", reason: "handed_back", detail: null,
257
+ })
258
+ const t1 = m.get("run-1")!.tasksById.get("t1")!
259
+ expect(t1.state).toBe("queued")
260
+ expect(t1.ownerWorkerId).toBeNull()
261
+ })
262
+ })
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Snapshot projection
266
+ // ---------------------------------------------------------------------------
267
+
268
+ describe("toOrchRunSnapshot", () => {
269
+ it("projects a run record into the correct snapshot shape", () => {
270
+ const m = makeMap()
271
+ seedRun(m)
272
+ const run = m.get("run-1")!
273
+ const snap = toOrchRunSnapshot(run)
274
+ expect(snap.runId).toBe("run-1")
275
+ expect(snap.status).toBe("running")
276
+ expect(snap.tasks).toHaveLength(2)
277
+ expect(snap.tasks[0]?.taskId).toBe("t1")
278
+ expect(snap.tasks[1]?.taskId).toBe("t2")
279
+ })
280
+
281
+ it("snapshot tasks do NOT expose prompt or scopePaths (those are spec-only)", () => {
282
+ const m = makeMap()
283
+ seedRun(m)
284
+ const snap = toOrchRunSnapshot(m.get("run-1")!)
285
+ const t = snap.tasks[0] as unknown as Record<string, unknown>
286
+ expect("prompt" in t).toBe(false)
287
+ expect("scopePaths" in t).toBe(false)
288
+ })
289
+ })
290
+
291
+ describe("getOrchRunSnapshot", () => {
292
+ it("returns null for an unknown runId", () => {
293
+ expect(getOrchRunSnapshot(makeMap(), "unknown")).toBeNull()
294
+ })
295
+
296
+ it("returns the snapshot for a known runId", () => {
297
+ const m = makeMap()
298
+ seedRun(m)
299
+ const snap = getOrchRunSnapshot(m, "run-1")
300
+ expect(snap?.runId).toBe("run-1")
301
+ })
302
+ })
303
+
304
+ describe("getAllOrchRunSnapshots", () => {
305
+ it("returns all runs", () => {
306
+ const m = makeMap()
307
+ seedRun(m, "run-1")
308
+ seedRun(m, "run-2")
309
+ const snaps = getAllOrchRunSnapshots(m)
310
+ expect(snaps).toHaveLength(2)
311
+ expect(snaps.map((s) => s.runId).sort()).toEqual(["run-1", "run-2"])
312
+ })
313
+ })
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Query helpers
317
+ // ---------------------------------------------------------------------------
318
+
319
+ describe("getOrchTaskSpec", () => {
320
+ it("returns prompt and scopePaths for a known task", () => {
321
+ const m = makeMap()
322
+ seedRun(m)
323
+ const spec = getOrchTaskSpec(m, "run-1", "t1")
324
+ expect(spec?.prompt).toBe("do task 1")
325
+ expect(spec?.scopePaths).toEqual(["src/"])
326
+ })
327
+
328
+ it("returns null for an unknown task", () => {
329
+ const m = makeMap()
330
+ seedRun(m)
331
+ expect(getOrchTaskSpec(m, "run-1", "ghost")).toBeNull()
332
+ })
333
+ })
334
+
335
+ describe("getOrchLastPhaseOutput", () => {
336
+ it("returns null before any phase completes", () => {
337
+ const m = makeMap()
338
+ seedRun(m)
339
+ expect(getOrchLastPhaseOutput(m, "run-1", "t1")).toBeNull()
340
+ })
341
+
342
+ it("returns the output after orch_phase_completed", () => {
343
+ const m = makeMap()
344
+ seedRun(m)
345
+ applyOrchEvent(m, {
346
+ v: 3, type: "orch_task_claimed", timestamp: T + 1,
347
+ runId: "run-1", taskId: "t1",
348
+ workerId: "w1", baseSha: "abc", worktreePath: "/wt/0", branch: "orch/run-1/wt-0",
349
+ })
350
+ applyOrchEvent(m, {
351
+ v: 3, type: "orch_phase_started", timestamp: T + 2,
352
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", workerIds: ["w1"],
353
+ })
354
+ applyOrchEvent(m, {
355
+ v: 3, type: "orch_phase_completed", timestamp: T + 3,
356
+ runId: "run-1", taskId: "t1", phaseIndex: 0,
357
+ output: "some output", outputChars: 11, workers: [],
358
+ })
359
+ expect(getOrchLastPhaseOutput(m, "run-1", "t1")).toBe("some output")
360
+ })
361
+ })
362
+
363
+ describe("getOrchRunEvents", () => {
364
+ it("returns a copy of the event log", () => {
365
+ const m = makeMap()
366
+ seedRun(m)
367
+ const events = getOrchRunEvents(m, "run-1")
368
+ expect(events).toHaveLength(1)
369
+ expect(events[0]?.type).toBe("orch_run_created")
370
+ // mutating the returned array must not affect the internal log
371
+ events.push({ v: 3, type: "orch_run_completed", timestamp: T + 999, runId: "run-1" })
372
+ expect(m.get("run-1")!.eventLog).toHaveLength(1)
373
+ })
374
+ })
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Recovery iterators
378
+ // ---------------------------------------------------------------------------
379
+
380
+ describe("nonTerminalOrchTasks", () => {
381
+ it("yields claimed and running tasks from active runs", () => {
382
+ const m = makeMap()
383
+ seedRun(m)
384
+ applyOrchEvent(m, {
385
+ v: 3, type: "orch_task_claimed", timestamp: T + 1,
386
+ runId: "run-1", taskId: "t1",
387
+ workerId: "w1", baseSha: "abc", worktreePath: "/wt/0", branch: "orch/run-1/wt-0",
388
+ })
389
+ applyOrchEvent(m, {
390
+ v: 3, type: "orch_phase_started", timestamp: T + 2,
391
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", workerIds: ["w1"],
392
+ })
393
+ const results = [...nonTerminalOrchTasks(m)]
394
+ expect(results).toHaveLength(1)
395
+ expect(results[0]?.taskId).toBe("t1")
396
+ expect(results[0]?.state).toBe("running")
397
+ })
398
+
399
+ it("excludes gated tasks (they are re-armed, not requeued)", () => {
400
+ const m = makeMap()
401
+ seedRun(m)
402
+ // claimed → running → gated (gate can only open from running state)
403
+ applyOrchEvent(m, {
404
+ v: 3, type: "orch_task_claimed", timestamp: T + 1,
405
+ runId: "run-1", taskId: "t1",
406
+ workerId: "w1", baseSha: "abc", worktreePath: "/wt/0", branch: "orch/run-1/wt-0",
407
+ })
408
+ applyOrchEvent(m, {
409
+ v: 3, type: "orch_phase_started", timestamp: T + 2,
410
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", workerIds: ["w1"],
411
+ })
412
+ applyOrchEvent(m, {
413
+ v: 3, type: "orch_gate_opened", timestamp: T + 3,
414
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", gateKind: "hard",
415
+ })
416
+ const results = [...nonTerminalOrchTasks(m)]
417
+ expect(results).toHaveLength(0)
418
+ })
419
+
420
+ it("excludes tasks from completed runs", () => {
421
+ const m = makeMap()
422
+ seedRun(m)
423
+ applyOrchEvent(m, {
424
+ v: 3, type: "orch_task_claimed", timestamp: T + 1,
425
+ runId: "run-1", taskId: "t1",
426
+ workerId: "w1", baseSha: "abc", worktreePath: "/wt/0", branch: "orch/run-1/wt-0",
427
+ })
428
+ applyOrchEvent(m, { v: 3, type: "orch_run_completed", timestamp: T + 2, runId: "run-1" })
429
+ expect([...nonTerminalOrchTasks(m)]).toHaveLength(0)
430
+ })
431
+ })
432
+
433
+ describe("gatedOrchTasks", () => {
434
+ it("yields gated tasks from active runs", () => {
435
+ const m = makeMap()
436
+ seedRun(m)
437
+ // Must reach running before gate can open
438
+ applyOrchEvent(m, {
439
+ v: 3, type: "orch_task_claimed", timestamp: T + 1,
440
+ runId: "run-1", taskId: "t1",
441
+ workerId: "w1", baseSha: "abc", worktreePath: "/wt/0", branch: "orch/run-1/wt-0",
442
+ })
443
+ applyOrchEvent(m, {
444
+ v: 3, type: "orch_phase_started", timestamp: T + 2,
445
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", workerIds: ["w1"],
446
+ })
447
+ applyOrchEvent(m, {
448
+ v: 3, type: "orch_gate_opened", timestamp: T + 3,
449
+ runId: "run-1", taskId: "t1", phaseIndex: 0, phaseName: "implement", gateKind: "hard",
450
+ })
451
+ const results = [...gatedOrchTasks(m)]
452
+ expect(results).toHaveLength(1)
453
+ expect(results[0]?.taskId).toBe("t1")
454
+ expect(results[0]?.phaseIndex).toBe(0)
455
+ })
456
+
457
+ it("yields nothing when no tasks are gated", () => {
458
+ const m = makeMap()
459
+ seedRun(m)
460
+ expect([...gatedOrchTasks(m)]).toHaveLength(0)
461
+ })
462
+ })