@lsctech/polaris 0.1.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 (316) hide show
  1. package/README.md +87 -0
  2. package/dist/__mocks__/tool-server-linear.js +34 -0
  3. package/dist/cli/adopt-approve.js +60 -0
  4. package/dist/cli/adopt-cognition.js +199 -0
  5. package/dist/cli/adopt-instructions.js +143 -0
  6. package/dist/cli/adopt-rules.js +169 -0
  7. package/dist/cli/adopt-scan.js +25 -0
  8. package/dist/cli/adopt-smartdocs.js +192 -0
  9. package/dist/cli/adoption-inventory.js +350 -0
  10. package/dist/cli/adoption-plan.js +222 -0
  11. package/dist/cli/args.js +28 -0
  12. package/dist/cli/args.test.js +30 -0
  13. package/dist/cli/branding.js +41 -0
  14. package/dist/cli/graph.js +358 -0
  15. package/dist/cli/index.js +127 -0
  16. package/dist/cli/index.test.js +162 -0
  17. package/dist/cli/init-detect.js +61 -0
  18. package/dist/cli/init.js +492 -0
  19. package/dist/cli/init.test.js +259 -0
  20. package/dist/cli/librarian.js +32 -0
  21. package/dist/cli/medic.js +96 -0
  22. package/dist/cli/startup.test.js +23 -0
  23. package/dist/cli/tracker.js +130 -0
  24. package/dist/cli/tracker.test.js +126 -0
  25. package/dist/cli/version.js +10 -0
  26. package/dist/cli/version.test.js +11 -0
  27. package/dist/cli/worker.js +287 -0
  28. package/dist/cluster-state/fixtures/test-clusters.json +19 -0
  29. package/dist/cluster-state/store.js +302 -0
  30. package/dist/cluster-state/store.test.js +145 -0
  31. package/dist/cluster-state/types.js +2 -0
  32. package/dist/cognition/archive.js +158 -0
  33. package/dist/cognition/closeout-librarian-types.js +161 -0
  34. package/dist/cognition/cognition.test.js +623 -0
  35. package/dist/cognition/index.js +41 -0
  36. package/dist/cognition/librarian-dispatch.js +391 -0
  37. package/dist/cognition/librarian-dispatch.test.js +247 -0
  38. package/dist/cognition/librarian-packet.js +313 -0
  39. package/dist/cognition/librarian-types.js +57 -0
  40. package/dist/cognition/route-cognition-delta.js +350 -0
  41. package/dist/cognition/summary-delta.js +279 -0
  42. package/dist/cognition/validate.js +162 -0
  43. package/dist/config/defaults.js +98 -0
  44. package/dist/config/doctor.js +273 -0
  45. package/dist/config/index.js +14 -0
  46. package/dist/config/loader.js +58 -0
  47. package/dist/config/provider-detect.js +60 -0
  48. package/dist/config/provider-detect.test.js +119 -0
  49. package/dist/config/schema.js +2 -0
  50. package/dist/config/show.js +44 -0
  51. package/dist/config/show.test.js +42 -0
  52. package/dist/config/validator.js +675 -0
  53. package/dist/config/validator.test.js +77 -0
  54. package/dist/docs/canon-check.js +334 -0
  55. package/dist/docs/canon-check.test.js +207 -0
  56. package/dist/docs/doctrine.js +133 -0
  57. package/dist/docs/doctrine.test.js +139 -0
  58. package/dist/docs/index.js +168 -0
  59. package/dist/docs/index.test.js +83 -0
  60. package/dist/docs/ingest.js +295 -0
  61. package/dist/docs/ingest.test.js +71 -0
  62. package/dist/docs/migrate.js +165 -0
  63. package/dist/docs/migrate.test.js +136 -0
  64. package/dist/docs/seed-instructions.js +147 -0
  65. package/dist/docs/seed-instructions.test.js +137 -0
  66. package/dist/docs/validate-instructions.js +267 -0
  67. package/dist/docs/validate-instructions.test.js +238 -0
  68. package/dist/finalize/artifact-policy.js +214 -0
  69. package/dist/finalize/artifact-policy.test.js +84 -0
  70. package/dist/finalize/delivery-integrity.js +141 -0
  71. package/dist/finalize/finalize.test.js +210 -0
  72. package/dist/finalize/github.js +41 -0
  73. package/dist/finalize/index.js +474 -0
  74. package/dist/finalize/linear.js +213 -0
  75. package/dist/finalize/run-report.js +47 -0
  76. package/dist/finalize/steps/01-map-update.js +11 -0
  77. package/dist/finalize/steps/02-map-validate.js +11 -0
  78. package/dist/finalize/steps/03-schema-validate.js +11 -0
  79. package/dist/finalize/steps/04-run-checks.js +118 -0
  80. package/dist/finalize/steps/05-generate-report.js +14 -0
  81. package/dist/finalize/steps/06-commit.js +39 -0
  82. package/dist/finalize/steps/07-push.js +10 -0
  83. package/dist/finalize/steps/08-create-pr.js +9 -0
  84. package/dist/finalize/steps/09-update-state.js +9 -0
  85. package/dist/finalize/steps/10-append-jsonl.js +23 -0
  86. package/dist/finalize/steps/11-update-linear.js +22 -0
  87. package/dist/finalize/steps/12-archive.js +21 -0
  88. package/dist/graph/adapter/c/adapter.js +45 -0
  89. package/dist/graph/adapter/c/extract.js +135 -0
  90. package/dist/graph/adapter/c/index.js +10 -0
  91. package/dist/graph/adapter/c/runtime.js +67 -0
  92. package/dist/graph/adapter/cpp/adapter.js +45 -0
  93. package/dist/graph/adapter/cpp/extract.js +159 -0
  94. package/dist/graph/adapter/cpp/index.js +10 -0
  95. package/dist/graph/adapter/cpp/runtime.js +61 -0
  96. package/dist/graph/adapter/csharp/adapter.js +45 -0
  97. package/dist/graph/adapter/csharp/extract.js +127 -0
  98. package/dist/graph/adapter/csharp/index.js +10 -0
  99. package/dist/graph/adapter/csharp/runtime.js +61 -0
  100. package/dist/graph/adapter/dart/adapter.js +56 -0
  101. package/dist/graph/adapter/dart/extract.js +130 -0
  102. package/dist/graph/adapter/dart/index.js +10 -0
  103. package/dist/graph/adapter/dart/runtime.js +61 -0
  104. package/dist/graph/adapter/go/adapter.js +45 -0
  105. package/dist/graph/adapter/go/extract.js +112 -0
  106. package/dist/graph/adapter/go/index.js +10 -0
  107. package/dist/graph/adapter/go/runtime.js +58 -0
  108. package/dist/graph/adapter/kotlin-java/extract-common.js +83 -0
  109. package/dist/graph/adapter/kotlin-java/index.js +15 -0
  110. package/dist/graph/adapter/kotlin-java/java-adapter.js +45 -0
  111. package/dist/graph/adapter/kotlin-java/java-extract.js +42 -0
  112. package/dist/graph/adapter/kotlin-java/kotlin-adapter.js +48 -0
  113. package/dist/graph/adapter/kotlin-java/kotlin-extract.js +58 -0
  114. package/dist/graph/adapter/kotlin-java/runtime.js +75 -0
  115. package/dist/graph/adapter/python/adapter.js +45 -0
  116. package/dist/graph/adapter/python/extract.js +121 -0
  117. package/dist/graph/adapter/python/index.js +10 -0
  118. package/dist/graph/adapter/python/runtime.js +70 -0
  119. package/dist/graph/adapter/registry.js +82 -0
  120. package/dist/graph/adapter/rust/adapter.js +45 -0
  121. package/dist/graph/adapter/rust/extract.js +110 -0
  122. package/dist/graph/adapter/rust/index.js +10 -0
  123. package/dist/graph/adapter/rust/runtime.js +74 -0
  124. package/dist/graph/adapter/shell/adapter.js +34 -0
  125. package/dist/graph/adapter/shell/extract.js +98 -0
  126. package/dist/graph/adapter/shell/index.js +8 -0
  127. package/dist/graph/adapter/svelte/adapter.js +91 -0
  128. package/dist/graph/adapter/svelte/extract.js +41 -0
  129. package/dist/graph/adapter/svelte/index.js +8 -0
  130. package/dist/graph/adapter/swift/adapter.js +45 -0
  131. package/dist/graph/adapter/swift/extract.js +120 -0
  132. package/dist/graph/adapter/swift/index.js +10 -0
  133. package/dist/graph/adapter/swift/runtime.js +70 -0
  134. package/dist/graph/adapter/types.js +2 -0
  135. package/dist/graph/adapter/typescript-javascript/adapter.js +72 -0
  136. package/dist/graph/adapter/typescript-javascript/extract.js +115 -0
  137. package/dist/graph/adapter/typescript-javascript/index.js +10 -0
  138. package/dist/graph/adapter/typescript-javascript/runtime.js +97 -0
  139. package/dist/graph/capability/index.js +105 -0
  140. package/dist/graph/governance.js +104 -0
  141. package/dist/graph/parser/extract.js +5 -0
  142. package/dist/graph/parser/loader.js +2 -0
  143. package/dist/graph/parser/pipeline.js +194 -0
  144. package/dist/graph/query/index.js +277 -0
  145. package/dist/graph/query/types.js +5 -0
  146. package/dist/graph/resolver/build-edges.js +211 -0
  147. package/dist/graph/resolver/index.js +23 -0
  148. package/dist/graph/resolver/resolve-imports.js +168 -0
  149. package/dist/graph/store/adapter.js +132 -0
  150. package/dist/graph/store/queries.js +124 -0
  151. package/dist/graph/store/types.js +4 -0
  152. package/dist/ignore/defaults.js +41 -0
  153. package/dist/ignore/parser.js +39 -0
  154. package/dist/lint/map-reference-validator.js +72 -0
  155. package/dist/loop/abort.js +188 -0
  156. package/dist/loop/abort.test.js +151 -0
  157. package/dist/loop/adapters/__tests__/terminal-cli.test.js +92 -0
  158. package/dist/loop/adapters/agent-subtask.js +137 -0
  159. package/dist/loop/adapters/agent-subtask.test.js +151 -0
  160. package/dist/loop/adapters/cli-subtask-bridge.js +108 -0
  161. package/dist/loop/adapters/index.js +9 -0
  162. package/dist/loop/adapters/registry.js +17 -0
  163. package/dist/loop/adapters/terminal-cli.js +403 -0
  164. package/dist/loop/adapters/types.js +2 -0
  165. package/dist/loop/adapters/worker-instructions.js +85 -0
  166. package/dist/loop/body-parser.js +226 -0
  167. package/dist/loop/bootstrap-packet.js +97 -0
  168. package/dist/loop/bootstrap-packet.test.js +171 -0
  169. package/dist/loop/branch-governance.js +54 -0
  170. package/dist/loop/budget.js +63 -0
  171. package/dist/loop/budget.test.js +163 -0
  172. package/dist/loop/checkpoint.js +325 -0
  173. package/dist/loop/compact-return.js +59 -0
  174. package/dist/loop/continue.js +645 -0
  175. package/dist/loop/continue.test.js +337 -0
  176. package/dist/loop/dispatch-boundary.js +335 -0
  177. package/dist/loop/dispatch-state.js +223 -0
  178. package/dist/loop/dispatch-state.test.js +186 -0
  179. package/dist/loop/dispatch.js +1236 -0
  180. package/dist/loop/dispatch.test.js +499 -0
  181. package/dist/loop/ephemeral-smoke.test.js +160 -0
  182. package/dist/loop/evidence-backfill.js +216 -0
  183. package/dist/loop/execution-adapter.js +141 -0
  184. package/dist/loop/execution-adapter.test.js +66 -0
  185. package/dist/loop/finalize-evidence.js +169 -0
  186. package/dist/loop/git-custody.js +250 -0
  187. package/dist/loop/impl-packet-input.js +38 -0
  188. package/dist/loop/index.js +270 -0
  189. package/dist/loop/ledger.js +49 -0
  190. package/dist/loop/ledger.test.js +127 -0
  191. package/dist/loop/lifecycle-dispatch.js +348 -0
  192. package/dist/loop/lifecycle-dispatch.test.js +318 -0
  193. package/dist/loop/lifecycle.js +93 -0
  194. package/dist/loop/lifecycle.test.js +150 -0
  195. package/dist/loop/orphan-recovery.js +276 -0
  196. package/dist/loop/orphan-recovery.test.js +236 -0
  197. package/dist/loop/parent.js +1566 -0
  198. package/dist/loop/parent.test.js +738 -0
  199. package/dist/loop/provider-sealed-packet.test.js +164 -0
  200. package/dist/loop/resume.js +295 -0
  201. package/dist/loop/resume.test.js +172 -0
  202. package/dist/loop/run-bootstrap.js +234 -0
  203. package/dist/loop/run-preflight.js +203 -0
  204. package/dist/loop/status.js +665 -0
  205. package/dist/loop/status.test.js +235 -0
  206. package/dist/loop/wait.js +86 -0
  207. package/dist/loop/worker-packet.js +406 -0
  208. package/dist/loop/worker-packet.test.js +243 -0
  209. package/dist/loop/worker-prompt.js +260 -0
  210. package/dist/loop/worker-prompt.test.js +247 -0
  211. package/dist/loop/worker.js +498 -0
  212. package/dist/loop/worker.test.js +372 -0
  213. package/dist/map/atlas.js +77 -0
  214. package/dist/map/backfill.js +171 -0
  215. package/dist/map/backfill.test.js +120 -0
  216. package/dist/map/index.js +255 -0
  217. package/dist/map/inference.js +73 -0
  218. package/dist/map/inference.test.js +35 -0
  219. package/dist/map/query.js +157 -0
  220. package/dist/map/query.test.js +174 -0
  221. package/dist/map/update.js +355 -0
  222. package/dist/map/update.test.js +67 -0
  223. package/dist/map/validate.js +153 -0
  224. package/dist/map/validate.test.js +90 -0
  225. package/dist/map/welfare.js +86 -0
  226. package/dist/mcp/lib/invoke.js +35 -0
  227. package/dist/mcp/lib/redact.js +24 -0
  228. package/dist/mcp/lib/redact.test.js +112 -0
  229. package/dist/mcp/lib/root.js +41 -0
  230. package/dist/mcp/lib/root.test.js +114 -0
  231. package/dist/mcp/server.js +15 -0
  232. package/dist/mcp/tools/claim-child.js +119 -0
  233. package/dist/mcp/tools/claim-child.test.js +69 -0
  234. package/dist/mcp/tools/current-state.js +73 -0
  235. package/dist/mcp/tools/current-state.test.js +114 -0
  236. package/dist/mcp/tools/dispatch-result.js +186 -0
  237. package/dist/mcp/tools/dispatch-result.test.js +109 -0
  238. package/dist/mcp/tools/index.js +222 -0
  239. package/dist/mcp/tools/index.test.js +172 -0
  240. package/dist/mcp/tools/linear.js +74 -0
  241. package/dist/mcp/tools/loop-continue.js +159 -0
  242. package/dist/mcp/tools/loop-continue.test.js +186 -0
  243. package/dist/mcp/tools/loop-dry-run.js +15 -0
  244. package/dist/mcp/tools/status.js +65 -0
  245. package/dist/medic/chart-id.js +75 -0
  246. package/dist/medic/chart-schema.js +139 -0
  247. package/dist/runs/index.js +119 -0
  248. package/dist/runs/index.test.js +184 -0
  249. package/dist/runs/ledger-tail.js +15 -0
  250. package/dist/runs/list.js +29 -0
  251. package/dist/runs/reconcile.js +74 -0
  252. package/dist/runs/show.js +10 -0
  253. package/dist/runtime/audit/logger.js +15 -0
  254. package/dist/runtime/audit/reader.js +42 -0
  255. package/dist/runtime/checkpoint.js +89 -0
  256. package/dist/runtime/continuation/confirmed.js +225 -0
  257. package/dist/runtime/continuation/confirmed.test.js +421 -0
  258. package/dist/runtime/continuation/dry-run.js +74 -0
  259. package/dist/runtime/execution-window.js +106 -0
  260. package/dist/runtime/scheduling/child-selector.js +16 -0
  261. package/dist/runtime/state.js +43 -0
  262. package/dist/runtime/status.js +46 -0
  263. package/dist/runtime/verification/envelope.js +80 -0
  264. package/dist/runtime/verification/fingerprint.js +26 -0
  265. package/dist/skill-packet/cli.test.js +105 -0
  266. package/dist/skill-packet/generator.js +225 -0
  267. package/dist/skill-packet/generator.test.js +159 -0
  268. package/dist/skill-packet/index.js +49 -0
  269. package/dist/skill-packet/types.js +2 -0
  270. package/dist/smartdocs-engine/audit.js +194 -0
  271. package/dist/smartdocs-engine/audit.test.js +122 -0
  272. package/dist/smartdocs-engine/canon-check.js +340 -0
  273. package/dist/smartdocs-engine/canon-check.test.js +234 -0
  274. package/dist/smartdocs-engine/doctrine.js +633 -0
  275. package/dist/smartdocs-engine/doctrine.test.js +374 -0
  276. package/dist/smartdocs-engine/index.js +456 -0
  277. package/dist/smartdocs-engine/index.test.js +82 -0
  278. package/dist/smartdocs-engine/ingest.js +495 -0
  279. package/dist/smartdocs-engine/ingest.test.js +163 -0
  280. package/dist/smartdocs-engine/migrate.js +217 -0
  281. package/dist/smartdocs-engine/migrate.test.js +200 -0
  282. package/dist/smartdocs-engine/seed-instructions.js +335 -0
  283. package/dist/smartdocs-engine/seed-instructions.test.js +275 -0
  284. package/dist/smartdocs-engine/smartdoc-ignore.js +258 -0
  285. package/dist/smartdocs-engine/smartdoc-ignore.test.js +146 -0
  286. package/dist/smartdocs-engine/validate-instructions.js +366 -0
  287. package/dist/smartdocs-engine/validate-instructions.test.js +283 -0
  288. package/dist/tracker/adapters/github/index.js +305 -0
  289. package/dist/tracker/adapters/jira/index.js +293 -0
  290. package/dist/tracker/adapters/linear/http-errors.test.js +51 -0
  291. package/dist/tracker/adapters/linear/index.js +621 -0
  292. package/dist/tracker/adapters/linear/index.test.js +238 -0
  293. package/dist/tracker/adapters/linear.js +50 -0
  294. package/dist/tracker/adapters/local/index.js +10 -0
  295. package/dist/tracker/adapters/local-file/index.js +159 -0
  296. package/dist/tracker/adapters/mcp-bridge.js +158 -0
  297. package/dist/tracker/adapters/mcp-bridge.test.js +12 -0
  298. package/dist/tracker/adapters/spec/index.js +146 -0
  299. package/dist/tracker/capabilities.js +2 -0
  300. package/dist/tracker/index.js +49 -0
  301. package/dist/tracker/lifecycle-policy.js +139 -0
  302. package/dist/tracker/lifecycle-transition.js +164 -0
  303. package/dist/tracker/local-graph.js +98 -0
  304. package/dist/tracker/migration.js +56 -0
  305. package/dist/tracker/schema.js +86 -0
  306. package/dist/tracker/sync/index.js +404 -0
  307. package/dist/tracker/sync/queue-store.js +95 -0
  308. package/dist/tracker/sync/queue-store.test.js +104 -0
  309. package/dist/tracker/types.js +11 -0
  310. package/dist/types/linear.js +3 -0
  311. package/dist/types/result-packet.js +12 -0
  312. package/dist/types/runtime-state.js +2 -0
  313. package/dist/types/work-contract.js +2 -0
  314. package/dist/utils/monotonic-timestamp.js +13 -0
  315. package/dist/utils/monotonic-timestamp.test.js +19 -0
  316. package/package.json +64 -0
