@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,337 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const node_os_1 = require("node:os");
7
+ const continue_js_1 = require("./continue.js");
8
+ const checkpoint_js_1 = require("./checkpoint.js");
9
+ function makeTestDir() {
10
+ const dir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `polaris-loop-test-${Date.now()}`);
11
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
12
+ // Minimal git repo so branch detection doesn't blow up
13
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(dir, ".git"), { recursive: true });
14
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, ".git/HEAD"), "ref: refs/heads/test-branch\n");
15
+ return dir;
16
+ }
17
+ function writeState(dir, state) {
18
+ const stateFile = (0, node_path_1.join)(dir, ".polaris", "runs", "current-state.json");
19
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(dir, ".polaris", "runs"), { recursive: true });
20
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify(state, null, 2));
21
+ return stateFile;
22
+ }
23
+ (0, vitest_1.describe)("validateState", () => {
24
+ (0, vitest_1.it)("returns no errors for a valid state", () => {
25
+ const state = {
26
+ schema_version: "1.0",
27
+ run_id: "test-run-1",
28
+ cluster_id: "POL-5",
29
+ active_child: "POL-23",
30
+ completed_children: [],
31
+ open_children: ["POL-24"],
32
+ step_cursor: "03-execute-child",
33
+ context_budget: { children_completed: 0 },
34
+ status: "running",
35
+ next_open_child: "POL-24",
36
+ };
37
+ (0, vitest_1.expect)((0, checkpoint_js_1.validateState)(state)).toEqual([]);
38
+ });
39
+ (0, vitest_1.it)("returns errors for missing required fields", () => {
40
+ const errors = (0, checkpoint_js_1.validateState)({ schema_version: "1.0" });
41
+ (0, vitest_1.expect)(errors.length).toBeGreaterThan(0);
42
+ (0, vitest_1.expect)(errors.some((e) => e.includes("run_id"))).toBe(true);
43
+ });
44
+ (0, vitest_1.it)("rejects non-object input", () => {
45
+ (0, vitest_1.expect)((0, checkpoint_js_1.validateState)("string")).toContain("current-state.json must be a JSON object");
46
+ (0, vitest_1.expect)((0, checkpoint_js_1.validateState)(null)).toContain("current-state.json must be a JSON object");
47
+ });
48
+ });
49
+ (0, vitest_1.describe)("runLoopContinue", () => {
50
+ let testDir;
51
+ (0, vitest_1.beforeEach)(() => {
52
+ testDir = makeTestDir();
53
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(testDir, ".polaris", "bootstrap"), { recursive: true });
54
+ });
55
+ (0, vitest_1.afterEach)(() => {
56
+ (0, node_fs_1.rmSync)(testDir, { recursive: true, force: true });
57
+ });
58
+ (0, vitest_1.it)("writes a bootstrap packet to .polaris/bootstrap/", () => {
59
+ const state = {
60
+ schema_version: "1.0",
61
+ run_id: "pol-5-session-1",
62
+ cluster_id: "POL-5",
63
+ active_child: "POL-23",
64
+ completed_children: [],
65
+ open_children: ["POL-24", "POL-25", "POL-26", "POL-27"],
66
+ step_cursor: "03-execute-child",
67
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
68
+ status: "running",
69
+ next_open_child: "POL-24",
70
+ };
71
+ const stateFile = writeState(testDir, state);
72
+ // Capture stdout
73
+ const stdoutChunks = [];
74
+ const originalWrite = process.stdout.write.bind(process.stdout);
75
+ process.stdout.write = (chunk) => {
76
+ stdoutChunks.push(chunk.toString());
77
+ return true;
78
+ };
79
+ try {
80
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
81
+ }
82
+ finally {
83
+ process.stdout.write = originalWrite;
84
+ }
85
+ // Bootstrap packet written to disk
86
+ const bootstrapDir = (0, node_path_1.join)(testDir, ".polaris", "bootstrap");
87
+ const packets = require("node:fs").readdirSync(bootstrapDir).filter((f) => f.endsWith(".json"));
88
+ (0, vitest_1.expect)(packets.length).toBe(1);
89
+ const packet = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(bootstrapDir, packets[0]), "utf-8"));
90
+ (0, vitest_1.expect)(packet.run_id).toBe("pol-5-session-1");
91
+ (0, vitest_1.expect)(packet.last_completed_child).toBe("POL-23");
92
+ (0, vitest_1.expect)(packet.current_state_sha).toBeTruthy();
93
+ (0, vitest_1.expect)(packet.open_children).toEqual(["POL-24", "POL-25", "POL-26", "POL-27"]);
94
+ (0, vitest_1.expect)(packet.execution_adapter.mode).toBe("terminal-cli");
95
+ (0, vitest_1.expect)(packet.execution_adapter.compact_bootstrap_state.child_id).toBe("POL-24");
96
+ });
97
+ (0, vitest_1.it)("updates current-state.json atomically (moves active_child to completed)", () => {
98
+ const state = {
99
+ schema_version: "1.0",
100
+ run_id: "pol-5-session-1",
101
+ cluster_id: "POL-5",
102
+ active_child: "POL-23",
103
+ completed_children: [],
104
+ open_children: ["POL-24"],
105
+ step_cursor: "03-execute-child",
106
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
107
+ status: "running",
108
+ next_open_child: "POL-24",
109
+ };
110
+ const stateFile = writeState(testDir, state);
111
+ const originalWrite = process.stdout.write.bind(process.stdout);
112
+ process.stdout.write = () => true;
113
+ try {
114
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
115
+ }
116
+ finally {
117
+ process.stdout.write = originalWrite;
118
+ }
119
+ const updated = (0, checkpoint_js_1.readState)(stateFile);
120
+ (0, vitest_1.expect)(updated.completed_children).toContain("POL-23");
121
+ (0, vitest_1.expect)(updated.active_child).toBe("");
122
+ (0, vitest_1.expect)(updated.open_children).toEqual(["POL-24"]);
123
+ (0, vitest_1.expect)(updated.context_budget.children_completed).toBe(1);
124
+ });
125
+ (0, vitest_1.it)("removes completed child from open_children before selecting next child", () => {
126
+ const state = {
127
+ schema_version: "1.0",
128
+ run_id: "pol-5-session-1",
129
+ cluster_id: "POL-5",
130
+ active_child: "POL-23",
131
+ completed_children: [],
132
+ open_children: ["POL-23", "POL-24"],
133
+ step_cursor: "03-execute-child",
134
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
135
+ status: "running",
136
+ next_open_child: "POL-23",
137
+ };
138
+ const stateFile = writeState(testDir, state);
139
+ const logs = [];
140
+ const origLog = console.log;
141
+ console.log = (...args) => logs.push(args.map(String).join(" "));
142
+ try {
143
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
144
+ }
145
+ finally {
146
+ console.log = origLog;
147
+ }
148
+ const updated = (0, checkpoint_js_1.readState)(stateFile);
149
+ const packet = JSON.parse(logs.join("\n"));
150
+ (0, vitest_1.expect)(updated.completed_children).toEqual(["POL-23"]);
151
+ (0, vitest_1.expect)(updated.open_children).toEqual(["POL-24"]);
152
+ (0, vitest_1.expect)(updated.next_open_child).toBe("POL-24");
153
+ (0, vitest_1.expect)(packet.open_children).toEqual(["POL-24"]);
154
+ (0, vitest_1.expect)(packet.execution_adapter.compact_bootstrap_state.child_id).toBe("POL-24");
155
+ });
156
+ (0, vitest_1.it)("appends a JSONL checkpoint event to telemetry file", () => {
157
+ const state = {
158
+ schema_version: "1.0",
159
+ run_id: "pol-5-session-1",
160
+ cluster_id: "POL-5",
161
+ active_child: "POL-23",
162
+ completed_children: [],
163
+ open_children: ["POL-24"],
164
+ step_cursor: "03-execute-child",
165
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
166
+ status: "running",
167
+ next_open_child: "POL-24",
168
+ artifact_dir: (0, node_path_1.join)(testDir, ".taskchain_artifacts", "bootstrap-run"),
169
+ };
170
+ const stateFile = writeState(testDir, state);
171
+ const originalWrite = process.stdout.write.bind(process.stdout);
172
+ process.stdout.write = () => true;
173
+ try {
174
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
175
+ }
176
+ finally {
177
+ process.stdout.write = originalWrite;
178
+ }
179
+ const telemetryFile = (0, node_path_1.join)(testDir, ".taskchain_artifacts", "bootstrap-run", "runs", "pol-5-session-1", "telemetry.jsonl");
180
+ (0, vitest_1.expect)((0, node_fs_1.existsSync)(telemetryFile)).toBe(true);
181
+ const lines = (0, node_fs_1.readFileSync)(telemetryFile, "utf-8")
182
+ .trim()
183
+ .split("\n")
184
+ .map((l) => JSON.parse(l));
185
+ const line = lines.find((e) => e.event === "loop-checkpoint");
186
+ (0, vitest_1.expect)(line).toBeTruthy();
187
+ (0, vitest_1.expect)(line.run_id).toBe("pol-5-session-1");
188
+ (0, vitest_1.expect)(line.child_id).toBe("POL-23");
189
+ });
190
+ (0, vitest_1.it)("emits bootstrap packet JSON to stdout", () => {
191
+ const state = {
192
+ schema_version: "1.0",
193
+ run_id: "pol-5-session-1",
194
+ cluster_id: "POL-5",
195
+ active_child: "POL-23",
196
+ completed_children: [],
197
+ open_children: ["POL-24"],
198
+ step_cursor: "03-execute-child",
199
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
200
+ status: "running",
201
+ next_open_child: "POL-24",
202
+ };
203
+ const stateFile = writeState(testDir, state);
204
+ const stdoutChunks = [];
205
+ const originalWrite = process.stdout.write.bind(process.stdout);
206
+ const originalLog = console.log;
207
+ const logs = [];
208
+ console.log = (...args) => logs.push(args.map(String).join(" "));
209
+ process.stdout.write = (chunk) => {
210
+ stdoutChunks.push(chunk.toString());
211
+ return true;
212
+ };
213
+ try {
214
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
215
+ }
216
+ finally {
217
+ process.stdout.write = originalWrite;
218
+ console.log = originalLog;
219
+ }
220
+ const output = logs.join("\n");
221
+ const packet = JSON.parse(output);
222
+ (0, vitest_1.expect)(packet.current_state_sha).toBeTruthy();
223
+ (0, vitest_1.expect)(packet.run_id).toBe("pol-5-session-1");
224
+ });
225
+ (0, vitest_1.it)("exits with error if state file missing", () => {
226
+ const exitSpy = vitest_1.vi.spyOn(process, "exit").mockImplementation(() => {
227
+ throw new Error("process.exit called");
228
+ });
229
+ (0, vitest_1.expect)(() => (0, continue_js_1.runLoopContinue)({
230
+ stateFile: (0, node_path_1.join)(testDir, "nonexistent.json"),
231
+ repoRoot: testDir,
232
+ })).toThrow();
233
+ exitSpy.mockRestore();
234
+ });
235
+ (0, vitest_1.it)("exits with error if state file is invalid", () => {
236
+ const stateFile = (0, node_path_1.join)(testDir, ".polaris", "runs", "current-state.json");
237
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(testDir, ".polaris", "runs"), { recursive: true });
238
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify({ schema_version: "1.0" }));
239
+ const exitSpy = vitest_1.vi.spyOn(process, "exit").mockImplementation(() => {
240
+ throw new Error("process.exit called");
241
+ });
242
+ const stderrSpy = vitest_1.vi.spyOn(process.stderr, "write").mockImplementation(() => true);
243
+ (0, vitest_1.expect)(() => (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir })).toThrow();
244
+ exitSpy.mockRestore();
245
+ stderrSpy.mockRestore();
246
+ });
247
+ (0, vitest_1.it)("sets boundary_enforcement in packet when analyze→implement boundary fires", () => {
248
+ const state = {
249
+ schema_version: "1.0",
250
+ run_id: "pol-5-session-1",
251
+ cluster_id: "POL-5",
252
+ active_child: "POL-23",
253
+ session_type: "analyze",
254
+ completed_children: [],
255
+ open_children: ["POL-24"],
256
+ open_children_meta: { "POL-24": { type: "implement" } },
257
+ step_cursor: "03-execute-child",
258
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
259
+ status: "running",
260
+ next_open_child: "POL-24",
261
+ };
262
+ const stateFile = writeState(testDir, state);
263
+ const logs = [];
264
+ const origLog = console.log;
265
+ console.log = (...args) => logs.push(args.map(String).join(" "));
266
+ try {
267
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
268
+ }
269
+ finally {
270
+ console.log = origLog;
271
+ }
272
+ const packet = JSON.parse(logs.join("\n"));
273
+ (0, vitest_1.expect)(packet.boundary_enforcement).toContain("analyze-session-ended");
274
+ });
275
+ (0, vitest_1.it)("emits boundary JSONL event to telemetry when boundary fires", () => {
276
+ const state = {
277
+ schema_version: "1.0",
278
+ run_id: "pol-5-session-1",
279
+ cluster_id: "POL-5",
280
+ active_child: "POL-23",
281
+ session_type: "analyze",
282
+ completed_children: [],
283
+ open_children: ["POL-24"],
284
+ open_children_meta: { "POL-24": { type: "implement" } },
285
+ step_cursor: "03-execute-child",
286
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
287
+ status: "running",
288
+ next_open_child: "POL-24",
289
+ artifact_dir: (0, node_path_1.join)(testDir, ".taskchain_artifacts", "bootstrap-run"),
290
+ };
291
+ const stateFile = writeState(testDir, state);
292
+ const origLog = console.log;
293
+ console.log = () => { };
294
+ try {
295
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
296
+ }
297
+ finally {
298
+ console.log = origLog;
299
+ }
300
+ const telemetryFile = (0, node_path_1.join)(testDir, ".taskchain_artifacts", "bootstrap-run", "runs", "pol-5-session-1", "telemetry.jsonl");
301
+ const lines = (0, node_fs_1.readFileSync)(telemetryFile, "utf-8")
302
+ .trim()
303
+ .split("\n")
304
+ .map((l) => JSON.parse(l));
305
+ const boundaryEvent = lines.find((e) => e.event === "analyze-impl-boundary-enforced");
306
+ (0, vitest_1.expect)(boundaryEvent).toBeTruthy();
307
+ (0, vitest_1.expect)(boundaryEvent.stopped_before).toBe("POL-24");
308
+ });
309
+ (0, vitest_1.it)("does NOT set boundary_enforcement when both are implement type", () => {
310
+ const state = {
311
+ schema_version: "1.0",
312
+ run_id: "pol-5-session-1",
313
+ cluster_id: "POL-5",
314
+ active_child: "POL-23",
315
+ session_type: "implement",
316
+ completed_children: [],
317
+ open_children: ["POL-24"],
318
+ open_children_meta: { "POL-24": { type: "implement" } },
319
+ step_cursor: "03-execute-child",
320
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
321
+ status: "running",
322
+ next_open_child: "POL-24",
323
+ };
324
+ const stateFile = writeState(testDir, state);
325
+ const logs = [];
326
+ const origLog = console.log;
327
+ console.log = (...args) => logs.push(args.map(String).join(" "));
328
+ try {
329
+ (0, continue_js_1.runLoopContinue)({ stateFile, repoRoot: testDir });
330
+ }
331
+ finally {
332
+ console.log = origLog;
333
+ }
334
+ const packet = JSON.parse(logs.join("\n"));
335
+ (0, vitest_1.expect)(packet.boundary_enforcement).toBeUndefined();
336
+ });
337
+ });
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ /**
3
+ * Polaris Dispatch Boundary Enforcement
4
+ *
5
+ * The dispatch boundary is a hard runtime constraint enforced by the Polaris
6
+ * runtime. The parent/orchestrator MUST call `polaris loop dispatch` before
7
+ * any child execution. Inline child execution by the parent/orchestrator is
8
+ * FORBIDDEN. Models do not decide execution legality — the runtime owns it.
9
+ *
10
+ * ──────────────────────────────────────────────────────────────────────────
11
+ * Allowed state transitions (enforced at runtime):
12
+ *
13
+ * selected → dispatched (via polaris loop dispatch)
14
+ * dispatched → worker-running (via adapter dispatch)
15
+ * worker-running → worker-completed (via worker CompactReturn)
16
+ * worker-completed → checkpointed (via polaris loop continue)
17
+ * dispatched → checkpointed (worker wrote own completion)
18
+ * checkpointed → dispatched (via polaris loop dispatch, next child)
19
+ * checkpointed → cluster-complete (no remaining children)
20
+ * * → blocked (via polaris loop abort)
21
+ * * → budget-exhausted (via budget check)
22
+ *
23
+ * Disallowed transitions (hard failure + telemetry):
24
+ *
25
+ * selected → completed (no dispatch: inline completion)
26
+ * selected → checkpointed (no dispatch: fake checkpoint)
27
+ * selected → implementation-inline (parent doing child work directly)
28
+ * idle → worker-completed (worker completed without dispatch)
29
+ * idle → checkpointed (continue called without dispatch)
30
+ * ──────────────────────────────────────────────────────────────────────────
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.DISALLOWED_TRANSITIONS = exports.ALLOWED_TRANSITIONS = exports.DISPATCH_REQUIRED_ERROR = exports.LIBRARIAN_GATE_ERROR = exports.INLINE_EXECUTION_ERROR = void 0;
34
+ exports.getMachineState = getMachineState;
35
+ exports.appendDispatchViolationEvent = appendDispatchViolationEvent;
36
+ exports.assertNoActiveChildBeforeDispatch = assertNoActiveChildBeforeDispatch;
37
+ exports.assertContinueRequiresDispatch = assertContinueRequiresDispatch;
38
+ exports.assertDispatchedBeforeCompletion = assertDispatchedBeforeCompletion;
39
+ exports.validateTransition = validateTransition;
40
+ exports.initialDispatchBoundary = initialDispatchBoundary;
41
+ exports.advanceDispatchEpoch = advanceDispatchEpoch;
42
+ exports.advanceContinueEpoch = advanceContinueEpoch;
43
+ const node_fs_1 = require("node:fs");
44
+ const node_path_1 = require("node:path");
45
+ // ──────────────────────────────────────────────────────────────────────────────
46
+ // Error constants
47
+ // ──────────────────────────────────────────────────────────────────────────────
48
+ /**
49
+ * Error emitted when the parent/orchestrator attempts inline child execution.
50
+ * This is the canonical error message for dispatch boundary violations.
51
+ */
52
+ exports.INLINE_EXECUTION_ERROR = "Child execution requires dispatch boundary. Parent/orchestrator may not implement child inline. Use npm run polaris -- loop dispatch.";
53
+ /**
54
+ * Error emitted when PR creation is attempted without a passing Librarian gate.
55
+ */
56
+ exports.LIBRARIAN_GATE_ERROR = "PR creation requires Closeout Librarian gate. Generate packet with `polaris librarian packet <cluster-id>`, dispatch the Librarian, then re-run finalize.";
57
+ /**
58
+ * Error emitted when `polaris loop continue` is called without a prior dispatch.
59
+ */
60
+ exports.DISPATCH_REQUIRED_ERROR = "Dispatch required before continuation. Call `npm run polaris -- loop dispatch` first, then run the worker before calling continue.";
61
+ /**
62
+ * Allowed transitions in the dispatch state machine.
63
+ * Format: [from, to, command-that-drives-transition]
64
+ */
65
+ exports.ALLOWED_TRANSITIONS = [
66
+ // Primary dispatch flow
67
+ ["idle", "dispatched", "polaris loop dispatch"],
68
+ ["checkpointed", "dispatched", "polaris loop dispatch"],
69
+ ["dispatched", "worker-running", "adapter-dispatch"],
70
+ ["worker-running", "worker-completed", "worker-compact-return"],
71
+ ["dispatched", "worker-completed", "worker-compact-return"],
72
+ ["worker-completed", "checkpointed", "polaris loop continue"],
73
+ ["dispatched", "checkpointed", "polaris loop continue"], // Worker wrote own completion
74
+ ["checkpointed", "cluster-complete", "polaris loop continue"],
75
+ // Halt paths (allowed from any operational state)
76
+ ["idle", "blocked", "polaris loop abort"],
77
+ ["dispatched", "blocked", "polaris loop abort"],
78
+ ["worker-running", "blocked", "polaris loop abort"],
79
+ ["worker-completed", "blocked", "polaris loop abort"],
80
+ ["checkpointed", "blocked", "polaris loop abort"],
81
+ // Budget exhaustion (allowed from any operational state)
82
+ ["idle", "budget-exhausted", "budget-check"],
83
+ ["dispatched", "budget-exhausted", "budget-check"],
84
+ ["checkpointed", "budget-exhausted", "budget-check"],
85
+ // Librarian phase (post cluster-complete, pre PR-creation)
86
+ ["cluster-complete", "librarian-dispatched", "polaris librarian packet + dispatch"],
87
+ ["librarian-dispatched", "librarian-complete", "librarian-sealed-result"],
88
+ ["librarian-complete", "librarian-complete", "finalize-delivery-attempt"], // delivery proceeds after librarian gate
89
+ ];
90
+ /**
91
+ * Transitions that are explicitly forbidden. These represent illegal paths
92
+ * that indicate inline execution or state machine violations.
93
+ */
94
+ exports.DISALLOWED_TRANSITIONS = [
95
+ ["idle", "worker-completed", "attempted worker completion without prior dispatch"],
96
+ ["idle", "checkpointed", "attempted continue without prior dispatch"],
97
+ ["idle", "cluster-complete", "attempted cluster completion without dispatch"],
98
+ ["selected", "completed", "attempted inline completion without dispatch"],
99
+ ["selected", "checkpointed", "attempted checkpoint without dispatch"],
100
+ ["selected", "implementation-inline", "parent doing child work directly (forbidden)"],
101
+ ];
102
+ // ──────────────────────────────────────────────────────────────────────────────
103
+ // State derivation
104
+ // ──────────────────────────────────────────────────────────────────────────────
105
+ /**
106
+ * Derive the logical dispatch machine state from the current LoopState.
107
+ * Uses dispatch_boundary epochs when present; falls back to step_cursor
108
+ * for states written before dispatch_boundary was introduced.
109
+ */
110
+ function getMachineState(state) {
111
+ // Terminal status values map directly to machine states
112
+ if (state.status === "cluster-complete")
113
+ return "cluster-complete";
114
+ if (state.status === "blocked")
115
+ return "blocked";
116
+ if (state.status === "budget-exhausted")
117
+ return "budget-exhausted";
118
+ const boundary = state.dispatch_boundary;
119
+ if (boundary) {
120
+ const { dispatch_epoch, continue_epoch } = boundary;
121
+ if (dispatch_epoch > continue_epoch) {
122
+ // Dispatch was called more times than continue — either dispatched or worker-completed
123
+ if (state.active_child && state.active_child !== "") {
124
+ // active_child is set → dispatch in progress, worker may be running
125
+ return "dispatched";
126
+ }
127
+ // active_child is cleared by worker → worker has returned, awaiting continue
128
+ return "worker-completed";
129
+ }
130
+ // dispatch_epoch === continue_epoch
131
+ if (dispatch_epoch === 0 && continue_epoch === 0)
132
+ return "idle";
133
+ return "checkpointed";
134
+ }
135
+ // Legacy fallback: no dispatch_boundary field present
136
+ // Infer from step_cursor and active_child
137
+ if (state.step_cursor === "dispatch") {
138
+ return state.active_child ? "dispatched" : "worker-completed";
139
+ }
140
+ if (state.step_cursor === "checkpoint") {
141
+ // Could be checkpointed (after continue) or worker-completed (before continue)
142
+ // We cannot distinguish safely, so treat as checkpointed (the safe state)
143
+ return "checkpointed";
144
+ }
145
+ return "idle";
146
+ }
147
+ /**
148
+ * Append a dispatch boundary violation event to the telemetry file.
149
+ * Must only emit on failure paths, not normal flow.
150
+ *
151
+ * Telemetry write failures are silently swallowed to ensure the
152
+ * enforcement error is always surfaced to the caller.
153
+ */
154
+ function appendDispatchViolationEvent(telemetryFile, event) {
155
+ try {
156
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
157
+ (0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
158
+ }
159
+ catch {
160
+ // Telemetry write failure must not mask the underlying enforcement error
161
+ }
162
+ }
163
+ // ──────────────────────────────────────────────────────────────────────────────
164
+ // Enforcement guards
165
+ // ──────────────────────────────────────────────────────────────────────────────
166
+ /**
167
+ * Assert that no child is already active before the parent/orchestrator
168
+ * attempts to dispatch another child.
169
+ *
170
+ * If `active_child` is set, it means a previous dispatch was not properly
171
+ * completed. The parent must NOT re-dispatch or complete inline — it must
172
+ * halt and require manual resolution.
173
+ *
174
+ * @throws Error with INLINE_EXECUTION_ERROR if active_child is already set
175
+ */
176
+ function assertNoActiveChildBeforeDispatch(state, telemetryFile) {
177
+ if (state.active_child && state.active_child !== "") {
178
+ appendDispatchViolationEvent(telemetryFile, {
179
+ event: "invalid-inline-attempt",
180
+ run_id: state.run_id,
181
+ child_id: state.active_child,
182
+ from_state: getMachineState(state),
183
+ to_state: "dispatched",
184
+ reason: `active_child is already set to "${state.active_child}". Previous dispatch not completed. ` + exports.INLINE_EXECUTION_ERROR,
185
+ timestamp: new Date().toISOString(),
186
+ });
187
+ throw new Error(`active_child is already set to "${state.active_child}". Previous dispatch was not completed. ` +
188
+ exports.INLINE_EXECUTION_ERROR +
189
+ " Use `npm run polaris -- loop abort` to reset blocked state.");
190
+ }
191
+ }
192
+ /**
193
+ * Assert that `polaris loop dispatch` was called before allowing
194
+ * `polaris loop continue` to checkpoint state.
195
+ *
196
+ * The check uses dispatch_boundary epochs when present (primary),
197
+ * and falls back to step_cursor for legacy states.
198
+ *
199
+ * @throws Error with DISPATCH_REQUIRED_ERROR if no dispatch preceded this continue
200
+ */
201
+ function assertContinueRequiresDispatch(state, telemetryFile) {
202
+ const boundary = state.dispatch_boundary;
203
+ let wasDispatched;
204
+ if (boundary) {
205
+ // Primary check: dispatch_epoch must be strictly greater than continue_epoch
206
+ wasDispatched = boundary.dispatch_epoch > boundary.continue_epoch;
207
+ }
208
+ else {
209
+ // Legacy fallback: no dispatch_boundary field present.
210
+ //
211
+ // dispatch.ts is the ONLY place that sets active_child. If active_child is
212
+ // non-empty, dispatch was called (and the worker hasn't cleared it yet).
213
+ // If step_cursor is "dispatch", dispatch was called (standard case).
214
+ // If step_cursor is "checkpoint" with active_child cleared, we cannot tell
215
+ // for certain — a conservative rejection would break existing flows, so we
216
+ // accept it for legacy states (dispatch_boundary was not yet tracking).
217
+ wasDispatched =
218
+ (state.active_child !== "" && state.active_child !== undefined && state.active_child !== null) ||
219
+ state.step_cursor === "dispatch" ||
220
+ state.step_cursor === "checkpoint";
221
+ }
222
+ if (!wasDispatched) {
223
+ appendDispatchViolationEvent(telemetryFile, {
224
+ event: "dispatch-required",
225
+ run_id: state.run_id,
226
+ child_id: state.active_child || undefined,
227
+ from_state: getMachineState(state),
228
+ to_state: "checkpointed",
229
+ reason: exports.DISPATCH_REQUIRED_ERROR,
230
+ timestamp: new Date().toISOString(),
231
+ });
232
+ throw new Error(exports.DISPATCH_REQUIRED_ERROR);
233
+ }
234
+ }
235
+ /**
236
+ * Assert that a dispatched child completion is valid.
237
+ *
238
+ * Before the parent/orchestrator advances state to mark a child as completed,
239
+ * verify that the dispatch boundary was properly established. If no dispatch
240
+ * was recorded, the child cannot be marked as complete.
241
+ *
242
+ * @param state - Current (post-worker-return) loop state
243
+ * @param childId - Child being completed
244
+ * @param telemetryFile - Telemetry file path
245
+ * @throws Error with INLINE_EXECUTION_ERROR if dispatch boundary was not set
246
+ */
247
+ function assertDispatchedBeforeCompletion(state, childId, telemetryFile) {
248
+ const boundary = state.dispatch_boundary;
249
+ let dispatchHappened;
250
+ if (boundary) {
251
+ // dispatch_epoch > continue_epoch means dispatch was called and not yet matched
252
+ dispatchHappened =
253
+ boundary.dispatch_epoch > boundary.continue_epoch &&
254
+ (boundary.last_dispatched_child === childId || !boundary.last_dispatched_child);
255
+ }
256
+ else {
257
+ // Legacy: step_cursor "dispatch" set by polaris loop dispatch
258
+ dispatchHappened =
259
+ state.step_cursor === "dispatch" ||
260
+ // Worker may have already updated step_cursor to "checkpoint"
261
+ (state.step_cursor === "checkpoint" && state.active_child === "");
262
+ }
263
+ if (!dispatchHappened) {
264
+ appendDispatchViolationEvent(telemetryFile, {
265
+ event: "illegal-state-transition",
266
+ run_id: state.run_id,
267
+ child_id: childId,
268
+ from_state: getMachineState(state),
269
+ to_state: "checkpointed",
270
+ reason: `Attempted to complete child "${childId}" without a prior dispatch event. ` +
271
+ exports.INLINE_EXECUTION_ERROR,
272
+ timestamp: new Date().toISOString(),
273
+ });
274
+ throw new Error(`Cannot complete child "${childId}" without a prior dispatch. ` + exports.INLINE_EXECUTION_ERROR);
275
+ }
276
+ }
277
+ /**
278
+ * Validate a state transition against the allowed transition graph.
279
+ *
280
+ * Returns an error message if the transition is illegal, or null if allowed.
281
+ * Emits an `illegal-state-transition` telemetry event on rejection.
282
+ * This function does NOT throw — the caller decides how to handle the error.
283
+ */
284
+ function validateTransition(from, to, runId, telemetryFile, childId) {
285
+ const allowed = exports.ALLOWED_TRANSITIONS.some(([f, t]) => f === from && t === to);
286
+ if (allowed)
287
+ return null;
288
+ const reason = `Illegal state transition: ${from} → ${to}. Only transitions through polaris loop dispatch → worker → polaris loop continue are allowed.`;
289
+ appendDispatchViolationEvent(telemetryFile, {
290
+ event: "illegal-state-transition",
291
+ run_id: runId,
292
+ child_id: childId,
293
+ from_state: from,
294
+ to_state: to,
295
+ reason,
296
+ timestamp: new Date().toISOString(),
297
+ });
298
+ return reason;
299
+ }
300
+ // ──────────────────────────────────────────────────────────────────────────────
301
+ // Dispatch boundary record builders
302
+ // ──────────────────────────────────────────────────────────────────────────────
303
+ /**
304
+ * Build an initial dispatch boundary record for a fresh state.
305
+ */
306
+ function initialDispatchBoundary() {
307
+ return { dispatch_epoch: 0, continue_epoch: 0, last_dispatched_child: null };
308
+ }
309
+ /**
310
+ * Return the updated dispatch_boundary after a dispatch call.
311
+ * Increments dispatch_epoch and records the dispatched child.
312
+ */
313
+ function advanceDispatchEpoch(current, childId) {
314
+ const base = current ?? initialDispatchBoundary();
315
+ return {
316
+ dispatch_epoch: base.dispatch_epoch + 1,
317
+ continue_epoch: base.continue_epoch,
318
+ last_dispatched_child: childId,
319
+ };
320
+ }
321
+ /**
322
+ * Return the updated dispatch_boundary after a successful continue call.
323
+ * Increments continue_epoch to match the consumed dispatch.
324
+ */
325
+ function advanceContinueEpoch(current) {
326
+ // If no boundary exists yet, return a fresh one without incrementing
327
+ if (current === undefined || current === null) {
328
+ return initialDispatchBoundary();
329
+ }
330
+ return {
331
+ dispatch_epoch: current.dispatch_epoch,
332
+ continue_epoch: current.continue_epoch + 1,
333
+ last_dispatched_child: current.last_dispatched_child,
334
+ };
335
+ }