@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,1566 @@
1
+ "use strict";
2
+ /**
3
+ * Scheduler-only parent loop.
4
+ *
5
+ * The parent loop is a pure scheduler: it reads cluster state, selects the
6
+ * next open child, dispatches a worker via the configured execution adapter,
7
+ * receives a compact return JSON, checks the budget, and loops or halts.
8
+ *
9
+ * The parent MUST NOT inline-execute child work. Every child is dispatched
10
+ * to an external worker. "ADAPTER HANDOFF" means dispatch + continue the loop;
11
+ * it is NOT a signal to halt the parent session.
12
+ *
13
+ * Loop steps:
14
+ * 01 – Load cluster / read current-state.json
15
+ * 02 – Select next open child (lowest-numbered, not Done/blocked)
16
+ * 03 – Dispatch worker via configured adapter
17
+ * 04 – Receive and validate compact worker return JSON
18
+ * 05 – Check budget policy
19
+ * 06 – CONTINUE (back to step 02) or halt (STOP / CLUSTER COMPLETE)
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.runParentLoop = runParentLoop;
23
+ const node_fs_1 = require("node:fs");
24
+ const node_crypto_1 = require("node:crypto");
25
+ const node_path_1 = require("node:path");
26
+ const git_custody_js_1 = require("./git-custody.js");
27
+ const checkpoint_js_1 = require("./checkpoint.js");
28
+ const registry_js_1 = require("./adapters/registry.js");
29
+ const budget_js_1 = require("./budget.js");
30
+ const loader_js_1 = require("../config/loader.js");
31
+ const store_js_1 = require("../cluster-state/store.js");
32
+ const lifecycle_js_1 = require("./lifecycle.js");
33
+ const worker_packet_js_1 = require("./worker-packet.js");
34
+ const worker_prompt_js_1 = require("./worker-prompt.js");
35
+ const body_parser_js_1 = require("./body-parser.js");
36
+ const node_child_process_1 = require("node:child_process");
37
+ const artifact_policy_js_1 = require("../finalize/artifact-policy.js");
38
+ const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
39
+ const run_bootstrap_js_1 = require("./run-bootstrap.js");
40
+ const ledger_js_1 = require("./ledger.js");
41
+ const dispatch_js_1 = require("./dispatch.js");
42
+ const index_js_1 = require("../tracker/index.js");
43
+ const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
44
+ const CLAIM_TTL_MS = 30 * 60 * 1000;
45
+ /**
46
+ * Returns the list of files touched by a git commit, or null when the commit
47
+ * cannot be resolved (short hash in a non-repo context, or non-existent object).
48
+ * Returns null (fail-open) for short/non-hex hashes so test environments using
49
+ * synthetic commit strings like "abc1234" are not blocked by the artifact gate.
50
+ * For full 40-char SHAs, resolves via git and returns null on any git failure.
51
+ */
52
+ function defaultGetCommitFiles(commit, repoRoot) {
53
+ if (!/^[0-9a-f]{40}$/i.test(commit)) {
54
+ // Short or non-hex hash — cannot verify without git resolution; skip check.
55
+ return null;
56
+ }
57
+ try {
58
+ // Resolve the ref to a commit OID, rejecting non-commit refs.
59
+ const resolvedCommit = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--verify", `${commit}^{commit}`], {
60
+ cwd: repoRoot,
61
+ encoding: "utf-8",
62
+ }).trim();
63
+ const output = (0, node_child_process_1.execFileSync)("git", ["show", "--name-only", "--format=", resolvedCommit], {
64
+ cwd: repoRoot,
65
+ encoding: "utf-8",
66
+ });
67
+ return output.trim().split("\n").filter(Boolean);
68
+ }
69
+ catch {
70
+ // Cannot resolve even a full-looking SHA — skip check rather than block.
71
+ return null;
72
+ }
73
+ }
74
+ /**
75
+ * Returns true when a parsed `## Goal` section text looks like placeholder
76
+ * or template content that has not been filled in.
77
+ * Impl-only gate — analyze children are exempted by their caller.
78
+ */
79
+ function isPlaceholderGoal(goal) {
80
+ const t = goal.trim().toLowerCase();
81
+ if (t.length === 0)
82
+ return true;
83
+ if (t === 'tbd')
84
+ return true;
85
+ if (t.startsWith('tbd ') || t.startsWith('tbd—') || t.startsWith('tbd -') || t.startsWith('tbd:'))
86
+ return true;
87
+ if (t === 'todo')
88
+ return true;
89
+ if (t.startsWith('todo:') || t.startsWith('todo '))
90
+ return true;
91
+ if (t === 'placeholder')
92
+ return true;
93
+ if (t.startsWith('[placeholder]') || t.startsWith('<<') || t.startsWith('<placeholder'))
94
+ return true;
95
+ return false;
96
+ }
97
+ function logStatus(format, message) {
98
+ if (format === "terse") {
99
+ process.stdout.write(`[POLARIS] ${message}\n`);
100
+ }
101
+ }
102
+ // ────────────────────────────────────────────────────────────────────────────
103
+ // Internal helpers
104
+ // ────────────────────────────────────────────────────────────────────────────
105
+ function appendTelemetry(telemetryFile, event) {
106
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
107
+ (0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
108
+ }
109
+ function resolveTelemetryFile(state, repoRoot) {
110
+ const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
111
+ return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
112
+ }
113
+ /**
114
+ * Select the next open child that is not Done/blocked.
115
+ * Returns null when all children are completed.
116
+ */
117
+ function selectNextChild(state) {
118
+ return state.open_children[0] ?? null;
119
+ }
120
+ /**
121
+ * Returns true when the given child is detected as an analyze issue.
122
+ * Detection is intentionally conservative: title prefix or label match only.
123
+ */
124
+ function isAnalyzeChild(childId, state) {
125
+ const meta = state.open_children_meta?.[childId];
126
+ if (!meta)
127
+ return false;
128
+ const title = meta.title ?? "";
129
+ if (title.startsWith("Analyze:") || title.startsWith("polaris-analyze"))
130
+ return true;
131
+ if (meta.labels?.includes("analyze"))
132
+ return true;
133
+ return false;
134
+ }
135
+ /**
136
+ * Returns true when the cluster root is an ANALYZE issue.
137
+ */
138
+ function isAnalyzeParent(state) {
139
+ const meta = state.open_children_meta?.[state.cluster_id];
140
+ if (!meta)
141
+ return false;
142
+ const title = meta.title ?? "";
143
+ if (title.startsWith("ANALYZE:"))
144
+ return true;
145
+ if (meta.labels?.includes("analyze"))
146
+ return true;
147
+ return false;
148
+ }
149
+ /** Normalise a file path to its canonical realpath (resolves macOS /var → /private/var symlinks). */
150
+ function canonicalPath(p) {
151
+ try {
152
+ return (0, node_fs_1.realpathSync)(p);
153
+ }
154
+ catch {
155
+ return p;
156
+ }
157
+ }
158
+ /** Returns the current git branch, or "unknown" on failure. */
159
+ function getCurrentBranch(cwd) {
160
+ try {
161
+ return (0, node_child_process_1.execFileSync)("git", ["branch", "--show-current"], {
162
+ cwd,
163
+ encoding: "utf-8",
164
+ }).trim();
165
+ }
166
+ catch {
167
+ return process.env["POLARIS_BRANCH"] ?? "unknown";
168
+ }
169
+ }
170
+ function normalizeRunType(sessionType) {
171
+ return sessionType === "analyze" ? "analyze" : "implement";
172
+ }
173
+ function ledgerLastCommit(state) {
174
+ return state.last_commit && state.last_commit.length > 0 ? state.last_commit : null;
175
+ }
176
+ function appendRunStartedLedgerEvent(repoRoot, state) {
177
+ new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH)).append({
178
+ schema_version: 1,
179
+ event_id: (0, node_crypto_1.randomUUID)(),
180
+ event: "run-started",
181
+ run_id: state.run_id,
182
+ run_type: normalizeRunType(state.session_type),
183
+ cluster_id: state.cluster_id,
184
+ issue_id: state.active_child || null,
185
+ branch: state.branch ?? getCurrentBranch(repoRoot),
186
+ status: "running",
187
+ completed_children: state.completed_children,
188
+ open_children: state.open_children,
189
+ next_child: state.next_open_child,
190
+ last_commit: ledgerLastCommit(state),
191
+ pr_url: null,
192
+ timestamp: new Date().toISOString(),
193
+ });
194
+ }
195
+ function appendChildCompletedLedgerEvent(repoRoot, state, completedChild, lastCommit, validation) {
196
+ const writer = new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH));
197
+ const base = {
198
+ schema_version: 1,
199
+ event_id: (0, node_crypto_1.randomUUID)(),
200
+ run_id: state.run_id,
201
+ run_type: normalizeRunType(state.session_type),
202
+ cluster_id: state.cluster_id,
203
+ branch: state.branch ?? getCurrentBranch(repoRoot),
204
+ completed_children: state.completed_children,
205
+ open_children: state.open_children,
206
+ next_child: state.next_open_child,
207
+ last_commit: ledgerLastCommit(state),
208
+ pr_url: null,
209
+ timestamp: new Date().toISOString(),
210
+ };
211
+ writer.append({
212
+ ...base,
213
+ event: "child-completed",
214
+ issue_id: completedChild,
215
+ status: state.status === "cluster-complete" ? "cluster-complete" : "running",
216
+ last_commit: lastCommit,
217
+ validation: typeof validation === "object" && validation !== null
218
+ ? { status: "complete", ...validation }
219
+ : { status: "complete" },
220
+ });
221
+ }
222
+ function appendClusterCompletedLedgerEvent(repoRoot, state) {
223
+ const writer = new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH));
224
+ const base = {
225
+ schema_version: 1,
226
+ event_id: (0, node_crypto_1.randomUUID)(),
227
+ run_id: state.run_id,
228
+ run_type: normalizeRunType(state.session_type),
229
+ cluster_id: state.cluster_id,
230
+ branch: state.branch ?? getCurrentBranch(repoRoot),
231
+ completed_children: state.completed_children,
232
+ open_children: state.open_children,
233
+ next_child: state.next_open_child,
234
+ last_commit: ledgerLastCommit(state),
235
+ pr_url: null,
236
+ timestamp: new Date().toISOString(),
237
+ };
238
+ writer.append({
239
+ ...base,
240
+ event_id: (0, node_crypto_1.randomUUID)(),
241
+ event: "cluster-complete",
242
+ issue_id: null,
243
+ status: "cluster-complete",
244
+ open_children: [],
245
+ next_child: null,
246
+ recovery_count: 0,
247
+ });
248
+ }
249
+ /**
250
+ * Builds the compiled WorkerPacket used to dispatch an implementation (impl) child.
251
+ *
252
+ * @param stateFile - Absolute path to the current-state.json file used by the worker
253
+ * @param telemetryFile - Path to the telemetry JSONL file the worker should append to
254
+ * @param repoRoot - Repository root used to resolve branch and paths
255
+ * @param resultFile - Optional override path where the worker should write its result
256
+ * @returns The compiled WorkerPacket configured for `activeChild`
257
+ */
258
+ function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, resultFile) {
259
+ const branch = state.branch ?? getCurrentBranch(repoRoot);
260
+ const childMeta = state.open_children_meta?.[activeChild];
261
+ // Hydrate body from cluster snapshot when runtime state lacks it.
262
+ const cachedBody = childMeta?.body;
263
+ const resolvedChildBody = (cachedBody && cachedBody.trim().length > 0)
264
+ ? cachedBody
265
+ : (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, activeChild, repoRoot);
266
+ const issueContext = (childMeta || resolvedChildBody)
267
+ ? {
268
+ id: activeChild,
269
+ title: childMeta?.title ?? activeChild,
270
+ key_requirements: [],
271
+ body: resolvedChildBody,
272
+ }
273
+ : undefined;
274
+ // Scope precedence: child body → parent/cluster-root body fallback.
275
+ // Derive here so compileImplPacket receives an authoritative scope list
276
+ // rather than falling back to [] when the child body has no scope section.
277
+ const childScope = resolvedChildBody ? (0, body_parser_js_1.parseIssueBody)(resolvedChildBody).scope : [];
278
+ const cachedParentBody = state.open_children_meta?.[state.cluster_id]?.body;
279
+ const parentBodyForScope = (cachedParentBody && cachedParentBody.trim().length > 0)
280
+ ? cachedParentBody
281
+ : (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, state.cluster_id, repoRoot) ?? '';
282
+ const resolvedScope = childScope.length > 0
283
+ ? childScope
284
+ : (0, body_parser_js_1.parseIssueBody)(parentBodyForScope).scope;
285
+ const promptMode = (0, worker_prompt_js_1.selectPromptMode)(activeChild, state);
286
+ return (0, worker_packet_js_1.compileImplPacket)({
287
+ runId: state.run_id,
288
+ clusterId: state.cluster_id,
289
+ childId: activeChild,
290
+ branch,
291
+ stateFile: canonicalPath(stateFile),
292
+ telemetryFile,
293
+ issueContext,
294
+ allowedScope: resolvedScope.length > 0 ? resolvedScope : undefined,
295
+ maxConcurrentWorkers: 1,
296
+ promptMode,
297
+ resultFile,
298
+ });
299
+ }
300
+ function absoluteResultFile(repoRoot, filePath) {
301
+ return (0, node_path_1.isAbsolute)(filePath) ? filePath : (0, node_path_1.resolve)(repoRoot, filePath);
302
+ }
303
+ function buildClusterArtifactPaths(repoRoot, clusterId, childId, dispatchId) {
304
+ const clusterDir = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId);
305
+ const filename = `${childId}-${dispatchId}.json`;
306
+ return {
307
+ packetPath: (0, node_path_1.join)(clusterDir, "packets", filename),
308
+ resultPath: (0, node_path_1.join)(clusterDir, "results", filename),
309
+ };
310
+ }
311
+ function writePacketArtifact(packetPath, packet) {
312
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(packetPath), { recursive: true });
313
+ (0, node_fs_1.writeFileSync)(packetPath, JSON.stringify(packet, null, 2), "utf-8");
314
+ }
315
+ function childResultFilePath(state, childId, repoRoot, dispatchId) {
316
+ const configured = state.open_children_meta?.[childId]?.result_file;
317
+ if (configured && configured.trim().length > 0) {
318
+ return absoluteResultFile(repoRoot, configured);
319
+ }
320
+ return buildClusterArtifactPaths(repoRoot, state.cluster_id, childId, dispatchId).resultPath;
321
+ }
322
+ function buildParentDispatchRecord(args) {
323
+ return {
324
+ dispatch_id: args.dispatchId,
325
+ child_id: args.childId,
326
+ run_id: args.runId,
327
+ cluster_id: args.clusterId,
328
+ packet_path: args.packetPath,
329
+ expected_result_path: args.resultPath,
330
+ provider: args.provider,
331
+ dispatched_at: args.dispatchedAt,
332
+ status: "dispatched",
333
+ dispatch_mode: "direct-worker",
334
+ runtime_state: "packet-created",
335
+ worker_id: args.workerId,
336
+ session_id: null,
337
+ attachment_capable: false,
338
+ role: "worker",
339
+ role_authority: "implementation",
340
+ may_implement: true,
341
+ session_type: "implementation",
342
+ };
343
+ }
344
+ function withChildDispatchMetadata(state, childId, resultFile, dispatchRecord) {
345
+ const current = state.open_children_meta?.[childId];
346
+ if (current?.result_file === resultFile &&
347
+ current.dispatch_record?.dispatch_id === dispatchRecord.dispatch_id) {
348
+ return state;
349
+ }
350
+ return {
351
+ ...state,
352
+ open_children_meta: {
353
+ ...(state.open_children_meta ?? {}),
354
+ [childId]: {
355
+ ...(current ?? {}),
356
+ result_file: resultFile,
357
+ dispatch_record: dispatchRecord,
358
+ },
359
+ },
360
+ };
361
+ }
362
+ /**
363
+ * Parse a compact worker return JSON from the dispatch result summary.
364
+ * Returns null if parsing fails.
365
+ */
366
+ function parseWorkerSummary(summary) {
367
+ if (!summary)
368
+ return null;
369
+ try {
370
+ const parsed = JSON.parse(summary);
371
+ if (typeof parsed === "object" && parsed !== null) {
372
+ return parsed;
373
+ }
374
+ return null;
375
+ }
376
+ catch {
377
+ return null;
378
+ }
379
+ }
380
+ /**
381
+ * Update state after a child completes successfully.
382
+ * Moves the child from open_children to completed_children.
383
+ */
384
+ function advanceState(state, completedChild, lastCommit) {
385
+ const remaining = state.open_children.filter((c) => c !== completedChild);
386
+ const completed = [...state.completed_children, completedChild];
387
+ return {
388
+ ...state,
389
+ active_child: "",
390
+ open_children: remaining,
391
+ completed_children: completed,
392
+ next_open_child: remaining[0] ?? null,
393
+ step_cursor: "checkpoint",
394
+ status: remaining.length > 0 ? "running" : "cluster-complete",
395
+ last_commit: lastCommit ?? state.last_commit,
396
+ context_budget: {
397
+ ...state.context_budget,
398
+ children_completed: completed.length,
399
+ },
400
+ };
401
+ }
402
+ async function syncClusterCompletion(args) {
403
+ const clusterState = await (0, store_js_1.readClusterState)(args.clusterId, args.repoRoot);
404
+ if (!clusterState) {
405
+ console.warn(`[polaris] syncClusterCompletion: no cluster state found for clusterId=${args.clusterId}; skipping sync`);
406
+ return false;
407
+ }
408
+ const childStates = clusterState.child_states.some((child) => child.id === args.childId)
409
+ ? clusterState.child_states.map((child) => child.id === args.childId
410
+ ? {
411
+ ...child,
412
+ status: "done",
413
+ commit: args.lastCommit ?? child.commit,
414
+ }
415
+ : child)
416
+ : [
417
+ ...clusterState.child_states,
418
+ {
419
+ id: args.childId,
420
+ status: "done",
421
+ ...(args.lastCommit ? { commit: args.lastCommit } : {}),
422
+ },
423
+ ];
424
+ const nextValidationResults = args.validationSummary === undefined
425
+ ? clusterState.validation_results
426
+ : {
427
+ ...clusterState.validation_results,
428
+ [args.childId]: {
429
+ passed: true,
430
+ output: String(args.validationSummary),
431
+ },
432
+ };
433
+ await (0, store_js_1.writeClusterState)(args.clusterId, {
434
+ ...clusterState,
435
+ state_generation: clusterState.state_generation + 1,
436
+ child_states: childStates,
437
+ claim_metadata: Object.fromEntries(Object.entries(clusterState.claim_metadata).filter(([childId]) => childId !== args.childId)),
438
+ result_pointers: args.resultFile
439
+ ? {
440
+ ...clusterState.result_pointers,
441
+ [args.childId]: args.resultFile,
442
+ }
443
+ : clusterState.result_pointers,
444
+ validation_results: nextValidationResults,
445
+ commits: args.lastCommit
446
+ ? {
447
+ ...clusterState.commits,
448
+ [args.childId]: args.lastCommit,
449
+ }
450
+ : clusterState.commits,
451
+ }, args.repoRoot);
452
+ return true;
453
+ }
454
+ async function syncClusterDispatch(args) {
455
+ const clusterState = await (0, store_js_1.readClusterState)(args.clusterId, args.repoRoot);
456
+ if (!clusterState) {
457
+ console.warn(`[polaris] syncClusterDispatch: no cluster state found for clusterId=${args.clusterId}; skipping sync`);
458
+ return false;
459
+ }
460
+ const dispatchedAtMs = new Date(args.dispatchedAt).getTime();
461
+ const expiresAt = new Date(Number.isFinite(dispatchedAtMs) ? dispatchedAtMs + CLAIM_TTL_MS : Date.now() + CLAIM_TTL_MS).toISOString();
462
+ const childStates = clusterState.child_states.some((child) => child.id === args.childId)
463
+ ? clusterState.child_states.map((child) => child.id === args.childId
464
+ ? {
465
+ ...child,
466
+ status: "dispatched",
467
+ }
468
+ : child)
469
+ : [...clusterState.child_states, { id: args.childId, status: "dispatched" }];
470
+ await (0, store_js_1.writeClusterState)(args.clusterId, {
471
+ ...clusterState,
472
+ state_generation: clusterState.state_generation + 1,
473
+ child_states: childStates,
474
+ claim_metadata: {
475
+ ...clusterState.claim_metadata,
476
+ [args.childId]: {
477
+ worker_id: args.workerId,
478
+ claimed_at: args.dispatchedAt,
479
+ expires_at: expiresAt,
480
+ },
481
+ },
482
+ packet_pointers: {
483
+ ...clusterState.packet_pointers,
484
+ [args.childId]: args.packetPath,
485
+ },
486
+ }, args.repoRoot);
487
+ return true;
488
+ }
489
+ // ────────────────────────────────────────────────────────────────────────────
490
+ // Main parent loop
491
+ // ────────────────────────────────────────────────────────────────────────────
492
+ /**
493
+ * Run the scheduler-only parent loop.
494
+ *
495
+ * This function is the entry point for the `polaris loop run` command and
496
+ * for programmatic use. It loops until a halt condition is reached, then
497
+ * returns a structured result describing why it halted.
498
+ */
499
+ async function runParentLoop(options) {
500
+ const { stateFile, repoRoot, dryRun = false, allowAnalyzeChildren: allowAnalyzeChildrenFlag = false } = options;
501
+ // getCommitFiles intentionally read via options.getCommitFiles in the gate below
502
+ // ── Step 01: Load cluster / read current-state.json ─────────────────────
503
+ let state;
504
+ try {
505
+ const rawState = (0, checkpoint_js_1.readState)(stateFile);
506
+ const errors = (0, checkpoint_js_1.validateState)(rawState);
507
+ if (errors.length > 0) {
508
+ return {
509
+ haltReason: 'state-invalid',
510
+ childrenDispatched: 0,
511
+ message: `current-state.json is invalid:\n${errors.join("\n")}`,
512
+ };
513
+ }
514
+ state = rawState;
515
+ }
516
+ catch (err) {
517
+ const msg = err instanceof Error ? err.message : String(err);
518
+ return {
519
+ haltReason: 'state-invalid',
520
+ childrenDispatched: 0,
521
+ message: `Cannot read state file ${stateFile}: ${msg}`,
522
+ };
523
+ }
524
+ if (isAnalyzeParent(state)) {
525
+ return {
526
+ haltReason: 'analyze-parent',
527
+ childrenDispatched: 0,
528
+ message: "polaris-run targets IMPLEMENT parents, not ANALYZE issues. Run polaris-analyze first to create an IMPLEMENT parent.",
529
+ };
530
+ }
531
+ // ── Bootstrap seal enforcement ─────────────────────────────────────────────
532
+ // The run MUST have been initialized through `polaris loop bootstrap`.
533
+ // Refuse to enter the dispatch loop if the state was hand-crafted.
534
+ const earlyTelemetry = resolveTelemetryFile(state, repoRoot);
535
+ try {
536
+ (0, run_bootstrap_js_1.assertBootstrapSeal)(state, earlyTelemetry);
537
+ }
538
+ catch (err) {
539
+ const msg = err instanceof Error ? err.message : String(err);
540
+ if (!dryRun) {
541
+ appendTelemetry(earlyTelemetry, {
542
+ event: "bootstrap-seal-missing",
543
+ run_id: state.run_id,
544
+ error: msg,
545
+ timestamp: new Date().toISOString(),
546
+ });
547
+ }
548
+ return {
549
+ haltReason: 'state-invalid',
550
+ childrenDispatched: 0,
551
+ message: msg,
552
+ };
553
+ }
554
+ if (!dryRun) {
555
+ appendRunStartedLedgerEvent(repoRoot, state);
556
+ }
557
+ // Load config for adapter and provider resolution
558
+ const config = (0, loader_js_1.loadConfig)(repoRoot);
559
+ const legacyOrchestrationMode = state["orchestration_mode"];
560
+ const legacyEphemeralMode = options.adapter === undefined && legacyOrchestrationMode === "ephemeral";
561
+ const orchestrationMode = config.orchestration?.mode ?? (legacyOrchestrationMode === "ephemeral" ? "auto" : "supervised");
562
+ const telemetryOrchestrationMode = legacyOrchestrationMode === "ephemeral" ? "ephemeral" : orchestrationMode;
563
+ const notificationFormat = config.orchestration?.notification_format ?? (orchestrationMode === 'auto' ? 'terse' : 'verbose');
564
+ const adapterName = legacyEphemeralMode ? "agent-subtask" : (options.adapter ?? config.execution?.adapter ?? "terminal-cli");
565
+ let providerName;
566
+ if (adapterName === "agent-subtask") {
567
+ providerName = "agent-subtask";
568
+ }
569
+ else {
570
+ let evidence;
571
+ try {
572
+ evidence = (0, dispatch_js_1.resolveProviderAndMode)({ stateFile, repoRoot, provider: options.provider }, "worker", config);
573
+ }
574
+ catch (err) {
575
+ const msg = err instanceof Error ? err.message : String(err);
576
+ return {
577
+ haltReason: "worker-error",
578
+ childrenDispatched: 0,
579
+ message: msg,
580
+ };
581
+ }
582
+ providerName = evidence.provider ?? "default";
583
+ }
584
+ const executionConfig = adapterName === "agent-subtask"
585
+ ? { ...(config.execution ?? { providers: {} }), adapter: "agent-subtask" }
586
+ : config.execution ?? { adapter: adapterName, providers: {} };
587
+ const adapter = (0, registry_js_1.createAdapter)(adapterName, executionConfig);
588
+ const budgetPolicy = (0, budget_js_1.policyFromConfig)(state.context_budget, config.budget);
589
+ const allowAnalyzeChildren = allowAnalyzeChildrenFlag || (config.budget?.allow_analyze_children === true);
590
+ const telemetryFile = resolveTelemetryFile(state, repoRoot);
591
+ // Enforce provider policy for explicit --provider flag before entering the loop.
592
+ // resolveProviderAndMode handles policy-filtered rotation; this gate blocks an
593
+ // explicit provider that the rotation resolution would not have chosen.
594
+ if (options.provider && adapterName !== "agent-subtask") {
595
+ try {
596
+ (0, dispatch_js_1.assertProviderAllowedForRole)("worker", options.provider, config.execution?.providerPolicy, telemetryFile, "parent-preflight", state.run_id, "pre-loop");
597
+ }
598
+ catch (err) {
599
+ const msg = err instanceof Error ? err.message : String(err);
600
+ return {
601
+ haltReason: "worker-error",
602
+ childrenDispatched: 0,
603
+ message: msg,
604
+ };
605
+ }
606
+ }
607
+ // ── Branch pre-flight ──────────────────────────────────────────────────────
608
+ // Ensure the working tree is on the cluster's delivery branch before any
609
+ // worker is dispatched. Without this, workers commit to whatever branch the
610
+ // operator left HEAD on (often main), bypassing delivery-branch custody and
611
+ // making PR creation impossible for those commits.
612
+ if (!dryRun && adapterName !== "agent-subtask") {
613
+ const deliveryBranch = state.branch;
614
+ if (deliveryBranch && deliveryBranch.trim().length > 0) {
615
+ const currentBranch = getCurrentBranch(repoRoot);
616
+ if (currentBranch !== "unknown" && currentBranch !== deliveryBranch) {
617
+ try {
618
+ (0, git_custody_js_1.ensureDeliveryBranch)(repoRoot, deliveryBranch);
619
+ }
620
+ catch (err) {
621
+ const msg = err instanceof Error ? err.message : String(err);
622
+ return {
623
+ haltReason: 'worker-error',
624
+ childrenDispatched: 0,
625
+ message: `Branch pre-flight: cannot switch to delivery branch "${deliveryBranch}" (current: "${currentBranch}"): ${msg}`,
626
+ };
627
+ }
628
+ }
629
+ }
630
+ }
631
+ let childrenDispatched = 0;
632
+ // ── Lifecycle manager: enforce one-active-worker policy ─────────────────
633
+ // forceReleaseAll() clears any orphaned registrations from a previous
634
+ // crashed session. Registrations are session-memory only; they are not
635
+ // persisted to disk, so a fresh loop start always begins clean.
636
+ const lifecycle = new lifecycle_js_1.WorkerLifecycleManager(1);
637
+ lifecycle.forceReleaseAll();
638
+ // ── Main dispatch loop ───────────────────────────────────────────────────
639
+ // eslint-disable-next-line no-constant-condition
640
+ while (true) {
641
+ // ── Step 02: Select next open child ─────────────────────────────────
642
+ const nextChild = selectNextChild(state);
643
+ if (nextChild === null) {
644
+ const autoFinalizeRequested = orchestrationMode === "auto" && config.orchestration?.auto_finalize === true;
645
+ // All children completed — write final state and halt
646
+ if (!dryRun) {
647
+ logStatus(notificationFormat, "COMPLETE");
648
+ const clusterCompleteState = { ...state, status: "cluster-complete" };
649
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, clusterCompleteState);
650
+ (0, checkpoint_js_1.writeStateAtomic)((0, node_path_1.join)(repoRoot, '.polaris', 'clusters', state.cluster_id, 'state.json'), clusterCompleteState);
651
+ appendTelemetry(telemetryFile, {
652
+ event: "cluster-complete",
653
+ run_id: state.run_id,
654
+ children_completed: state.completed_children.length,
655
+ timestamp: new Date().toISOString(),
656
+ });
657
+ appendClusterCompletedLedgerEvent(repoRoot, { ...state, status: "cluster-complete" });
658
+ if (autoFinalizeRequested) {
659
+ appendTelemetry(telemetryFile, {
660
+ event: "auto-finalize-requested",
661
+ run_id: state.run_id,
662
+ next_action: "polaris finalize run",
663
+ timestamp: new Date().toISOString(),
664
+ });
665
+ }
666
+ }
667
+ const messageSuffix = autoFinalizeRequested
668
+ ? " Auto-finalize handoff requested; run `polaris finalize run` to complete delivery."
669
+ : "";
670
+ return {
671
+ haltReason: 'cluster-complete',
672
+ childrenDispatched,
673
+ message: `Cluster complete. All ${state.completed_children.length} children dispatched.${messageSuffix}`,
674
+ };
675
+ }
676
+ // ── Step 02 (post-select): Analyze-drift guardrail ───────────────────
677
+ if (!allowAnalyzeChildren && isAnalyzeChild(nextChild, state)) {
678
+ const errMsg = [
679
+ `ERROR: Next child ${nextChild} is an analyze issue.`,
680
+ `Loop halted to prevent recursive analysis drift.`,
681
+ ``,
682
+ `To override:`,
683
+ ` polaris.config.json → budget.allow_analyze_children: true`,
684
+ ` or: polaris loop continue --allow-analyze-children`,
685
+ ].join("\n");
686
+ process.stderr.write(errMsg + "\n");
687
+ if (!dryRun) {
688
+ appendTelemetry(telemetryFile, {
689
+ event: "analyze-drift-halt",
690
+ run_id: state.run_id,
691
+ child_id: nextChild,
692
+ timestamp: new Date().toISOString(),
693
+ });
694
+ }
695
+ return {
696
+ haltReason: 'analyze-drift',
697
+ childrenDispatched,
698
+ haltingChild: nextChild,
699
+ message: errMsg,
700
+ };
701
+ }
702
+ // ── Step 05 (pre-dispatch): Check budget before dispatching ─────────
703
+ const budgetCheck = (0, budget_js_1.checkBudget)({
704
+ childrenCompleted: state.context_budget.children_completed,
705
+ policy: budgetPolicy,
706
+ });
707
+ if (budgetCheck.status === 'exhausted') {
708
+ // Write checkpoint before halting
709
+ if (!dryRun) {
710
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
711
+ ...state,
712
+ status: "budget-exhausted",
713
+ step_cursor: "budget-check",
714
+ next_open_child: nextChild,
715
+ });
716
+ appendTelemetry(telemetryFile, {
717
+ event: "budget-exhausted",
718
+ run_id: state.run_id,
719
+ children_completed: state.context_budget.children_completed,
720
+ next_child: nextChild,
721
+ reason: budgetCheck.reason,
722
+ timestamp: new Date().toISOString(),
723
+ });
724
+ }
725
+ return {
726
+ haltReason: 'budget-exhausted',
727
+ childrenDispatched,
728
+ haltingChild: nextChild,
729
+ message: budgetCheck.reason,
730
+ };
731
+ }
732
+ // ── Dispatch boundary guard ──────────────────────────────────────────────
733
+ //
734
+ // HARD CONSTRAINT: The parent/orchestrator MUST NOT execute child work
735
+ // inline. If active_child is already set, a previous dispatch was not
736
+ // properly completed — halt and require manual resolution.
737
+ //
738
+ // This guard also fires if the runtime somehow reached a child-execution
739
+ // state without a dispatch event (e.g. state corruption or inline attempt).
740
+ try {
741
+ (0, dispatch_boundary_js_1.assertNoActiveChildBeforeDispatch)(state, telemetryFile);
742
+ }
743
+ catch (err) {
744
+ const msg = err instanceof Error ? err.message : String(err);
745
+ if (!dryRun) {
746
+ appendTelemetry(telemetryFile, {
747
+ event: "invalid-inline-attempt",
748
+ run_id: state.run_id,
749
+ child_id: nextChild,
750
+ reason: msg,
751
+ timestamp: new Date().toISOString(),
752
+ });
753
+ }
754
+ return {
755
+ haltReason: 'worker-error',
756
+ childrenDispatched,
757
+ haltingChild: nextChild,
758
+ message: msg,
759
+ };
760
+ }
761
+ // ── Child body preflight gate ────────────────────────────────────────────
762
+ //
763
+ // HARD GATE: Every child MUST have a resolvable body/description before
764
+ // dispatch. Runtime state is checked first (cache); if absent, the durable
765
+ // cluster snapshot (.polaris/clusters/<id>/clusters.json) is the fallback.
766
+ // If neither source has a body, tell the operator to run tracker sync-in.
767
+ {
768
+ const cachedChildBody = state.open_children_meta?.[nextChild]?.body;
769
+ const childBody = (cachedChildBody && cachedChildBody.trim().length > 0)
770
+ ? cachedChildBody
771
+ : (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, nextChild, repoRoot);
772
+ if (!dryRun && (!childBody || childBody.trim().length === 0)) {
773
+ const errMsg = `Child ${nextChild} has no body/description; cannot generate actionable packet. ` +
774
+ `Run 'tracker sync-in ${state.cluster_id}' to populate issue body data from Linear.`;
775
+ appendTelemetry(telemetryFile, {
776
+ event: "preflight-body-missing",
777
+ run_id: state.run_id,
778
+ child_id: nextChild,
779
+ timestamp: new Date().toISOString(),
780
+ });
781
+ return {
782
+ haltReason: 'preflight-failed',
783
+ childrenDispatched,
784
+ haltingChild: nextChild,
785
+ message: errMsg,
786
+ };
787
+ }
788
+ }
789
+ // ── Child scope preflight gate ───────────────────────────────────────────
790
+ //
791
+ // HARD GATE: Every impl child MUST have a derivable allowed_scope before
792
+ // dispatch. Scope is parsed from the child's body "## Scope" / "## Expected
793
+ // code areas" section, with fallback to the cluster-root (parent) body.
794
+ // Both sources check runtime state first, then the cluster snapshot.
795
+ // A packet with empty allowed_scope would block the worker immediately —
796
+ // fail here with a clear error instead.
797
+ // Analyze children are exempt since they don't produce impl packets.
798
+ {
799
+ const cachedChildBody = state.open_children_meta?.[nextChild]?.body;
800
+ const childBody = (cachedChildBody && cachedChildBody.trim().length > 0)
801
+ ? cachedChildBody
802
+ : (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, nextChild, repoRoot) ?? '';
803
+ if (!dryRun && childBody.trim().length > 0 && !isAnalyzeChild(nextChild, state)) {
804
+ const { scope: childScope } = (0, body_parser_js_1.parseIssueBody)(childBody);
805
+ if (childScope.length === 0) {
806
+ // Fallback: check the cluster-root (parent) body for scope
807
+ const cachedParentBody = state.open_children_meta?.[state.cluster_id]?.body;
808
+ const parentBody = (cachedParentBody && cachedParentBody.trim().length > 0)
809
+ ? cachedParentBody
810
+ : (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, state.cluster_id, repoRoot) ?? '';
811
+ const { scope: parentScope } = (0, body_parser_js_1.parseIssueBody)(parentBody);
812
+ if (parentScope.length === 0) {
813
+ const errMsg = `Child ${nextChild} body has no scope section; cannot generate actionable packet. ` +
814
+ `Add a "## Scope" section with explicit repo paths or globs. ` +
815
+ `If scope is unknown, write "- TBD — BLOCKED: scope missing" and mark the issue as Blocked in Linear.`;
816
+ appendTelemetry(telemetryFile, {
817
+ event: "preflight-scope-missing",
818
+ run_id: state.run_id,
819
+ child_id: nextChild,
820
+ timestamp: new Date().toISOString(),
821
+ });
822
+ return {
823
+ haltReason: 'preflight-failed',
824
+ childrenDispatched,
825
+ haltingChild: nextChild,
826
+ message: errMsg,
827
+ };
828
+ }
829
+ }
830
+ }
831
+ }
832
+ // ── Placeholder primary goal gate ───────────────────────────────────────
833
+ //
834
+ // HARD GATE (impl only): The child's ## Goal section must not be empty or
835
+ // placeholder text. A placeholder goal means the issue was never properly
836
+ // authored — dispatching would produce meaningless or incorrect work.
837
+ // Analyze children are exempt since they are not implementation issues.
838
+ {
839
+ const cachedChildBodyForGoal = state.open_children_meta?.[nextChild]?.body;
840
+ const childBody = (cachedChildBodyForGoal && cachedChildBodyForGoal.trim().length > 0)
841
+ ? cachedChildBodyForGoal
842
+ : (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, nextChild, repoRoot) ?? '';
843
+ if (!dryRun && childBody.trim().length > 0 && !isAnalyzeChild(nextChild, state)) {
844
+ const { goal } = (0, body_parser_js_1.parseIssueBody)(childBody);
845
+ if (isPlaceholderGoal(goal)) {
846
+ const errMsg = `Child ${nextChild} body has a placeholder primary goal; cannot generate actionable packet. ` +
847
+ `Fill in the "## Goal" section with a specific, actionable objective.`;
848
+ appendTelemetry(telemetryFile, {
849
+ event: "preflight-placeholder-goal",
850
+ run_id: state.run_id,
851
+ child_id: nextChild,
852
+ timestamp: new Date().toISOString(),
853
+ });
854
+ return {
855
+ haltReason: 'preflight-failed',
856
+ childrenDispatched,
857
+ haltingChild: nextChild,
858
+ message: errMsg,
859
+ };
860
+ }
861
+ }
862
+ }
863
+ // ── Step 03: Dispatch worker via configured adapter ──────────────────
864
+ //
865
+ // ADAPTER HANDOFF semantics: dispatching the worker and then continuing
866
+ // the loop is the correct behaviour. The parent does NOT halt here.
867
+ // The worker runs the child in an external process/subtask, writes its
868
+ // result to current-state.json and telemetry.jsonl, then returns a
869
+ // compact JSON summary via stdout.
870
+ //
871
+ // Lifecycle: register before dispatch, release after result.
872
+ // Only one worker may be active at a time (maxConcurrentWorkers = 1).
873
+ //
874
+ // Record the dispatch in state BEFORE calling the adapter so that
875
+ // dispatch_boundary.dispatch_epoch is always incremented before any
876
+ // worker execution occurs. This ensures the "dispatched → completed"
877
+ // transition is always verifiable from state alone.
878
+ const dispatchId = (0, node_crypto_1.randomUUID)();
879
+ const workerId = `${state.run_id}:${nextChild}:${Date.now()}`;
880
+ const dispatchedAt = new Date().toISOString();
881
+ const resultFile = childResultFilePath(state, nextChild, repoRoot, dispatchId);
882
+ const packetPath = buildClusterArtifactPaths(repoRoot, state.cluster_id, nextChild, dispatchId).packetPath;
883
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(resultFile), { recursive: true });
884
+ const statePreDispatch = state;
885
+ let stateBeforeDispatch = state;
886
+ const packet = buildPacket(state, nextChild, stateFile, telemetryFile, repoRoot, resultFile);
887
+ if (!dryRun) {
888
+ try {
889
+ writePacketArtifact(packetPath, packet);
890
+ }
891
+ catch (err) {
892
+ const msg = err instanceof Error ? err.message : String(err);
893
+ appendTelemetry(telemetryFile, {
894
+ event: "child-dispatched-error",
895
+ run_id: state.run_id,
896
+ child_id: nextChild,
897
+ worker_id: workerId,
898
+ error: `Failed to write packet artifact: ${msg}`,
899
+ timestamp: new Date().toISOString(),
900
+ });
901
+ return {
902
+ haltReason: 'worker-error',
903
+ childrenDispatched,
904
+ haltingChild: nextChild,
905
+ message: `Failed to write packet artifact for ${nextChild}: ${msg}`,
906
+ };
907
+ }
908
+ const dispatchRecord = buildParentDispatchRecord({
909
+ dispatchId,
910
+ runId: state.run_id,
911
+ clusterId: state.cluster_id,
912
+ childId: nextChild,
913
+ packetPath,
914
+ resultPath: resultFile,
915
+ provider: providerName,
916
+ workerId,
917
+ dispatchedAt,
918
+ });
919
+ const stateWithDispatch = {
920
+ ...withChildDispatchMetadata(state, nextChild, resultFile, dispatchRecord),
921
+ active_child: nextChild,
922
+ step_cursor: "dispatch",
923
+ dispatch_boundary: (0, dispatch_boundary_js_1.advanceDispatchEpoch)(state.dispatch_boundary, nextChild),
924
+ };
925
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, stateWithDispatch);
926
+ state = stateWithDispatch;
927
+ stateBeforeDispatch = stateWithDispatch;
928
+ try {
929
+ await syncClusterDispatch({
930
+ clusterId: state.cluster_id,
931
+ childId: nextChild,
932
+ repoRoot,
933
+ packetPath,
934
+ workerId,
935
+ dispatchedAt,
936
+ });
937
+ }
938
+ catch (err) {
939
+ const msg = err instanceof Error ? err.message : String(err);
940
+ appendTelemetry(telemetryFile, {
941
+ event: "child-dispatched-error",
942
+ run_id: state.run_id,
943
+ child_id: nextChild,
944
+ worker_id: workerId,
945
+ error: `Failed to sync cluster dispatch state: ${msg}`,
946
+ timestamp: new Date().toISOString(),
947
+ });
948
+ return {
949
+ haltReason: 'worker-error',
950
+ childrenDispatched,
951
+ haltingChild: nextChild,
952
+ message: `Failed to sync cluster dispatch state for ${nextChild}: ${msg}`,
953
+ };
954
+ }
955
+ }
956
+ const childrenCompletedBeforeDispatch = state.context_budget.children_completed;
957
+ // Register the worker slot before dispatch.
958
+ try {
959
+ lifecycle.register(workerId, nextChild, 'impl');
960
+ }
961
+ catch (err) {
962
+ const msg = err instanceof Error ? err.message : String(err);
963
+ return {
964
+ haltReason: 'worker-error',
965
+ childrenDispatched,
966
+ haltingChild: nextChild,
967
+ message: `Lifecycle slot unavailable for ${nextChild}: ${msg}`,
968
+ };
969
+ }
970
+ if (!dryRun) {
971
+ const totalChildren = state.open_children.length + state.completed_children.length;
972
+ const childIndex = state.completed_children.length + 1;
973
+ logStatus(notificationFormat, `RUNNING ${nextChild} (${childIndex}/${totalChildren})`);
974
+ appendTelemetry(telemetryFile, {
975
+ event: "child-dispatched",
976
+ run_id: state.run_id,
977
+ child_id: nextChild,
978
+ worker_id: workerId,
979
+ adapter: adapterName,
980
+ orchestration_mode: telemetryOrchestrationMode,
981
+ provider: providerName,
982
+ prompt_mode: packet.prompt_mode,
983
+ prompt_estimated_tokens: packet.prompt_metrics.estimated_tokens,
984
+ packet_path: packetPath,
985
+ expected_result_path: resultFile,
986
+ dry_run: dryRun,
987
+ timestamp: new Date().toISOString(),
988
+ });
989
+ }
990
+ let dispatchResult;
991
+ try {
992
+ dispatchResult = await adapter.dispatch(packet, { provider: providerName, dryRun });
993
+ }
994
+ catch (err) {
995
+ // Release slot on dispatch failure so the parent can retry or halt cleanly.
996
+ lifecycle.release(workerId);
997
+ const msg = err instanceof Error ? err.message : String(err);
998
+ if (!dryRun) {
999
+ appendTelemetry(telemetryFile, {
1000
+ event: "child-dispatched-error",
1001
+ run_id: state.run_id,
1002
+ child_id: nextChild,
1003
+ worker_id: workerId,
1004
+ error: msg,
1005
+ timestamp: new Date().toISOString(),
1006
+ });
1007
+ }
1008
+ return {
1009
+ haltReason: 'worker-error',
1010
+ childrenDispatched,
1011
+ haltingChild: nextChild,
1012
+ message: `Adapter "${adapterName}" threw during dispatch of ${nextChild}: ${msg}`,
1013
+ };
1014
+ }
1015
+ // Release the lifecycle slot — worker has returned its result.
1016
+ lifecycle.release(workerId);
1017
+ // ── Step 04: Receive and validate compact worker return JSON ─────────
1018
+ let finalWorkerSummary = null;
1019
+ // Pre-dispatch failure: adapter returned before launching any worker.
1020
+ // No result file was written. Roll back dispatch state so the run is
1021
+ // cleanly resumable without manual `loop abort` intervention.
1022
+ if (dispatchResult.pre_dispatch_failure && !dryRun) {
1023
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, statePreDispatch);
1024
+ appendTelemetry(telemetryFile, {
1025
+ event: "pre-dispatch-failure",
1026
+ run_id: state.run_id,
1027
+ child_id: nextChild,
1028
+ adapter: adapterName,
1029
+ error: dispatchResult.summary ?? "adapter returned pre-dispatch failure",
1030
+ timestamp: new Date().toISOString(),
1031
+ });
1032
+ return {
1033
+ haltReason: 'worker-error',
1034
+ childrenDispatched,
1035
+ haltingChild: nextChild,
1036
+ message: `Adapter "${adapterName}" cannot dispatch ${nextChild}: ${dispatchResult.summary ?? "no dispatcher available"}`,
1037
+ };
1038
+ }
1039
+ // Only read the sealed result file when the adapter reports success.
1040
+ // A non-zero exit_code means the worker never produced a sealed result —
1041
+ // attempting readFileSync would cause ENOENT.
1042
+ if (dispatchResult.exit_code === 0 && !dryRun) {
1043
+ try {
1044
+ const sealedFileContent = (0, node_fs_1.readFileSync)(packet.result_file_contract.result_file, 'utf-8');
1045
+ const parsed = JSON.parse(sealedFileContent);
1046
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
1047
+ throw new Error(`Sealed result file has unexpected shape (expected object, got ${Array.isArray(parsed) ? 'array' : typeof parsed})`);
1048
+ }
1049
+ const sealedResult = parsed;
1050
+ // Translate SealedWorkerResult status values to WorkerSummary status values.
1051
+ // SealedWorkerResult uses "success"/"failure"; WorkerSummary uses "done"/"failed".
1052
+ if (sealedResult['status'] === 'success') {
1053
+ sealedResult['status'] = 'done';
1054
+ }
1055
+ else if (sealedResult['status'] === 'failure') {
1056
+ sealedResult['status'] = 'failed';
1057
+ }
1058
+ finalWorkerSummary = sealedResult;
1059
+ }
1060
+ catch (error) {
1061
+ const msg = error instanceof Error ? error.message : String(error);
1062
+ if (!dryRun) {
1063
+ appendTelemetry(telemetryFile, {
1064
+ event: "sealed-result-read-error",
1065
+ run_id: state.run_id,
1066
+ child_id: nextChild,
1067
+ error: msg,
1068
+ result_file: packet.result_file_contract.result_file,
1069
+ timestamp: new Date().toISOString(),
1070
+ });
1071
+ }
1072
+ return {
1073
+ haltReason: 'worker-error',
1074
+ childrenDispatched,
1075
+ haltingChild: nextChild,
1076
+ message: `Failed to read sealed result file for ${nextChild}: ${msg}`,
1077
+ };
1078
+ }
1079
+ }
1080
+ else {
1081
+ finalWorkerSummary = parseWorkerSummary(dispatchResult.summary);
1082
+ }
1083
+ const workerStatus = finalWorkerSummary?.status ?? (dispatchResult.exit_code === 0 ? 'done' : 'error');
1084
+ // Verify active_child or child_id matches if present in worker summary
1085
+ const summaryAsRecord = finalWorkerSummary;
1086
+ const summaryChild = summaryAsRecord?.['active_child'] ?? summaryAsRecord?.['child_id'];
1087
+ if (summaryChild !== undefined && summaryChild !== nextChild) {
1088
+ const errMsg = `Worker returned mismatched child identifier: expected ${nextChild}, got ${String(summaryChild)}`;
1089
+ if (!dryRun) {
1090
+ appendTelemetry(telemetryFile, {
1091
+ event: "child-error",
1092
+ run_id: state.run_id,
1093
+ child_id: nextChild,
1094
+ error: errMsg,
1095
+ timestamp: new Date().toISOString(),
1096
+ });
1097
+ }
1098
+ return {
1099
+ haltReason: 'worker-error',
1100
+ childrenDispatched,
1101
+ haltingChild: nextChild,
1102
+ message: errMsg,
1103
+ };
1104
+ }
1105
+ if (workerStatus === 'blocked') {
1106
+ const blockerMsg = finalWorkerSummary?.['blocker'] ??
1107
+ dispatchResult.summary ??
1108
+ `Worker reported blocked for ${nextChild}`;
1109
+ if (!dryRun) {
1110
+ logStatus(notificationFormat, `BLOCKED ${nextChild}: ${blockerMsg.replace(/\n/g, ' ')}`);
1111
+ appendTelemetry(telemetryFile, {
1112
+ event: "child-blocked",
1113
+ run_id: state.run_id,
1114
+ child_id: nextChild,
1115
+ blocker: blockerMsg.replace(/\n/g, ' '),
1116
+ timestamp: new Date().toISOString(),
1117
+ });
1118
+ // Write checkpoint with blocker information
1119
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
1120
+ ...state,
1121
+ status: "blocked",
1122
+ step_cursor: "blocked",
1123
+ blocker: {
1124
+ reason: typeof blockerMsg === 'string' ? blockerMsg : String(blockerMsg),
1125
+ child_id: nextChild,
1126
+ timestamp: new Date().toISOString(),
1127
+ resolved: false,
1128
+ },
1129
+ });
1130
+ // ── Apply lifecycle transition for child-triage-required event ─────────
1131
+ // This is fire-and-forget: must not block on tracker mutations.
1132
+ // Errors are logged to telemetry but do not fail the halt.
1133
+ let adapter;
1134
+ try {
1135
+ adapter = (0, index_js_1.loadTrackerAdapter)(config);
1136
+ }
1137
+ catch (err) {
1138
+ const errorMsg = err instanceof Error ? err.message : String(err);
1139
+ appendTelemetry(telemetryFile, {
1140
+ event: "lifecycle-transition-error",
1141
+ run_id: state.run_id,
1142
+ child_id: nextChild,
1143
+ transition_event: "child-triage-required",
1144
+ error: `Failed to load tracker adapter: ${errorMsg}`,
1145
+ timestamp: new Date().toISOString(),
1146
+ });
1147
+ adapter = null;
1148
+ }
1149
+ const lifecyclePolicy = config.tracker?.lifecyclePolicy;
1150
+ if (adapter || lifecyclePolicy) {
1151
+ try {
1152
+ const transitionService = new lifecycle_transition_js_1.LifecycleTransitionService();
1153
+ transitionService
1154
+ .applyTransitionSafe({
1155
+ adapter,
1156
+ policy: lifecyclePolicy,
1157
+ taskId: nextChild,
1158
+ event: "child-triage-required",
1159
+ evidence: {
1160
+ error: blockerMsg,
1161
+ },
1162
+ timestamp: new Date().toISOString(),
1163
+ })
1164
+ .then((result) => {
1165
+ appendTelemetry(telemetryFile, {
1166
+ event: "lifecycle-transition-attempt",
1167
+ run_id: state.run_id,
1168
+ child_id: nextChild,
1169
+ transition_event: result.event,
1170
+ target_state: result.targetState,
1171
+ applied: result.applied,
1172
+ skipped: result.skipped,
1173
+ skip_reason: result.skipReason,
1174
+ error: result.error,
1175
+ timestamp: result.timestamp,
1176
+ });
1177
+ })
1178
+ .catch((err) => {
1179
+ const errorMsg = err instanceof Error ? err.message : String(err);
1180
+ appendTelemetry(telemetryFile, {
1181
+ event: "lifecycle-transition-error",
1182
+ run_id: state.run_id,
1183
+ child_id: nextChild,
1184
+ transition_event: "child-triage-required",
1185
+ error: errorMsg,
1186
+ timestamp: new Date().toISOString(),
1187
+ });
1188
+ });
1189
+ }
1190
+ catch (err) {
1191
+ const errorMsg = err instanceof Error ? err.message : String(err);
1192
+ appendTelemetry(telemetryFile, {
1193
+ event: "lifecycle-transition-error",
1194
+ run_id: state.run_id,
1195
+ child_id: nextChild,
1196
+ transition_event: "child-triage-required",
1197
+ error: `Failed to create lifecycle transition service: ${errorMsg}`,
1198
+ timestamp: new Date().toISOString(),
1199
+ });
1200
+ }
1201
+ }
1202
+ }
1203
+ return {
1204
+ haltReason: 'blocked',
1205
+ childrenDispatched,
1206
+ haltingChild: nextChild,
1207
+ message: `Child ${nextChild} is blocked: ${typeof blockerMsg === 'string' ? blockerMsg : String(blockerMsg)}`,
1208
+ };
1209
+ }
1210
+ // 'failed' is the CompactReturn terminal failure status; 'error' is the adapter-level status.
1211
+ // Both map to worker-error halt — the exit_code check catches exit=1 from runWorker().
1212
+ if (workerStatus === 'error' || workerStatus === 'failed' || dispatchResult.exit_code !== 0) {
1213
+ const errMsg = dispatchResult.summary ?? `Worker exited with code ${dispatchResult.exit_code}`;
1214
+ if (!dryRun) {
1215
+ appendTelemetry(telemetryFile, {
1216
+ event: "child-error",
1217
+ run_id: state.run_id,
1218
+ child_id: nextChild,
1219
+ exit_code: dispatchResult.exit_code,
1220
+ summary: errMsg,
1221
+ timestamp: new Date().toISOString(),
1222
+ });
1223
+ }
1224
+ return {
1225
+ haltReason: 'worker-error',
1226
+ childrenDispatched,
1227
+ haltingChild: nextChild,
1228
+ message: `Worker for ${nextChild} failed (exit ${dispatchResult.exit_code}): ${errMsg}`,
1229
+ };
1230
+ }
1231
+ // Only treat 'done' as successful completion; treat unknown statuses as errors
1232
+ if (workerStatus !== 'done') {
1233
+ const errMsg = `Worker returned unexpected status: ${workerStatus}`;
1234
+ if (!dryRun) {
1235
+ appendTelemetry(telemetryFile, {
1236
+ event: "child-error",
1237
+ run_id: state.run_id,
1238
+ child_id: nextChild,
1239
+ error: errMsg,
1240
+ timestamp: new Date().toISOString(),
1241
+ });
1242
+ }
1243
+ return {
1244
+ haltReason: 'worker-error',
1245
+ childrenDispatched,
1246
+ haltingChild: nextChild,
1247
+ message: `Worker for ${nextChild} returned unexpected status: ${workerStatus}`,
1248
+ };
1249
+ }
1250
+ // Worker completed successfully — reload state from disk before advancing
1251
+ // The worker may have updated current-state.json; reload to avoid clobbering
1252
+ let reloadedStateValid = false;
1253
+ try {
1254
+ const reloadedState = (0, checkpoint_js_1.readState)(stateFile);
1255
+ const reloadErrors = (0, checkpoint_js_1.validateState)(reloadedState);
1256
+ if (reloadErrors.length > 0) {
1257
+ const errMsg = `State file corrupted after worker execution:\n${reloadErrors.join("\n")}`;
1258
+ if (!dryRun && adapterName === "terminal-cli") {
1259
+ appendTelemetry(telemetryFile, {
1260
+ event: "state-reload-fallback",
1261
+ run_id: state.run_id,
1262
+ child_id: nextChild,
1263
+ error: errMsg,
1264
+ timestamp: new Date().toISOString(),
1265
+ });
1266
+ state = stateBeforeDispatch;
1267
+ }
1268
+ else if (!dryRun) {
1269
+ appendTelemetry(telemetryFile, {
1270
+ event: "state-reload-error",
1271
+ run_id: state.run_id,
1272
+ child_id: nextChild,
1273
+ error: errMsg,
1274
+ timestamp: new Date().toISOString(),
1275
+ });
1276
+ return {
1277
+ haltReason: 'state-invalid',
1278
+ childrenDispatched,
1279
+ haltingChild: nextChild,
1280
+ message: errMsg,
1281
+ };
1282
+ }
1283
+ }
1284
+ else {
1285
+ state = reloadedState;
1286
+ reloadedStateValid = true;
1287
+ }
1288
+ }
1289
+ catch (err) {
1290
+ const msg = err instanceof Error ? err.message : String(err);
1291
+ const errMsg = `Failed to reload state after worker execution: ${msg}`;
1292
+ if (!dryRun && adapterName === "terminal-cli") {
1293
+ appendTelemetry(telemetryFile, {
1294
+ event: "state-reload-fallback",
1295
+ run_id: state.run_id,
1296
+ child_id: nextChild,
1297
+ error: errMsg,
1298
+ timestamp: new Date().toISOString(),
1299
+ });
1300
+ state = stateBeforeDispatch;
1301
+ }
1302
+ else if (!dryRun) {
1303
+ appendTelemetry(telemetryFile, {
1304
+ event: "state-reload-error",
1305
+ run_id: state.run_id,
1306
+ child_id: nextChild,
1307
+ error: errMsg,
1308
+ timestamp: new Date().toISOString(),
1309
+ });
1310
+ return {
1311
+ haltReason: 'state-invalid',
1312
+ childrenDispatched,
1313
+ haltingChild: nextChild,
1314
+ message: errMsg,
1315
+ };
1316
+ }
1317
+ }
1318
+ const workerWroteCompletion = reloadedStateValid &&
1319
+ (state.completed_children.includes(nextChild) ||
1320
+ state.context_budget.children_completed > childrenCompletedBeforeDispatch);
1321
+ const lastCommit = finalWorkerSummary?.['commit'] ??
1322
+ finalWorkerSummary?.['commit_hash'];
1323
+ const validationSummary = finalWorkerSummary?.['validation'] ??
1324
+ finalWorkerSummary?.['validation_summary'];
1325
+ if (!dryRun && workerStatus === 'done' && (!lastCommit || lastCommit.trim().length === 0)) {
1326
+ const errMsg = `Worker reported done for ${nextChild} without commit evidence`;
1327
+ appendTelemetry(telemetryFile, {
1328
+ event: "child-error",
1329
+ run_id: state.run_id,
1330
+ child_id: nextChild,
1331
+ error: errMsg,
1332
+ result_file: packet.result_file_contract.result_file,
1333
+ timestamp: new Date().toISOString(),
1334
+ });
1335
+ return {
1336
+ haltReason: 'worker-error',
1337
+ childrenDispatched,
1338
+ haltingChild: nextChild,
1339
+ message: errMsg,
1340
+ };
1341
+ }
1342
+ // ── Artifact-only commit gate ────────────────────────────────────────────
1343
+ //
1344
+ // HARD GATE: A worker commit that touches only Polaris artifact files
1345
+ // (.polaris/**, .taskchain_artifacts/**) indicates no real implementation
1346
+ // work was done — reject it to prevent phantom completions.
1347
+ // Uses getCommitFiles from options for testability; defaults to a git-backed
1348
+ // implementation that throws when the commit cannot be resolved (fail closed).
1349
+ let commitFiles = null;
1350
+ if (!dryRun && workerStatus === 'done' && lastCommit && lastCommit.trim().length > 0) {
1351
+ const getFiles = options.getCommitFiles ?? defaultGetCommitFiles;
1352
+ try {
1353
+ commitFiles = getFiles(lastCommit, repoRoot);
1354
+ if (workerWroteCompletion && commitFiles === null) {
1355
+ const errMsg = `Cannot verify worker commit ${lastCommit} for ${nextChild}: commit does not resolve to a git object`;
1356
+ appendTelemetry(telemetryFile, {
1357
+ event: "child-error",
1358
+ run_id: state.run_id,
1359
+ child_id: nextChild,
1360
+ error: errMsg,
1361
+ commit: lastCommit,
1362
+ timestamp: new Date().toISOString(),
1363
+ });
1364
+ return {
1365
+ haltReason: 'worker-error',
1366
+ childrenDispatched,
1367
+ haltingChild: nextChild,
1368
+ message: errMsg,
1369
+ };
1370
+ }
1371
+ if (commitFiles !== null) {
1372
+ const nonArtifact = commitFiles.filter((f) => (0, artifact_policy_js_1.classifyArtifactPath)(f, state.cluster_id) === 'non-artifact');
1373
+ if (nonArtifact.length === 0) {
1374
+ const errMsg = `Worker commit ${lastCommit} for ${nextChild} contains only artifact files; no implementation evidence found`;
1375
+ appendTelemetry(telemetryFile, {
1376
+ event: "child-error",
1377
+ run_id: state.run_id,
1378
+ child_id: nextChild,
1379
+ error: errMsg,
1380
+ commit: lastCommit,
1381
+ timestamp: new Date().toISOString(),
1382
+ });
1383
+ return {
1384
+ haltReason: 'worker-error',
1385
+ childrenDispatched,
1386
+ haltingChild: nextChild,
1387
+ message: errMsg,
1388
+ };
1389
+ }
1390
+ }
1391
+ // null → skip check (fail-open for unresolvable commits)
1392
+ }
1393
+ catch (err) {
1394
+ const msg = err instanceof Error ? err.message : String(err);
1395
+ const errMsg = `Cannot verify worker commit ${lastCommit} for ${nextChild}: ${msg}`;
1396
+ appendTelemetry(telemetryFile, {
1397
+ event: "child-error",
1398
+ run_id: state.run_id,
1399
+ child_id: nextChild,
1400
+ error: errMsg,
1401
+ commit: lastCommit,
1402
+ timestamp: new Date().toISOString(),
1403
+ });
1404
+ return {
1405
+ haltReason: 'worker-error',
1406
+ childrenDispatched,
1407
+ haltingChild: nextChild,
1408
+ message: errMsg,
1409
+ };
1410
+ }
1411
+ }
1412
+ // workerStatus === "done" has already been validated upstream.
1413
+ // If the reloaded state already reflects the completed child,
1414
+ // the worker owns the completion checkpoint and the parent
1415
+ // must not rewrite it.
1416
+ if (!workerWroteCompletion) {
1417
+ // ── Dispatch boundary: verify dispatch happened before advancing ──────
1418
+ // The parent dispatched via adapter, so dispatch_boundary should show
1419
+ // dispatch_epoch > continue_epoch. If not, the state is inconsistent
1420
+ // and we must not complete the child (it would be inline completion).
1421
+ if (!dryRun) {
1422
+ try {
1423
+ (0, dispatch_boundary_js_1.assertDispatchedBeforeCompletion)(state, nextChild, telemetryFile);
1424
+ }
1425
+ catch (err) {
1426
+ const msg = err instanceof Error ? err.message : String(err);
1427
+ appendTelemetry(telemetryFile, {
1428
+ event: "illegal-state-transition",
1429
+ run_id: state.run_id,
1430
+ child_id: nextChild,
1431
+ error: msg,
1432
+ timestamp: new Date().toISOString(),
1433
+ });
1434
+ return {
1435
+ haltReason: 'worker-error',
1436
+ childrenDispatched,
1437
+ haltingChild: nextChild,
1438
+ message: msg,
1439
+ };
1440
+ }
1441
+ }
1442
+ // Advance state, including continue_epoch to match the consumed dispatch
1443
+ const advanced = advanceState(state, nextChild, lastCommit);
1444
+ state = {
1445
+ ...advanced,
1446
+ dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
1447
+ };
1448
+ childrenDispatched += 1;
1449
+ // Worker did not write its own completion — orchestrator fills the gap.
1450
+ if (!dryRun) {
1451
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, state);
1452
+ }
1453
+ }
1454
+ else {
1455
+ // Worker wrote its own completion — advance continue_epoch to stay in sync.
1456
+ // The worker does not manage dispatch_boundary, so the parent must do it.
1457
+ state = {
1458
+ ...state,
1459
+ dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
1460
+ };
1461
+ childrenDispatched += 1;
1462
+ if (!dryRun) {
1463
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, state);
1464
+ }
1465
+ }
1466
+ // Orchestrator checkpoint event — always emitted after a successful child.
1467
+ if (!dryRun) {
1468
+ const commitSuffix = lastCommit && lastCommit.length > 0 ? ` (commit: ${lastCommit})` : "";
1469
+ try {
1470
+ const synced = await syncClusterCompletion({
1471
+ clusterId: state.cluster_id,
1472
+ childId: nextChild,
1473
+ repoRoot,
1474
+ resultFile: packet.result_file_contract.result_file,
1475
+ validationSummary,
1476
+ lastCommit,
1477
+ });
1478
+ if (!synced) {
1479
+ appendTelemetry(telemetryFile, {
1480
+ event: "cluster-state-sync-skipped",
1481
+ run_id: state.run_id,
1482
+ child_id: nextChild,
1483
+ reason: "no cluster state found",
1484
+ timestamp: new Date().toISOString(),
1485
+ });
1486
+ }
1487
+ }
1488
+ catch (error) {
1489
+ const message = error instanceof Error ? error.message : String(error);
1490
+ appendTelemetry(telemetryFile, {
1491
+ event: "cluster-state-sync-error",
1492
+ run_id: state.run_id,
1493
+ child_id: nextChild,
1494
+ error: message,
1495
+ timestamp: new Date().toISOString(),
1496
+ });
1497
+ return {
1498
+ haltReason: "state-invalid",
1499
+ childrenDispatched,
1500
+ haltingChild: nextChild,
1501
+ message: `Failed to sync cluster-state.json for ${nextChild}: ${message}`,
1502
+ };
1503
+ }
1504
+ logStatus(notificationFormat, `COMPLETE ${nextChild}${commitSuffix}`);
1505
+ // Compute elapsed_seconds from dispatch_record.dispatched_at if available
1506
+ const dispatchedAt = state.open_children_meta?.[nextChild]?.dispatch_record?.dispatched_at;
1507
+ const elapsedSeconds = dispatchedAt
1508
+ ? Math.floor((Date.now() - new Date(dispatchedAt).getTime()) / 1000)
1509
+ : undefined;
1510
+ const telemetryEvent = {
1511
+ event: "child-complete",
1512
+ run_id: state.run_id,
1513
+ child_id: nextChild,
1514
+ children_completed: state.context_budget.children_completed,
1515
+ validation_summary: validationSummary,
1516
+ commit_hash: lastCommit,
1517
+ commit_files: commitFiles,
1518
+ timestamp: new Date().toISOString(),
1519
+ };
1520
+ if (elapsedSeconds !== undefined) {
1521
+ telemetryEvent.elapsed_seconds = elapsedSeconds;
1522
+ }
1523
+ appendTelemetry(telemetryFile, telemetryEvent);
1524
+ appendChildCompletedLedgerEvent(repoRoot, state, nextChild, lastCommit ?? null, validationSummary);
1525
+ }
1526
+ if (orchestrationMode === 'supervised') {
1527
+ return {
1528
+ haltReason: 'supervised-mode-child-complete',
1529
+ childrenDispatched,
1530
+ message: `Child ${nextChild} complete. Re-run to continue.`,
1531
+ };
1532
+ }
1533
+ // ── Step 05 (post-dispatch): Re-check budget before next iteration ───
1534
+ const postBudgetCheck = (0, budget_js_1.checkBudget)({
1535
+ childrenCompleted: state.context_budget.children_completed,
1536
+ lastChildStatus: workerStatus,
1537
+ policy: budgetPolicy,
1538
+ });
1539
+ if (postBudgetCheck.status === 'exhausted') {
1540
+ const nextPending = state.open_children[0] ?? null;
1541
+ if (!dryRun) {
1542
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
1543
+ ...state,
1544
+ status: "budget-exhausted",
1545
+ step_cursor: "budget-check",
1546
+ next_open_child: nextPending,
1547
+ });
1548
+ appendTelemetry(telemetryFile, {
1549
+ event: "budget-exhausted",
1550
+ run_id: state.run_id,
1551
+ children_completed: state.context_budget.children_completed,
1552
+ next_child: nextPending,
1553
+ reason: postBudgetCheck.reason,
1554
+ timestamp: new Date().toISOString(),
1555
+ });
1556
+ }
1557
+ return {
1558
+ haltReason: 'budget-exhausted',
1559
+ childrenDispatched,
1560
+ haltingChild: nextPending ?? undefined,
1561
+ message: postBudgetCheck.reason,
1562
+ };
1563
+ }
1564
+ // ── Step 06: CONTINUE (back to step 02) ─────────────────────────────
1565
+ }
1566
+ }