@@ -0,0 +1,738 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for the scheduler-only parent loop (src/loop/parent.ts).
4
+ *
5
+ * Uses a mock adapter that returns valid compact JSON; verifies that:
6
+ * - The parent loops N times then halts at budget exhaustion.
7
+ * - Each iteration dispatches exactly one child (no inline child work).
8
+ * - ADAPTER HANDOFF semantics: dispatch + continue, not halt.
9
+ * - Blocked children halt immediately with a clear blocker description.
10
+ * - Cluster-complete halts cleanly when all children are done.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const vitest_1 = require("vitest");
14
+ const node_fs_1 = require("node:fs");
15
+ const node_path_1 = require("node:path");
16
+ const node_os_1 = require("node:os");
17
+ const parent_js_1 = require("./parent.js");
18
+ const index_js_1 = require("./index.js");
19
+ const run_bootstrap_js_1 = require("./run-bootstrap.js");
20
+ function makeMockAdapter(responses, calls = []) {
21
+ let callIndex = 0;
22
+ return {
23
+ name: "mock",
24
+ async dispatch(packet, options) {
25
+ calls.push({ packet, options });
26
+ const base = responses[callIndex] ?? responses[responses.length - 1];
27
+ callIndex += 1;
28
+ // Rewrite child_id in the summary to match the dispatched child so the
29
+ // parent loop's mismatch guard doesn't reject a valid mock result.
30
+ try {
31
+ const parsed = JSON.parse(base.summary ?? "{}");
32
+ if ("child_id" in parsed) {
33
+ return { ...base, summary: JSON.stringify({ ...parsed, child_id: packet.active_child }) };
34
+ }
35
+ }
36
+ catch {
37
+ // Summary is not JSON — leave as-is
38
+ }
39
+ return base;
40
+ },
41
+ };
42
+ }
43
+ // ── Test fixtures ────────────────────────────────────────────────────────────
44
+ const SUCCESS_RESULT = {
45
+ exit_code: 0,
46
+ provider_used: "mock",
47
+ command_run: "mock-worker",
48
+ summary: JSON.stringify({ child_id: "POL-99", status: "done", commit: "abc1234" }),
49
+ };
50
+ const BLOCKED_RESULT = {
51
+ exit_code: 0,
52
+ provider_used: "mock",
53
+ command_run: "mock-worker",
54
+ summary: JSON.stringify({
55
+ child_id: "POL-99",
56
+ status: "blocked",
57
+ blocker: "Waiting for POL-98 to merge",
58
+ }),
59
+ };
60
+ const ERROR_RESULT = {
61
+ exit_code: 1,
62
+ provider_used: "mock",
63
+ command_run: "mock-worker",
64
+ summary: "Worker process exited with code 1",
65
+ };
66
+ const SUCCESS_RESULT_NO_COMMIT = {
67
+ exit_code: 0,
68
+ provider_used: "mock",
69
+ command_run: "mock-worker",
70
+ summary: JSON.stringify({ child_id: "POL-99", status: "done" }),
71
+ };
72
+ // ── Test helpers ─────────────────────────────────────────────────────────────
73
+ function makeStateFile(dir, overrides = {}) {
74
+ const stateFile = (0, node_path_1.join)(dir, "current-state.json");
75
+ const openChildren = overrides.open_children ?? ["POL-100", "POL-101", "POL-102"];
76
+ const state = {
77
+ schema_version: "1.0",
78
+ run_id: "test-run-001",
79
+ cluster_id: "POL-99",
80
+ skill: "polaris-run",
81
+ artifact_dir: dir,
82
+ branch: "feature/test",
83
+ current_step_id: "03-execute-child",
84
+ step_cursor: "dispatching",
85
+ status: "executing",
86
+ session_type: "implementation",
87
+ active_child: "",
88
+ last_commit: "",
89
+ next_open_child: openChildren[0] ?? null,
90
+ completed_children: overrides.completed_children ?? [],
91
+ open_children: openChildren,
92
+ open_children_meta: {},
93
+ context_budget: {
94
+ children_completed: overrides.children_completed ?? 0,
95
+ files_touched_total: 0,
96
+ max_children_per_session: overrides.max_children_per_session ?? 3,
97
+ },
98
+ dispatch_boundary: { dispatch_epoch: 0, continue_epoch: 0, last_dispatched_child: null },
99
+ run_bootstrap_seal: (0, run_bootstrap_js_1.createBootstrapSeal)("test-run-001", "POL-99", openChildren),
100
+ updated_at: new Date().toISOString(),
101
+ };
102
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify(state, null, 2), "utf-8");
103
+ return stateFile;
104
+ }
105
+ function makeStateFileWithMeta(dir, openChildren, meta, maxChildren = 10) {
106
+ const stateFile = (0, node_path_1.join)(dir, "current-state.json");
107
+ const state = {
108
+ schema_version: "1.0",
109
+ run_id: "test-run-001",
110
+ cluster_id: "POL-99",
111
+ skill: "polaris-run",
112
+ artifact_dir: dir,
113
+ branch: "feature/test",
114
+ current_step_id: "03-execute-child",
115
+ step_cursor: "dispatching",
116
+ status: "executing",
117
+ session_type: "implementation",
118
+ active_child: "",
119
+ last_commit: "",
120
+ next_open_child: openChildren[0] ?? null,
121
+ completed_children: [],
122
+ open_children: openChildren,
123
+ open_children_meta: meta,
124
+ context_budget: {
125
+ children_completed: 0,
126
+ files_touched_total: 0,
127
+ max_children_per_session: maxChildren,
128
+ },
129
+ dispatch_boundary: { dispatch_epoch: 0, continue_epoch: 0, last_dispatched_child: null },
130
+ run_bootstrap_seal: (0, run_bootstrap_js_1.createBootstrapSeal)("test-run-001", "POL-99", openChildren),
131
+ updated_at: new Date().toISOString(),
132
+ };
133
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify(state, null, 2), "utf-8");
134
+ return stateFile;
135
+ }
136
+ function readJsonLines(path) {
137
+ return (0, node_fs_1.readFileSync)(path, "utf-8")
138
+ .trim()
139
+ .split("\n")
140
+ .filter(Boolean)
141
+ .map((line) => JSON.parse(line));
142
+ }
143
+ function configureCommandForTest(command, output) {
144
+ command.exitOverride();
145
+ command.configureOutput({
146
+ writeOut: (value) => {
147
+ output.stdout += value;
148
+ },
149
+ writeErr: (value) => {
150
+ output.stderr += value;
151
+ },
152
+ });
153
+ for (const child of command.commands) {
154
+ configureCommandForTest(child, output);
155
+ }
156
+ }
157
+ async function runLoopCommand(command, argv) {
158
+ const output = { stdout: "", stderr: "" };
159
+ let exitCode = 0;
160
+ const originalExit = process.exit;
161
+ const originalStdoutWrite = process.stdout.write;
162
+ const originalStderrWrite = process.stderr.write;
163
+ configureCommandForTest(command, output);
164
+ process.stdout.write = ((chunk) => {
165
+ output.stdout += String(chunk);
166
+ return true;
167
+ });
168
+ process.stderr.write = ((chunk) => {
169
+ output.stderr += String(chunk);
170
+ return true;
171
+ });
172
+ process.exit = ((code) => {
173
+ exitCode = typeof code === "number" ? code : 0;
174
+ throw new Error("process.exit called");
175
+ });
176
+ try {
177
+ await command.parseAsync(["node", "polaris", ...argv], { from: "node" });
178
+ }
179
+ catch (error) {
180
+ if (!(error instanceof Error && "exitCode" in error) &&
181
+ !(error instanceof Error && error.message === "process.exit called")) {
182
+ throw error;
183
+ }
184
+ if (error instanceof Error && "exitCode" in error) {
185
+ exitCode = Number(error.exitCode);
186
+ }
187
+ }
188
+ finally {
189
+ process.exit = originalExit;
190
+ process.stdout.write = originalStdoutWrite;
191
+ process.stderr.write = originalStderrWrite;
192
+ }
193
+ return { ...output, exitCode };
194
+ }
195
+ // ── Mock registry so tests can inject a mock adapter ────────────────────────
196
+ vitest_1.vi.mock("./adapters/registry.js", () => ({
197
+ createAdapter: vitest_1.vi.fn(),
198
+ }));
199
+ vitest_1.vi.mock("../config/loader.js", () => ({
200
+ loadConfig: vitest_1.vi.fn(() => ({
201
+ orchestration: {
202
+ mode: "auto",
203
+ notification_format: "verbose",
204
+ auto_finalize: false,
205
+ },
206
+ execution: {
207
+ adapter: "mock",
208
+ providers: { mock: { command: "mock-worker" } },
209
+ rotation: ["mock"],
210
+ },
211
+ })),
212
+ }));
213
+ const registry_js_1 = require("./adapters/registry.js");
214
+ // ── Tests ────────────────────────────────────────────────────────────────────
215
+ (0, vitest_1.describe)("runParentLoop", () => {
216
+ let tmpDir;
217
+ (0, vitest_1.beforeEach)(() => {
218
+ tmpDir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `polaris-parent-test-${Date.now()}`);
219
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(tmpDir, "runs", "test-run-001"), { recursive: true });
220
+ vitest_1.vi.clearAllMocks();
221
+ });
222
+ (0, vitest_1.afterEach)(() => {
223
+ try {
224
+ (0, node_fs_1.rmSync)(tmpDir, { recursive: true, force: true });
225
+ }
226
+ catch {
227
+ // ignore cleanup errors
228
+ }
229
+ });
230
+ (0, vitest_1.it)("loops N times then halts at budget exhaustion", async () => {
231
+ // Budget: 3 max children, 0 completed, 3 open children
232
+ const calls = [];
233
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT, SUCCESS_RESULT, SUCCESS_RESULT], calls);
234
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
235
+ const stateFile = makeStateFile(tmpDir, {
236
+ open_children: ["POL-100", "POL-101", "POL-102"],
237
+ children_completed: 0,
238
+ max_children_per_session: 2, // budget of 2, 3 children → exhaust after 2
239
+ });
240
+ const result = await (0, parent_js_1.runParentLoop)({
241
+ stateFile,
242
+ repoRoot: tmpDir,
243
+ });
244
+ (0, vitest_1.expect)(result.haltReason).toBe("budget-exhausted");
245
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(2);
246
+ // Adapter dispatched exactly 2 children before budget was exhausted
247
+ (0, vitest_1.expect)(calls).toHaveLength(2);
248
+ (0, vitest_1.expect)(calls[0].packet.active_child).toBe("POL-100");
249
+ (0, vitest_1.expect)(calls[1].packet.active_child).toBe("POL-101");
250
+ });
251
+ (0, vitest_1.it)("dispatches each child exactly once (no inline child work)", async () => {
252
+ const calls = [];
253
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT, SUCCESS_RESULT, SUCCESS_RESULT], calls);
254
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
255
+ const stateFile = makeStateFile(tmpDir, {
256
+ open_children: ["POL-100", "POL-101", "POL-102"],
257
+ children_completed: 0,
258
+ max_children_per_session: 10,
259
+ });
260
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
261
+ // All 3 children done, no budget exhaustion
262
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
263
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(3);
264
+ // Each child dispatched exactly once
265
+ const dispatchedIds = calls.map((c) => c.packet.active_child);
266
+ (0, vitest_1.expect)(dispatchedIds).toEqual(["POL-100", "POL-101", "POL-102"]);
267
+ });
268
+ (0, vitest_1.it)("appends run-started to the global ledger on entry", async () => {
269
+ const calls = [];
270
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT], calls);
271
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
272
+ const stateFile = makeStateFile(tmpDir, {
273
+ open_children: ["POL-100"],
274
+ children_completed: 0,
275
+ max_children_per_session: 10,
276
+ });
277
+ await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
278
+ const events = (0, node_fs_1.readFileSync)((0, node_path_1.join)(tmpDir, ".polaris", "runs", "ledger.jsonl"), "utf-8")
279
+ .trim()
280
+ .split("\n")
281
+ .map((line) => JSON.parse(line));
282
+ (0, vitest_1.expect)(events[0]).toMatchObject({
283
+ event: "run-started",
284
+ run_id: "test-run-001",
285
+ run_type: "implement",
286
+ cluster_id: "POL-99",
287
+ issue_id: null,
288
+ branch: "feature/test",
289
+ status: "running",
290
+ completed_children: [],
291
+ open_children: ["POL-100"],
292
+ next_child: "POL-100",
293
+ last_commit: null,
294
+ pr_url: null,
295
+ });
296
+ });
297
+ (0, vitest_1.it)("ADAPTER HANDOFF: dispatch + continue, not halt", async () => {
298
+ // Verify that after each dispatch the loop continues to the next child
299
+ const calls = [];
300
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT, SUCCESS_RESULT], calls);
301
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
302
+ const stateFile = makeStateFile(tmpDir, {
303
+ open_children: ["POL-100", "POL-101"],
304
+ children_completed: 0,
305
+ max_children_per_session: 10,
306
+ });
307
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
308
+ // The loop continued after the first dispatch (ADAPTER HANDOFF = continue)
309
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
310
+ (0, vitest_1.expect)(calls).toHaveLength(2);
311
+ });
312
+ (0, vitest_1.it)("halts after one successful child in supervised mode", async () => {
313
+ const calls = [];
314
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(makeMockAdapter([SUCCESS_RESULT, SUCCESS_RESULT], calls));
315
+ const { loadConfig } = await import("../config/loader.js");
316
+ vitest_1.vi.mocked(loadConfig).mockReturnValueOnce({
317
+ orchestration: {
318
+ mode: "supervised",
319
+ notification_format: "verbose",
320
+ auto_finalize: false,
321
+ },
322
+ execution: {
323
+ adapter: "mock",
324
+ providers: { mock: { command: "mock-worker" } },
325
+ rotation: ["mock"],
326
+ },
327
+ });
328
+ const stateFile = makeStateFile(tmpDir, {
329
+ open_children: ["POL-100", "POL-101"],
330
+ children_completed: 0,
331
+ max_children_per_session: 10,
332
+ });
333
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
334
+ (0, vitest_1.expect)(result.haltReason).toBe("supervised-mode-child-complete");
335
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(1);
336
+ (0, vitest_1.expect)(calls).toHaveLength(1);
337
+ });
338
+ (0, vitest_1.it)("continues through eligible children in auto mode", async () => {
339
+ const calls = [];
340
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(makeMockAdapter([SUCCESS_RESULT, SUCCESS_RESULT], calls));
341
+ const { loadConfig } = await import("../config/loader.js");
342
+ vitest_1.vi.mocked(loadConfig).mockReturnValueOnce({
343
+ orchestration: {
344
+ mode: "auto",
345
+ notification_format: "verbose",
346
+ auto_finalize: false,
347
+ },
348
+ execution: {
349
+ adapter: "mock",
350
+ providers: { mock: { command: "mock-worker" } },
351
+ rotation: ["mock"],
352
+ },
353
+ });
354
+ const stateFile = makeStateFile(tmpDir, {
355
+ open_children: ["POL-100", "POL-101"],
356
+ children_completed: 0,
357
+ max_children_per_session: 10,
358
+ });
359
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
360
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
361
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(2);
362
+ (0, vitest_1.expect)(calls).toHaveLength(2);
363
+ });
364
+ (0, vitest_1.it)("halts immediately when a child reports blocked", async () => {
365
+ const calls = [];
366
+ const mockAdapter = makeMockAdapter([BLOCKED_RESULT], calls);
367
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
368
+ const stateFile = makeStateFile(tmpDir, {
369
+ open_children: ["POL-100", "POL-101"],
370
+ children_completed: 0,
371
+ max_children_per_session: 10,
372
+ });
373
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
374
+ (0, vitest_1.expect)(result.haltReason).toBe("blocked");
375
+ (0, vitest_1.expect)(result.haltingChild).toBe("POL-100");
376
+ (0, vitest_1.expect)(result.message).toContain("POL-98 to merge");
377
+ // Only one dispatch attempt before halt
378
+ (0, vitest_1.expect)(calls).toHaveLength(1);
379
+ // POL-101 never dispatched
380
+ (0, vitest_1.expect)(calls[0].packet.active_child).toBe("POL-100");
381
+ });
382
+ (0, vitest_1.it)("halts on worker error with non-zero exit code", async () => {
383
+ const calls = [];
384
+ const mockAdapter = makeMockAdapter([ERROR_RESULT], calls);
385
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
386
+ const stateFile = makeStateFile(tmpDir, {
387
+ open_children: ["POL-100"],
388
+ children_completed: 0,
389
+ max_children_per_session: 10,
390
+ });
391
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
392
+ (0, vitest_1.expect)(result.haltReason).toBe("worker-error");
393
+ (0, vitest_1.expect)(result.haltingChild).toBe("POL-100");
394
+ });
395
+ (0, vitest_1.it)("halts cleanly with cluster-complete when all children are done", async () => {
396
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT]);
397
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
398
+ const stateFile = makeStateFile(tmpDir, {
399
+ open_children: ["POL-100"],
400
+ children_completed: 0,
401
+ max_children_per_session: 10,
402
+ });
403
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
404
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
405
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(1);
406
+ (0, vitest_1.expect)(result.message).toContain("Cluster complete");
407
+ });
408
+ (0, vitest_1.it)("does not double-count when worker already persisted child completion", async () => {
409
+ const stateFile = makeStateFile(tmpDir, {
410
+ open_children: ["POL-100"],
411
+ children_completed: 0,
412
+ max_children_per_session: 10,
413
+ });
414
+ const mockAdapter = {
415
+ name: "mock",
416
+ async dispatch() {
417
+ const state = JSON.parse((0, node_fs_1.readFileSync)(stateFile, "utf-8"));
418
+ const contextBudget = state.context_budget;
419
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify({
420
+ ...state,
421
+ open_children: [],
422
+ completed_children: ["POL-100"],
423
+ context_budget: {
424
+ ...contextBudget,
425
+ children_completed: 1,
426
+ },
427
+ next_open_child: null,
428
+ status: "cluster-complete",
429
+ }, null, 2), "utf-8");
430
+ return {
431
+ exit_code: 0,
432
+ provider_used: "mock",
433
+ command_run: "mock-worker",
434
+ summary: JSON.stringify({ child_id: "POL-100", status: "done", commit: "abc1234" }),
435
+ };
436
+ },
437
+ };
438
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
439
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
440
+ const finalState = JSON.parse((0, node_fs_1.readFileSync)(stateFile, "utf-8"));
441
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
442
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(1);
443
+ (0, vitest_1.expect)(finalState.completed_children).toEqual(["POL-100"]);
444
+ (0, vitest_1.expect)(finalState.context_budget.children_completed).toBe(1);
445
+ });
446
+ (0, vitest_1.it)("returns state-invalid when current-state.json is malformed", async () => {
447
+ const stateFile = (0, node_path_1.join)(tmpDir, "current-state.json");
448
+ (0, node_fs_1.writeFileSync)(stateFile, '{"not_valid": true}', "utf-8");
449
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(makeMockAdapter([SUCCESS_RESULT]));
450
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
451
+ (0, vitest_1.expect)(result.haltReason).toBe("state-invalid");
452
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(0);
453
+ });
454
+ (0, vitest_1.it)("passes the correct bootstrap packet to the adapter", async () => {
455
+ const calls = [];
456
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT], calls);
457
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
458
+ const stateFile = makeStateFile(tmpDir, {
459
+ open_children: ["POL-100"],
460
+ children_completed: 0,
461
+ max_children_per_session: 10,
462
+ });
463
+ await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
464
+ // Parent now dispatches compiled WorkerPackets (schema_version 2.1)
465
+ // instead of raw v1 BootstrapPackets. WorkerPacket is a structural
466
+ // superset of BootstrapPacket so all adapter contracts remain valid.
467
+ (0, vitest_1.expect)(calls[0].packet).toMatchObject({
468
+ schema_version: "2.1",
469
+ run_id: "test-run-001",
470
+ cluster_id: "POL-99",
471
+ active_child: "POL-100",
472
+ // parent.ts normalizes via realpathSync to resolve macOS /var → /private/var symlinks
473
+ state_file: (0, node_fs_1.realpathSync)(stateFile),
474
+ });
475
+ });
476
+ (0, vitest_1.it)("halts with analyze-parent when cluster root title starts with 'ANALYZE:'", async () => {
477
+ const calls = [];
478
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT], calls);
479
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
480
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-100"], { "POL-99": { title: "ANALYZE: Split execution architecture" } });
481
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
482
+ (0, vitest_1.expect)(result.haltReason).toBe("analyze-parent");
483
+ (0, vitest_1.expect)(result.childrenDispatched).toBe(0);
484
+ (0, vitest_1.expect)(result.message).toBe("polaris-run targets IMPLEMENT parents, not ANALYZE issues. Run polaris-analyze first to create an IMPLEMENT parent.");
485
+ (0, vitest_1.expect)(calls).toHaveLength(0);
486
+ });
487
+ (0, vitest_1.it)("halts with analyze-parent when cluster root has 'analyze' label", async () => {
488
+ const calls = [];
489
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT], calls);
490
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
491
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-100"], { "POL-99": { title: "Plan issue hierarchy", labels: ["analyze"] } });
492
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
493
+ (0, vitest_1.expect)(result.haltReason).toBe("analyze-parent");
494
+ (0, vitest_1.expect)(calls).toHaveLength(0);
495
+ });
496
+ // ── Analyze-drift guardrail tests ──────────────────────────────────────────
497
+ (0, vitest_1.it)("halts with analyze-drift when next child title starts with 'Analyze:'", async () => {
498
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT]);
499
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
500
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-200"], { "POL-200": { title: "Analyze: scheduler behavior" } });
501
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
502
+ (0, vitest_1.expect)(result.haltReason).toBe("analyze-drift");
503
+ (0, vitest_1.expect)(result.haltingChild).toBe("POL-200");
504
+ (0, vitest_1.expect)(result.message).toContain("POL-200");
505
+ (0, vitest_1.expect)(result.message).toContain("allow_analyze_children");
506
+ });
507
+ (0, vitest_1.it)("halts with analyze-drift when next child title starts with 'polaris-analyze'", async () => {
508
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT]);
509
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
510
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-200"], { "POL-200": { title: "polaris-analyze worker adapters" } });
511
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
512
+ (0, vitest_1.expect)(result.haltReason).toBe("analyze-drift");
513
+ (0, vitest_1.expect)(result.haltingChild).toBe("POL-200");
514
+ });
515
+ (0, vitest_1.it)("halts with analyze-drift when next child has 'analyze' label", async () => {
516
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT]);
517
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
518
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-200"], { "POL-200": { labels: ["analyze", "spike"] } });
519
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
520
+ (0, vitest_1.expect)(result.haltReason).toBe("analyze-drift");
521
+ (0, vitest_1.expect)(result.haltingChild).toBe("POL-200");
522
+ });
523
+ (0, vitest_1.it)("does NOT halt for an implementation/fix child", async () => {
524
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT]);
525
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
526
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-200"], { "POL-200": { title: "Implement: add budget guardrail", labels: ["feature"] } });
527
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
528
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
529
+ });
530
+ (0, vitest_1.it)("does NOT halt when allowAnalyzeChildren flag is set", async () => {
531
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT]);
532
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
533
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-200"], { "POL-200": { title: "Analyze: scheduler behavior" } });
534
+ const result = await (0, parent_js_1.runParentLoop)({
535
+ stateFile,
536
+ repoRoot: tmpDir,
537
+ allowAnalyzeChildren: true,
538
+ });
539
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
540
+ });
541
+ (0, vitest_1.it)("does NOT halt when budget.allow_analyze_children is true in config", async () => {
542
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT]);
543
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
544
+ // Override config mock to include allow_analyze_children: true
545
+ const { loadConfig } = await import("../config/loader.js");
546
+ vitest_1.vi.mocked(loadConfig).mockReturnValueOnce({
547
+ orchestration: {
548
+ mode: "auto",
549
+ notification_format: "verbose",
550
+ auto_finalize: false,
551
+ },
552
+ execution: {
553
+ adapter: "mock",
554
+ providers: { mock: { command: "mock-worker" } },
555
+ rotation: ["mock"],
556
+ },
557
+ budget: {
558
+ allow_analyze_children: true,
559
+ },
560
+ });
561
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-200"], { "POL-200": { title: "Analyze: scheduler behavior" } });
562
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
563
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
564
+ });
565
+ (0, vitest_1.it)("uses the agent-subtask adapter when orchestration_mode is ephemeral", async () => {
566
+ const calls = [];
567
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT], calls);
568
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
569
+ const { loadConfig } = await import("../config/loader.js");
570
+ vitest_1.vi.mocked(loadConfig).mockReturnValueOnce({
571
+ orchestration: {
572
+ mode: "auto",
573
+ notification_format: "verbose",
574
+ auto_finalize: false,
575
+ },
576
+ execution: {
577
+ adapter: "terminal-cli",
578
+ providers: { terminal: { command: "terminal-worker" } },
579
+ rotation: ["terminal"],
580
+ },
581
+ });
582
+ const stateFile = makeStateFile(tmpDir, {
583
+ open_children: ["POL-100"],
584
+ children_completed: 0,
585
+ max_children_per_session: 10,
586
+ });
587
+ const state = JSON.parse((0, node_fs_1.readFileSync)(stateFile, "utf-8"));
588
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify({ ...state, orchestration_mode: "ephemeral" }, null, 2), "utf-8");
589
+ await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
590
+ (0, vitest_1.expect)(registry_js_1.createAdapter).toHaveBeenCalledWith("agent-subtask", vitest_1.expect.objectContaining({ adapter: "agent-subtask" }));
591
+ (0, vitest_1.expect)(calls[0].options.provider).toBe("agent-subtask");
592
+ });
593
+ (0, vitest_1.it)("uses the configured adapter when orchestration_mode is persistent-parent", async () => {
594
+ const calls = [];
595
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT], calls);
596
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
597
+ const stateFile = makeStateFile(tmpDir, {
598
+ open_children: ["POL-100"],
599
+ children_completed: 0,
600
+ max_children_per_session: 10,
601
+ });
602
+ const state = JSON.parse((0, node_fs_1.readFileSync)(stateFile, "utf-8"));
603
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify({ ...state, orchestration_mode: "persistent-parent" }, null, 2), "utf-8");
604
+ await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
605
+ (0, vitest_1.expect)(registry_js_1.createAdapter).toHaveBeenCalledWith("mock", vitest_1.expect.objectContaining({ adapter: "mock" }));
606
+ (0, vitest_1.expect)(calls[0].options.provider).toBe("mock");
607
+ });
608
+ (0, vitest_1.it)("omits undefined commit hashes from terse completion output", async () => {
609
+ const calls = [];
610
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(makeMockAdapter([SUCCESS_RESULT_NO_COMMIT], calls));
611
+ const stateFile = makeStateFile(tmpDir, {
612
+ open_children: ["POL-100"],
613
+ children_completed: 0,
614
+ max_children_per_session: 10,
615
+ });
616
+ let stdout = "";
617
+ const originalStdoutWrite = process.stdout.write;
618
+ process.stdout.write = ((chunk) => {
619
+ stdout += String(chunk);
620
+ return true;
621
+ });
622
+ try {
623
+ const { loadConfig } = await import("../config/loader.js");
624
+ vitest_1.vi.mocked(loadConfig).mockReturnValueOnce({
625
+ orchestration: {
626
+ mode: "auto",
627
+ notification_format: "terse",
628
+ auto_finalize: false,
629
+ },
630
+ execution: {
631
+ adapter: "mock",
632
+ providers: { mock: { command: "mock-worker" } },
633
+ rotation: ["mock"],
634
+ },
635
+ });
636
+ await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
637
+ }
638
+ finally {
639
+ process.stdout.write = originalStdoutWrite;
640
+ }
641
+ (0, vitest_1.expect)(stdout).toContain("[POLARIS] COMPLETE POL-100");
642
+ (0, vitest_1.expect)(stdout).not.toContain("undefined");
643
+ (0, vitest_1.expect)(stdout).not.toContain("(commit:");
644
+ });
645
+ (0, vitest_1.it)("records an explicit auto-finalize handoff in auto mode", async () => {
646
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(makeMockAdapter([SUCCESS_RESULT]));
647
+ const stateFile = makeStateFile(tmpDir, {
648
+ open_children: [],
649
+ completed_children: ["POL-100"],
650
+ children_completed: 1,
651
+ max_children_per_session: 10,
652
+ });
653
+ const { loadConfig } = await import("../config/loader.js");
654
+ vitest_1.vi.mocked(loadConfig).mockReturnValueOnce({
655
+ orchestration: {
656
+ mode: "auto",
657
+ notification_format: "verbose",
658
+ auto_finalize: true,
659
+ },
660
+ execution: {
661
+ adapter: "mock",
662
+ providers: { mock: { command: "mock-worker" } },
663
+ rotation: ["mock"],
664
+ },
665
+ });
666
+ const result = await (0, parent_js_1.runParentLoop)({ stateFile, repoRoot: tmpDir });
667
+ (0, vitest_1.expect)(result.haltReason).toBe("cluster-complete");
668
+ (0, vitest_1.expect)(result.message).toContain("Auto-finalize handoff requested");
669
+ const telemetry = readJsonLines((0, node_path_1.join)(tmpDir, "runs", "test-run-001", "telemetry.jsonl"));
670
+ (0, vitest_1.expect)(telemetry).toEqual(vitest_1.expect.arrayContaining([
671
+ vitest_1.expect.objectContaining({ event: "cluster-complete" }),
672
+ vitest_1.expect.objectContaining({
673
+ event: "auto-finalize-requested",
674
+ next_action: "polaris finalize run",
675
+ }),
676
+ ]));
677
+ });
678
+ (0, vitest_1.it)("CLI loop run dry-run with a valid state file runs to cluster-complete and exits 0", async () => {
679
+ const calls = [];
680
+ const mockAdapter = makeMockAdapter([SUCCESS_RESULT], calls);
681
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(mockAdapter);
682
+ const stateFile = makeStateFile(tmpDir, {
683
+ open_children: ["POL-100"],
684
+ children_completed: 0,
685
+ max_children_per_session: 10,
686
+ });
687
+ const command = (0, index_js_1.createLoopCommand)({ repoRoot: tmpDir });
688
+ const result = await runLoopCommand(command, [
689
+ "run",
690
+ "POL-99",
691
+ "--state-file",
692
+ stateFile,
693
+ "--adapter",
694
+ "mock",
695
+ "--provider",
696
+ "mock",
697
+ "--dry-run",
698
+ ]);
699
+ (0, vitest_1.expect)(result.exitCode).toBe(0);
700
+ (0, vitest_1.expect)(result.stdout).toContain("Polaris parent loop halted: cluster-complete");
701
+ (0, vitest_1.expect)(result.stdout).toContain("Cluster complete");
702
+ (0, vitest_1.expect)(calls).toHaveLength(1);
703
+ (0, vitest_1.expect)(calls[0].options).toMatchObject({ provider: "mock", dryRun: true });
704
+ });
705
+ (0, vitest_1.it)("CLI loop run exits 1 when state file is invalid", async () => {
706
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(makeMockAdapter([SUCCESS_RESULT]));
707
+ const stateFile = (0, node_path_1.join)(tmpDir, "current-state.json");
708
+ (0, node_fs_1.writeFileSync)(stateFile, '{"not_valid": true}', "utf-8");
709
+ const command = (0, index_js_1.createLoopCommand)({ repoRoot: tmpDir });
710
+ const result = await runLoopCommand(command, [
711
+ "run",
712
+ "POL-99",
713
+ "--state-file",
714
+ stateFile,
715
+ "--dry-run",
716
+ ]);
717
+ (0, vitest_1.expect)(result.exitCode).toBe(1);
718
+ (0, vitest_1.expect)(result.stderr).toContain("Polaris parent loop halted: state-invalid");
719
+ (0, vitest_1.expect)(result.stderr).toContain("current-state.json is invalid");
720
+ });
721
+ (0, vitest_1.it)("CLI loop run exits 1 with an ANALYZE parent message", async () => {
722
+ const calls = [];
723
+ vitest_1.vi.mocked(registry_js_1.createAdapter).mockReturnValue(makeMockAdapter([SUCCESS_RESULT], calls));
724
+ const stateFile = makeStateFileWithMeta(tmpDir, ["POL-100"], { "POL-99": { title: "ANALYZE: Split execution architecture" } });
725
+ const command = (0, index_js_1.createLoopCommand)({ repoRoot: tmpDir });
726
+ const result = await runLoopCommand(command, [
727
+ "run",
728
+ "POL-99",
729
+ "--state-file",
730
+ stateFile,
731
+ "--dry-run",
732
+ ]);
733
+ (0, vitest_1.expect)(result.exitCode).toBe(1);
734
+ (0, vitest_1.expect)(result.stderr).toContain("Polaris parent loop halted: analyze-parent");
735
+ (0, vitest_1.expect)(result.stderr).toContain("polaris-run targets IMPLEMENT parents");
736
+ (0, vitest_1.expect)(calls).toHaveLength(0);
737
+ });
738
+ });