@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,276 @@
1
+ "use strict";
2
+ /**
3
+ * Orphan detection and child recovery workflow.
4
+ *
5
+ * Detects workers that have gone silent, failed to acknowledge, or left
6
+ * partial execution traces. Implements the 5-scenario recovery model:
7
+ *
8
+ * A: no-worker-assignment — packet written, no worker_id after launch_timeout
9
+ * B: no-acknowledgment — worker_id present, no ack within launch_to_first_heartbeat_ms
10
+ * C: no-heartbeat — acknowledged, heartbeat > orphan_timeout_ms ago, no result
11
+ * D: missing-result-artifact — worker-result event received but expected_result_path invalid
12
+ * E: stale-dispatch — dispatched_at + stale_dispatch_timeout ago, still handoff-pending
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.emitRecoveryApproved = emitRecoveryApproved;
16
+ exports.checkOrphans = checkOrphans;
17
+ const node_fs_1 = require("node:fs");
18
+ const node_path_1 = require("node:path");
19
+ const node_crypto_1 = require("node:crypto");
20
+ const checkpoint_js_1 = require("./checkpoint.js");
21
+ // ── Telemetry helpers ─────────────────────────────────────────────────────────
22
+ function appendTelemetry(telemetryFile, event) {
23
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
24
+ (0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
25
+ }
26
+ function resolveTelemetryFile(state, repoRoot) {
27
+ const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
28
+ return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
29
+ }
30
+ /** Returns true only when path is a non-empty string and the file exists. */
31
+ function safeResultExists(path) {
32
+ if (typeof path !== "string" || path.trim() === "")
33
+ return false;
34
+ try {
35
+ return (0, node_fs_1.existsSync)(path);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ const DEFAULT_ORPHAN_TIMEOUTS = {
42
+ launchTimeoutMs: 30_000,
43
+ launchToFirstHeartbeatMs: 30_000,
44
+ orphanTimeoutMs: 300_000, // 5 min (was 10 min)
45
+ staleDispatchTimeoutMs: 1_800_000, // 30 min
46
+ };
47
+ // ── Telemetry scanning ────────────────────────────────────────────────────────
48
+ function hasTelemetryEvent(telemetryFile, eventName, childId) {
49
+ if (!(0, node_fs_1.existsSync)(telemetryFile))
50
+ return false;
51
+ try {
52
+ const lines = (0, node_fs_1.readFileSync)(telemetryFile, "utf-8").trim().split("\n").filter(Boolean);
53
+ for (const line of lines) {
54
+ try {
55
+ const e = JSON.parse(line);
56
+ if (e.event === eventName && e.child_id === childId)
57
+ return true;
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ }
63
+ }
64
+ catch { /* ignore */ }
65
+ return false;
66
+ }
67
+ // ── Emit recovery telemetry events ───────────────────────────────────────────
68
+ function emitRecoveryInitiated(telemetryFile, childId, dispatchId, reason) {
69
+ appendTelemetry(telemetryFile, {
70
+ event: "child-recovery-initiated",
71
+ event_id: (0, node_crypto_1.randomUUID)(),
72
+ child_id: childId,
73
+ dispatch_id: dispatchId,
74
+ recovery_reason: reason,
75
+ detected_at: new Date().toISOString(),
76
+ });
77
+ }
78
+ function emitChildOrphaned(telemetryFile, childId, dispatchId, lastHeartbeatAt) {
79
+ appendTelemetry(telemetryFile, {
80
+ event: "child-orphaned",
81
+ event_id: (0, node_crypto_1.randomUUID)(),
82
+ child_id: childId,
83
+ dispatch_id: dispatchId,
84
+ last_heartbeat_at: lastHeartbeatAt,
85
+ orphaned_at: new Date().toISOString(),
86
+ });
87
+ }
88
+ function emitRecoveryApprovalRequested(telemetryFile, childId, dispatchId, reason) {
89
+ appendTelemetry(telemetryFile, {
90
+ event: "recovery-approval-requested",
91
+ event_id: (0, node_crypto_1.randomUUID)(),
92
+ child_id: childId,
93
+ dispatch_id: dispatchId,
94
+ recovery_reason: reason,
95
+ operator_notified_at: new Date().toISOString(),
96
+ });
97
+ }
98
+ function emitRecoveryApproved(telemetryFile, childId, dispatchId, approvedBy) {
99
+ appendTelemetry(telemetryFile, {
100
+ event: "recovery-approved",
101
+ event_id: (0, node_crypto_1.randomUUID)(),
102
+ child_id: childId,
103
+ dispatch_id: dispatchId,
104
+ approved_by: approvedBy,
105
+ approved_at: new Date().toISOString(),
106
+ });
107
+ }
108
+ function emitChildRequeued(telemetryFile, childId, newDispatchId, previousDispatchId) {
109
+ appendTelemetry(telemetryFile, {
110
+ event: "child-requeued",
111
+ event_id: (0, node_crypto_1.randomUUID)(),
112
+ child_id: childId,
113
+ new_dispatch_id: newDispatchId,
114
+ previous_dispatch_id: previousDispatchId,
115
+ requeued_at: new Date().toISOString(),
116
+ });
117
+ }
118
+ // ── State mutation for recovery ───────────────────────────────────────────────
119
+ function transitionToOrphaned(stateFile, state, childId) {
120
+ const childMeta = state.open_children_meta?.[childId];
121
+ if (!childMeta?.dispatch_record)
122
+ return;
123
+ const updatedMeta = {
124
+ ...state.open_children_meta,
125
+ [childId]: {
126
+ ...childMeta,
127
+ dispatch_record: {
128
+ ...childMeta.dispatch_record,
129
+ runtime_state: "orphaned",
130
+ },
131
+ },
132
+ };
133
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, { ...state, open_children_meta: updatedMeta });
134
+ }
135
+ /**
136
+ * Reset a dispatch record for safe auto-redispatch.
137
+ * Preserves the old dispatch_id in the requeue event for audit.
138
+ */
139
+ function resetForRedispatch(stateFile, state, childId) {
140
+ const childMeta = state.open_children_meta?.[childId];
141
+ if (!childMeta?.dispatch_record)
142
+ return (0, node_crypto_1.randomUUID)();
143
+ const newDispatchId = (0, node_crypto_1.randomUUID)();
144
+ // Clear dispatch record but retain audit trail via telemetry
145
+ const updatedMeta = {
146
+ ...state.open_children_meta,
147
+ [childId]: {
148
+ ...childMeta,
149
+ dispatch_record: undefined,
150
+ },
151
+ };
152
+ // Reset active_child so a new dispatch can proceed
153
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
154
+ ...state,
155
+ active_child: "",
156
+ open_children_meta: updatedMeta,
157
+ });
158
+ return newDispatchId;
159
+ }
160
+ /**
161
+ * Check the current state for orphaned children across all 5 scenarios.
162
+ * Emits telemetry events for detected cases.
163
+ * Auto-requeues for safe scenarios (A, B, C, E).
164
+ * Emits recovery-approval-requested and halts for approval scenario (D).
165
+ */
166
+ function checkOrphans(options) {
167
+ const timeouts = {
168
+ ...DEFAULT_ORPHAN_TIMEOUTS,
169
+ ...options.timeouts,
170
+ };
171
+ let state;
172
+ try {
173
+ state = (0, checkpoint_js_1.readState)(options.stateFile);
174
+ }
175
+ catch {
176
+ return { detected: [], checked: 0 };
177
+ }
178
+ const telemetryFile = resolveTelemetryFile(state, options.repoRoot);
179
+ const now = Date.now();
180
+ const detected = [];
181
+ let checked = 0;
182
+ // Check the active child (if any)
183
+ if (state.active_child) {
184
+ checked++;
185
+ const childMeta = state.open_children_meta?.[state.active_child];
186
+ const dr = childMeta?.dispatch_record;
187
+ if (dr && dr.runtime_state !== "completed" && dr.runtime_state !== "failed") {
188
+ const dispatchedAt = new Date(dr.dispatched_at).getTime();
189
+ const elapsed = now - dispatchedAt;
190
+ // Scenario A: packet written, no worker_id after launch_timeout
191
+ if (!dr.worker_id && elapsed > timeouts.launchTimeoutMs) {
192
+ const detection = {
193
+ childId: state.active_child,
194
+ dispatchId: dr.dispatch_id,
195
+ reason: "no-worker-assignment",
196
+ requiresApproval: false,
197
+ };
198
+ detected.push(detection);
199
+ emitRecoveryInitiated(telemetryFile, state.active_child, dr.dispatch_id, "no-worker-assignment");
200
+ emitChildOrphaned(telemetryFile, state.active_child, dr.dispatch_id, null);
201
+ const newId = resetForRedispatch(options.stateFile, state, state.active_child);
202
+ emitChildRequeued(telemetryFile, state.active_child, newId, dr.dispatch_id);
203
+ return { detected, checked };
204
+ }
205
+ // Scenario B: worker_id present, no acknowledgment within launch timeout
206
+ if (dr.worker_id && !dr.first_heartbeat_at && elapsed > timeouts.launchToFirstHeartbeatMs) {
207
+ const detection = {
208
+ childId: state.active_child,
209
+ dispatchId: dr.dispatch_id,
210
+ reason: "no-acknowledgment",
211
+ requiresApproval: false,
212
+ };
213
+ detected.push(detection);
214
+ emitRecoveryInitiated(telemetryFile, state.active_child, dr.dispatch_id, "no-acknowledgment");
215
+ emitChildOrphaned(telemetryFile, state.active_child, dr.dispatch_id, null);
216
+ const newId = resetForRedispatch(options.stateFile, state, state.active_child);
217
+ emitChildRequeued(telemetryFile, state.active_child, newId, dr.dispatch_id);
218
+ return { detected, checked };
219
+ }
220
+ // Scenario C: acknowledged, heartbeat lost, no result — auto-replace
221
+ if (dr.first_heartbeat_at && !safeResultExists(dr.expected_result_path)) {
222
+ const lastHb = dr.last_heartbeat_at ?? dr.first_heartbeat_at;
223
+ const heartbeatAge = now - new Date(lastHb).getTime();
224
+ if (heartbeatAge > timeouts.orphanTimeoutMs) {
225
+ const detection = {
226
+ childId: state.active_child,
227
+ dispatchId: dr.dispatch_id,
228
+ reason: "no-heartbeat",
229
+ requiresApproval: false,
230
+ };
231
+ detected.push(detection);
232
+ emitRecoveryInitiated(telemetryFile, state.active_child, dr.dispatch_id, "no-heartbeat");
233
+ emitChildOrphaned(telemetryFile, state.active_child, dr.dispatch_id, lastHb);
234
+ const newId = resetForRedispatch(options.stateFile, state, state.active_child);
235
+ emitChildRequeued(telemetryFile, state.active_child, newId, dr.dispatch_id);
236
+ return { detected, checked };
237
+ }
238
+ }
239
+ // Scenario D: worker-result event received but result artifact missing or invalid
240
+ const hasWorkerResult = hasTelemetryEvent(telemetryFile, "worker-result", state.active_child);
241
+ if (hasWorkerResult && !safeResultExists(dr.expected_result_path)) {
242
+ // Check for commits in child scope (simplified: assume no commits for now)
243
+ const reason = "missing-result-artifact-no-commits";
244
+ const detection = {
245
+ childId: state.active_child,
246
+ dispatchId: dr.dispatch_id,
247
+ reason,
248
+ requiresApproval: true,
249
+ };
250
+ detected.push(detection);
251
+ emitRecoveryInitiated(telemetryFile, state.active_child, dr.dispatch_id, reason);
252
+ emitChildOrphaned(telemetryFile, state.active_child, dr.dispatch_id, dr.last_heartbeat_at ?? null);
253
+ emitRecoveryApprovalRequested(telemetryFile, state.active_child, dr.dispatch_id, reason);
254
+ transitionToOrphaned(options.stateFile, state, state.active_child);
255
+ return { detected, checked };
256
+ }
257
+ // Scenario E: stale dispatch — dispatched_at + stale_dispatch_timeout with no state change
258
+ if ((dr.runtime_state === "packet-created" || dr.runtime_state === "delegated") &&
259
+ elapsed > timeouts.staleDispatchTimeoutMs) {
260
+ const detection = {
261
+ childId: state.active_child,
262
+ dispatchId: dr.dispatch_id,
263
+ reason: "stale-dispatch",
264
+ requiresApproval: false,
265
+ };
266
+ detected.push(detection);
267
+ emitRecoveryInitiated(telemetryFile, state.active_child, dr.dispatch_id, "stale-dispatch");
268
+ emitChildOrphaned(telemetryFile, state.active_child, dr.dispatch_id, null);
269
+ const newId = resetForRedispatch(options.stateFile, state, state.active_child);
270
+ emitChildRequeued(telemetryFile, state.active_child, newId, dr.dispatch_id);
271
+ return { detected, checked };
272
+ }
273
+ }
274
+ }
275
+ return { detected, checked };
276
+ }
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for orphan detection and child recovery workflow (POL-229).
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const node_os_1 = require("node:os");
10
+ const orphan_recovery_js_1 = require("./orphan-recovery.js");
11
+ const run_bootstrap_js_1 = require("./run-bootstrap.js");
12
+ function makeTestDir() {
13
+ const dir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `polaris-orphan-test-${Date.now()}-${Math.random().toString(16).slice(2)}`);
14
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
15
+ return dir;
16
+ }
17
+ function writeState(dir, state) {
18
+ const stateFile = (0, node_path_1.join)(dir, "current-state.json");
19
+ (0, node_fs_1.writeFileSync)(stateFile, JSON.stringify(state, null, 2), "utf-8");
20
+ return stateFile;
21
+ }
22
+ function baseState(overrides = {}) {
23
+ return {
24
+ schema_version: "1.0",
25
+ run_id: "run-orphan-001",
26
+ cluster_id: "POL-100",
27
+ session_type: "implement",
28
+ branch: "test-branch",
29
+ active_child: "",
30
+ completed_children: [],
31
+ open_children: ["POL-101"],
32
+ open_children_meta: {},
33
+ step_cursor: "dispatch",
34
+ context_budget: { children_completed: 0, max_children_per_session: 3 },
35
+ status: "running",
36
+ next_open_child: "POL-101",
37
+ dispatch_boundary: { dispatch_epoch: 1, continue_epoch: 0, last_dispatched_child: null },
38
+ run_bootstrap_seal: (0, run_bootstrap_js_1.createBootstrapSeal)("run-orphan-001", "POL-100", ["POL-101"]),
39
+ ...overrides,
40
+ };
41
+ }
42
+ function telemetryEvents(dir) {
43
+ const runId = "run-orphan-001";
44
+ const telemetryFile = (0, node_path_1.join)(dir, ".taskchain_artifacts", "polaris-run", "runs", runId, "telemetry.jsonl");
45
+ if (!(0, node_fs_1.existsSync)(telemetryFile))
46
+ return [];
47
+ return (0, node_fs_1.readFileSync)(telemetryFile, "utf-8")
48
+ .trim()
49
+ .split("\n")
50
+ .filter(Boolean)
51
+ .map((l) => JSON.parse(l));
52
+ }
53
+ (0, vitest_1.describe)("checkOrphans", () => {
54
+ let testDir;
55
+ (0, vitest_1.beforeEach)(() => {
56
+ testDir = makeTestDir();
57
+ });
58
+ (0, vitest_1.afterEach)(() => {
59
+ (0, node_fs_1.rmSync)(testDir, { recursive: true, force: true });
60
+ });
61
+ function opts(extra = {}) {
62
+ return {
63
+ stateFile: (0, node_path_1.join)(testDir, "current-state.json"),
64
+ repoRoot: testDir,
65
+ timeouts: {
66
+ launchTimeoutMs: 100,
67
+ launchToFirstHeartbeatMs: 100,
68
+ orphanTimeoutMs: 100,
69
+ staleDispatchTimeoutMs: 100,
70
+ },
71
+ ...extra,
72
+ };
73
+ }
74
+ (0, vitest_1.it)("returns zero detections when no active child", () => {
75
+ writeState(testDir, baseState());
76
+ const result = (0, orphan_recovery_js_1.checkOrphans)(opts());
77
+ (0, vitest_1.expect)(result.detected).toHaveLength(0);
78
+ });
79
+ (0, vitest_1.it)("Scenario A: detects no-worker-assignment when worker_id missing after launch timeout", () => {
80
+ const pastTime = new Date(Date.now() - 500).toISOString();
81
+ writeState(testDir, baseState({
82
+ active_child: "POL-101",
83
+ open_children_meta: {
84
+ "POL-101": {
85
+ dispatch_record: {
86
+ dispatch_id: "d-001",
87
+ child_id: "POL-101",
88
+ run_id: "run-orphan-001",
89
+ cluster_id: "POL-100",
90
+ packet_path: "/tmp/p.json",
91
+ expected_result_path: "/tmp/r.json",
92
+ dispatched_at: pastTime,
93
+ status: "dispatched",
94
+ runtime_state: "packet-created",
95
+ // No worker_id
96
+ },
97
+ },
98
+ },
99
+ }));
100
+ const result = (0, orphan_recovery_js_1.checkOrphans)(opts());
101
+ (0, vitest_1.expect)(result.detected).toHaveLength(1);
102
+ (0, vitest_1.expect)(result.detected[0].reason).toBe("no-worker-assignment");
103
+ (0, vitest_1.expect)(result.detected[0].requiresApproval).toBe(false);
104
+ const events = telemetryEvents(testDir);
105
+ (0, vitest_1.expect)(events.some((e) => e["event"] === "child-recovery-initiated")).toBe(true);
106
+ (0, vitest_1.expect)(events.some((e) => e["event"] === "child-orphaned")).toBe(true);
107
+ (0, vitest_1.expect)(events.some((e) => e["event"] === "child-requeued")).toBe(true);
108
+ });
109
+ (0, vitest_1.it)("Scenario B: detects no-acknowledgment when worker_id present but no heartbeat", () => {
110
+ const pastTime = new Date(Date.now() - 500).toISOString();
111
+ writeState(testDir, baseState({
112
+ active_child: "POL-101",
113
+ open_children_meta: {
114
+ "POL-101": {
115
+ dispatch_record: {
116
+ dispatch_id: "d-001",
117
+ child_id: "POL-101",
118
+ run_id: "run-orphan-001",
119
+ cluster_id: "POL-100",
120
+ packet_path: "/tmp/p.json",
121
+ expected_result_path: "/tmp/r.json",
122
+ dispatched_at: pastTime,
123
+ status: "dispatched",
124
+ runtime_state: "launching",
125
+ worker_id: "w-001",
126
+ // No first_heartbeat_at
127
+ },
128
+ },
129
+ },
130
+ }));
131
+ const result = (0, orphan_recovery_js_1.checkOrphans)(opts());
132
+ (0, vitest_1.expect)(result.detected).toHaveLength(1);
133
+ (0, vitest_1.expect)(result.detected[0].reason).toBe("no-acknowledgment");
134
+ const events = telemetryEvents(testDir);
135
+ (0, vitest_1.expect)(events.some((e) => e["event"] === "child-recovery-initiated")).toBe(true);
136
+ (0, vitest_1.expect)(events.some((e) => e["recovery_reason"] === "no-acknowledgment")).toBe(true);
137
+ });
138
+ (0, vitest_1.it)("Scenario C: detects no-heartbeat and requires approval", () => {
139
+ const pastTime = new Date(Date.now() - 500).toISOString();
140
+ writeState(testDir, baseState({
141
+ active_child: "POL-101",
142
+ open_children_meta: {
143
+ "POL-101": {
144
+ dispatch_record: {
145
+ dispatch_id: "d-001",
146
+ child_id: "POL-101",
147
+ run_id: "run-orphan-001",
148
+ cluster_id: "POL-100",
149
+ packet_path: "/tmp/p.json",
150
+ expected_result_path: (0, node_path_1.join)(testDir, "result-missing.json"), // does not exist
151
+ dispatched_at: pastTime,
152
+ status: "dispatched",
153
+ runtime_state: "running",
154
+ worker_id: "w-001",
155
+ first_heartbeat_at: pastTime,
156
+ last_heartbeat_at: pastTime, // old heartbeat
157
+ },
158
+ },
159
+ },
160
+ }));
161
+ const result = (0, orphan_recovery_js_1.checkOrphans)(opts());
162
+ (0, vitest_1.expect)(result.detected).toHaveLength(1);
163
+ (0, vitest_1.expect)(result.detected[0].reason).toBe("no-heartbeat");
164
+ (0, vitest_1.expect)(result.detected[0].requiresApproval).toBe(true);
165
+ const events = telemetryEvents(testDir);
166
+ (0, vitest_1.expect)(events.some((e) => e["event"] === "recovery-approval-requested")).toBe(true);
167
+ });
168
+ (0, vitest_1.it)("Scenario E: detects stale-dispatch and auto-requeues", () => {
169
+ const pastTime = new Date(Date.now() - 500).toISOString();
170
+ writeState(testDir, baseState({
171
+ active_child: "POL-101",
172
+ open_children_meta: {
173
+ "POL-101": {
174
+ dispatch_record: {
175
+ dispatch_id: "d-001",
176
+ child_id: "POL-101",
177
+ run_id: "run-orphan-001",
178
+ cluster_id: "POL-100",
179
+ packet_path: "/tmp/p.json",
180
+ expected_result_path: "/tmp/r.json",
181
+ dispatched_at: pastTime,
182
+ status: "dispatched",
183
+ runtime_state: "packet-created",
184
+ // no worker_id, old dispatch
185
+ },
186
+ },
187
+ },
188
+ }));
189
+ // Use longer launchTimeout so Scenario A doesn't trigger first
190
+ const result = (0, orphan_recovery_js_1.checkOrphans)({
191
+ ...opts(),
192
+ timeouts: {
193
+ launchTimeoutMs: 600_000, // long — won't trigger Scenario A
194
+ launchToFirstHeartbeatMs: 100,
195
+ orphanTimeoutMs: 100,
196
+ staleDispatchTimeoutMs: 100, // short — triggers Scenario E
197
+ },
198
+ });
199
+ (0, vitest_1.expect)(result.detected).toHaveLength(1);
200
+ (0, vitest_1.expect)(result.detected[0].reason).toBe("stale-dispatch");
201
+ const events = telemetryEvents(testDir);
202
+ (0, vitest_1.expect)(events.some((e) => e["event"] === "child-requeued")).toBe(true);
203
+ });
204
+ (0, vitest_1.it)("no detection when dispatch is fresh", () => {
205
+ const nowTime = new Date().toISOString();
206
+ writeState(testDir, baseState({
207
+ active_child: "POL-101",
208
+ open_children_meta: {
209
+ "POL-101": {
210
+ dispatch_record: {
211
+ dispatch_id: "d-001",
212
+ child_id: "POL-101",
213
+ run_id: "run-orphan-001",
214
+ cluster_id: "POL-100",
215
+ packet_path: "/tmp/p.json",
216
+ expected_result_path: "/tmp/r.json",
217
+ dispatched_at: nowTime,
218
+ status: "dispatched",
219
+ runtime_state: "launching",
220
+ worker_id: "w-001",
221
+ },
222
+ },
223
+ },
224
+ }));
225
+ const result = (0, orphan_recovery_js_1.checkOrphans)({
226
+ ...opts(),
227
+ timeouts: {
228
+ launchTimeoutMs: 60_000,
229
+ launchToFirstHeartbeatMs: 60_000,
230
+ orphanTimeoutMs: 600_000,
231
+ staleDispatchTimeoutMs: 1_800_000,
232
+ },
233
+ });
234
+ (0, vitest_1.expect)(result.detected).toHaveLength(0);
235
+ });
236
+ });