@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.
- package/README.md +87 -0
- package/dist/__mocks__/tool-server-linear.js +34 -0
- package/dist/cli/adopt-approve.js +60 -0
- package/dist/cli/adopt-cognition.js +199 -0
- package/dist/cli/adopt-instructions.js +143 -0
- package/dist/cli/adopt-rules.js +169 -0
- package/dist/cli/adopt-scan.js +25 -0
- package/dist/cli/adopt-smartdocs.js +192 -0
- package/dist/cli/adoption-inventory.js +350 -0
- package/dist/cli/adoption-plan.js +222 -0
- package/dist/cli/args.js +28 -0
- package/dist/cli/args.test.js +30 -0
- package/dist/cli/branding.js +41 -0
- package/dist/cli/graph.js +358 -0
- package/dist/cli/index.js +127 -0
- package/dist/cli/index.test.js +162 -0
- package/dist/cli/init-detect.js +61 -0
- package/dist/cli/init.js +492 -0
- package/dist/cli/init.test.js +259 -0
- package/dist/cli/librarian.js +32 -0
- package/dist/cli/medic.js +96 -0
- package/dist/cli/startup.test.js +23 -0
- package/dist/cli/tracker.js +130 -0
- package/dist/cli/tracker.test.js +126 -0
- package/dist/cli/version.js +10 -0
- package/dist/cli/version.test.js +11 -0
- package/dist/cli/worker.js +287 -0
- package/dist/cluster-state/fixtures/test-clusters.json +19 -0
- package/dist/cluster-state/store.js +302 -0
- package/dist/cluster-state/store.test.js +145 -0
- package/dist/cluster-state/types.js +2 -0
- package/dist/cognition/archive.js +158 -0
- package/dist/cognition/closeout-librarian-types.js +161 -0
- package/dist/cognition/cognition.test.js +623 -0
- package/dist/cognition/index.js +41 -0
- package/dist/cognition/librarian-dispatch.js +391 -0
- package/dist/cognition/librarian-dispatch.test.js +247 -0
- package/dist/cognition/librarian-packet.js +313 -0
- package/dist/cognition/librarian-types.js +57 -0
- package/dist/cognition/route-cognition-delta.js +350 -0
- package/dist/cognition/summary-delta.js +279 -0
- package/dist/cognition/validate.js +162 -0
- package/dist/config/defaults.js +98 -0
- package/dist/config/doctor.js +273 -0
- package/dist/config/index.js +14 -0
- package/dist/config/loader.js +58 -0
- package/dist/config/provider-detect.js +60 -0
- package/dist/config/provider-detect.test.js +119 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/show.js +44 -0
- package/dist/config/show.test.js +42 -0
- package/dist/config/validator.js +675 -0
- package/dist/config/validator.test.js +77 -0
- package/dist/docs/canon-check.js +334 -0
- package/dist/docs/canon-check.test.js +207 -0
- package/dist/docs/doctrine.js +133 -0
- package/dist/docs/doctrine.test.js +139 -0
- package/dist/docs/index.js +168 -0
- package/dist/docs/index.test.js +83 -0
- package/dist/docs/ingest.js +295 -0
- package/dist/docs/ingest.test.js +71 -0
- package/dist/docs/migrate.js +165 -0
- package/dist/docs/migrate.test.js +136 -0
- package/dist/docs/seed-instructions.js +147 -0
- package/dist/docs/seed-instructions.test.js +137 -0
- package/dist/docs/validate-instructions.js +267 -0
- package/dist/docs/validate-instructions.test.js +238 -0
- package/dist/finalize/artifact-policy.js +214 -0
- package/dist/finalize/artifact-policy.test.js +84 -0
- package/dist/finalize/delivery-integrity.js +141 -0
- package/dist/finalize/finalize.test.js +210 -0
- package/dist/finalize/github.js +41 -0
- package/dist/finalize/index.js +474 -0
- package/dist/finalize/linear.js +213 -0
- package/dist/finalize/run-report.js +47 -0
- package/dist/finalize/steps/01-map-update.js +11 -0
- package/dist/finalize/steps/02-map-validate.js +11 -0
- package/dist/finalize/steps/03-schema-validate.js +11 -0
- package/dist/finalize/steps/04-run-checks.js +118 -0
- package/dist/finalize/steps/05-generate-report.js +14 -0
- package/dist/finalize/steps/06-commit.js +39 -0
- package/dist/finalize/steps/07-push.js +10 -0
- package/dist/finalize/steps/08-create-pr.js +9 -0
- package/dist/finalize/steps/09-update-state.js +9 -0
- package/dist/finalize/steps/10-append-jsonl.js +23 -0
- package/dist/finalize/steps/11-update-linear.js +22 -0
- package/dist/finalize/steps/12-archive.js +21 -0
- package/dist/graph/adapter/c/adapter.js +45 -0
- package/dist/graph/adapter/c/extract.js +135 -0
- package/dist/graph/adapter/c/index.js +10 -0
- package/dist/graph/adapter/c/runtime.js +67 -0
- package/dist/graph/adapter/cpp/adapter.js +45 -0
- package/dist/graph/adapter/cpp/extract.js +159 -0
- package/dist/graph/adapter/cpp/index.js +10 -0
- package/dist/graph/adapter/cpp/runtime.js +61 -0
- package/dist/graph/adapter/csharp/adapter.js +45 -0
- package/dist/graph/adapter/csharp/extract.js +127 -0
- package/dist/graph/adapter/csharp/index.js +10 -0
- package/dist/graph/adapter/csharp/runtime.js +61 -0
- package/dist/graph/adapter/dart/adapter.js +56 -0
- package/dist/graph/adapter/dart/extract.js +130 -0
- package/dist/graph/adapter/dart/index.js +10 -0
- package/dist/graph/adapter/dart/runtime.js +61 -0
- package/dist/graph/adapter/go/adapter.js +45 -0
- package/dist/graph/adapter/go/extract.js +112 -0
- package/dist/graph/adapter/go/index.js +10 -0
- package/dist/graph/adapter/go/runtime.js +58 -0
- package/dist/graph/adapter/kotlin-java/extract-common.js +83 -0
- package/dist/graph/adapter/kotlin-java/index.js +15 -0
- package/dist/graph/adapter/kotlin-java/java-adapter.js +45 -0
- package/dist/graph/adapter/kotlin-java/java-extract.js +42 -0
- package/dist/graph/adapter/kotlin-java/kotlin-adapter.js +48 -0
- package/dist/graph/adapter/kotlin-java/kotlin-extract.js +58 -0
- package/dist/graph/adapter/kotlin-java/runtime.js +75 -0
- package/dist/graph/adapter/python/adapter.js +45 -0
- package/dist/graph/adapter/python/extract.js +121 -0
- package/dist/graph/adapter/python/index.js +10 -0
- package/dist/graph/adapter/python/runtime.js +70 -0
- package/dist/graph/adapter/registry.js +82 -0
- package/dist/graph/adapter/rust/adapter.js +45 -0
- package/dist/graph/adapter/rust/extract.js +110 -0
- package/dist/graph/adapter/rust/index.js +10 -0
- package/dist/graph/adapter/rust/runtime.js +74 -0
- package/dist/graph/adapter/shell/adapter.js +34 -0
- package/dist/graph/adapter/shell/extract.js +98 -0
- package/dist/graph/adapter/shell/index.js +8 -0
- package/dist/graph/adapter/svelte/adapter.js +91 -0
- package/dist/graph/adapter/svelte/extract.js +41 -0
- package/dist/graph/adapter/svelte/index.js +8 -0
- package/dist/graph/adapter/swift/adapter.js +45 -0
- package/dist/graph/adapter/swift/extract.js +120 -0
- package/dist/graph/adapter/swift/index.js +10 -0
- package/dist/graph/adapter/swift/runtime.js +70 -0
- package/dist/graph/adapter/types.js +2 -0
- package/dist/graph/adapter/typescript-javascript/adapter.js +72 -0
- package/dist/graph/adapter/typescript-javascript/extract.js +115 -0
- package/dist/graph/adapter/typescript-javascript/index.js +10 -0
- package/dist/graph/adapter/typescript-javascript/runtime.js +97 -0
- package/dist/graph/capability/index.js +105 -0
- package/dist/graph/governance.js +104 -0
- package/dist/graph/parser/extract.js +5 -0
- package/dist/graph/parser/loader.js +2 -0
- package/dist/graph/parser/pipeline.js +194 -0
- package/dist/graph/query/index.js +277 -0
- package/dist/graph/query/types.js +5 -0
- package/dist/graph/resolver/build-edges.js +211 -0
- package/dist/graph/resolver/index.js +23 -0
- package/dist/graph/resolver/resolve-imports.js +168 -0
- package/dist/graph/store/adapter.js +132 -0
- package/dist/graph/store/queries.js +124 -0
- package/dist/graph/store/types.js +4 -0
- package/dist/ignore/defaults.js +41 -0
- package/dist/ignore/parser.js +39 -0
- package/dist/lint/map-reference-validator.js +72 -0
- package/dist/loop/abort.js +188 -0
- package/dist/loop/abort.test.js +151 -0
- package/dist/loop/adapters/__tests__/terminal-cli.test.js +92 -0
- package/dist/loop/adapters/agent-subtask.js +137 -0
- package/dist/loop/adapters/agent-subtask.test.js +151 -0
- package/dist/loop/adapters/cli-subtask-bridge.js +108 -0
- package/dist/loop/adapters/index.js +9 -0
- package/dist/loop/adapters/registry.js +17 -0
- package/dist/loop/adapters/terminal-cli.js +403 -0
- package/dist/loop/adapters/types.js +2 -0
- package/dist/loop/adapters/worker-instructions.js +85 -0
- package/dist/loop/body-parser.js +226 -0
- package/dist/loop/bootstrap-packet.js +97 -0
- package/dist/loop/bootstrap-packet.test.js +171 -0
- package/dist/loop/branch-governance.js +54 -0
- package/dist/loop/budget.js +63 -0
- package/dist/loop/budget.test.js +163 -0
- package/dist/loop/checkpoint.js +325 -0
- package/dist/loop/compact-return.js +59 -0
- package/dist/loop/continue.js +645 -0
- package/dist/loop/continue.test.js +337 -0
- package/dist/loop/dispatch-boundary.js +335 -0
- package/dist/loop/dispatch-state.js +223 -0
- package/dist/loop/dispatch-state.test.js +186 -0
- package/dist/loop/dispatch.js +1236 -0
- package/dist/loop/dispatch.test.js +499 -0
- package/dist/loop/ephemeral-smoke.test.js +160 -0
- package/dist/loop/evidence-backfill.js +216 -0
- package/dist/loop/execution-adapter.js +141 -0
- package/dist/loop/execution-adapter.test.js +66 -0
- package/dist/loop/finalize-evidence.js +169 -0
- package/dist/loop/git-custody.js +250 -0
- package/dist/loop/impl-packet-input.js +38 -0
- package/dist/loop/index.js +270 -0
- package/dist/loop/ledger.js +49 -0
- package/dist/loop/ledger.test.js +127 -0
- package/dist/loop/lifecycle-dispatch.js +348 -0
- package/dist/loop/lifecycle-dispatch.test.js +318 -0
- package/dist/loop/lifecycle.js +93 -0
- package/dist/loop/lifecycle.test.js +150 -0
- package/dist/loop/orphan-recovery.js +276 -0
- package/dist/loop/orphan-recovery.test.js +236 -0
- package/dist/loop/parent.js +1566 -0
- package/dist/loop/parent.test.js +738 -0
- package/dist/loop/provider-sealed-packet.test.js +164 -0
- package/dist/loop/resume.js +295 -0
- package/dist/loop/resume.test.js +172 -0
- package/dist/loop/run-bootstrap.js +234 -0
- package/dist/loop/run-preflight.js +203 -0
- package/dist/loop/status.js +665 -0
- package/dist/loop/status.test.js +235 -0
- package/dist/loop/wait.js +86 -0
- package/dist/loop/worker-packet.js +406 -0
- package/dist/loop/worker-packet.test.js +243 -0
- package/dist/loop/worker-prompt.js +260 -0
- package/dist/loop/worker-prompt.test.js +247 -0
- package/dist/loop/worker.js +498 -0
- package/dist/loop/worker.test.js +372 -0
- package/dist/map/atlas.js +77 -0
- package/dist/map/backfill.js +171 -0
- package/dist/map/backfill.test.js +120 -0
- package/dist/map/index.js +255 -0
- package/dist/map/inference.js +73 -0
- package/dist/map/inference.test.js +35 -0
- package/dist/map/query.js +157 -0
- package/dist/map/query.test.js +174 -0
- package/dist/map/update.js +355 -0
- package/dist/map/update.test.js +67 -0
- package/dist/map/validate.js +153 -0
- package/dist/map/validate.test.js +90 -0
- package/dist/map/welfare.js +86 -0
- package/dist/mcp/lib/invoke.js +35 -0
- package/dist/mcp/lib/redact.js +24 -0
- package/dist/mcp/lib/redact.test.js +112 -0
- package/dist/mcp/lib/root.js +41 -0
- package/dist/mcp/lib/root.test.js +114 -0
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/tools/claim-child.js +119 -0
- package/dist/mcp/tools/claim-child.test.js +69 -0
- package/dist/mcp/tools/current-state.js +73 -0
- package/dist/mcp/tools/current-state.test.js +114 -0
- package/dist/mcp/tools/dispatch-result.js +186 -0
- package/dist/mcp/tools/dispatch-result.test.js +109 -0
- package/dist/mcp/tools/index.js +222 -0
- package/dist/mcp/tools/index.test.js +172 -0
- package/dist/mcp/tools/linear.js +74 -0
- package/dist/mcp/tools/loop-continue.js +159 -0
- package/dist/mcp/tools/loop-continue.test.js +186 -0
- package/dist/mcp/tools/loop-dry-run.js +15 -0
- package/dist/mcp/tools/status.js +65 -0
- package/dist/medic/chart-id.js +75 -0
- package/dist/medic/chart-schema.js +139 -0
- package/dist/runs/index.js +119 -0
- package/dist/runs/index.test.js +184 -0
- package/dist/runs/ledger-tail.js +15 -0
- package/dist/runs/list.js +29 -0
- package/dist/runs/reconcile.js +74 -0
- package/dist/runs/show.js +10 -0
- package/dist/runtime/audit/logger.js +15 -0
- package/dist/runtime/audit/reader.js +42 -0
- package/dist/runtime/checkpoint.js +89 -0
- package/dist/runtime/continuation/confirmed.js +225 -0
- package/dist/runtime/continuation/confirmed.test.js +421 -0
- package/dist/runtime/continuation/dry-run.js +74 -0
- package/dist/runtime/execution-window.js +106 -0
- package/dist/runtime/scheduling/child-selector.js +16 -0
- package/dist/runtime/state.js +43 -0
- package/dist/runtime/status.js +46 -0
- package/dist/runtime/verification/envelope.js +80 -0
- package/dist/runtime/verification/fingerprint.js +26 -0
- package/dist/skill-packet/cli.test.js +105 -0
- package/dist/skill-packet/generator.js +225 -0
- package/dist/skill-packet/generator.test.js +159 -0
- package/dist/skill-packet/index.js +49 -0
- package/dist/skill-packet/types.js +2 -0
- package/dist/smartdocs-engine/audit.js +194 -0
- package/dist/smartdocs-engine/audit.test.js +122 -0
- package/dist/smartdocs-engine/canon-check.js +340 -0
- package/dist/smartdocs-engine/canon-check.test.js +234 -0
- package/dist/smartdocs-engine/doctrine.js +633 -0
- package/dist/smartdocs-engine/doctrine.test.js +374 -0
- package/dist/smartdocs-engine/index.js +456 -0
- package/dist/smartdocs-engine/index.test.js +82 -0
- package/dist/smartdocs-engine/ingest.js +495 -0
- package/dist/smartdocs-engine/ingest.test.js +163 -0
- package/dist/smartdocs-engine/migrate.js +217 -0
- package/dist/smartdocs-engine/migrate.test.js +200 -0
- package/dist/smartdocs-engine/seed-instructions.js +335 -0
- package/dist/smartdocs-engine/seed-instructions.test.js +275 -0
- package/dist/smartdocs-engine/smartdoc-ignore.js +258 -0
- package/dist/smartdocs-engine/smartdoc-ignore.test.js +146 -0
- package/dist/smartdocs-engine/validate-instructions.js +366 -0
- package/dist/smartdocs-engine/validate-instructions.test.js +283 -0
- package/dist/tracker/adapters/github/index.js +305 -0
- package/dist/tracker/adapters/jira/index.js +293 -0
- package/dist/tracker/adapters/linear/http-errors.test.js +51 -0
- package/dist/tracker/adapters/linear/index.js +621 -0
- package/dist/tracker/adapters/linear/index.test.js +238 -0
- package/dist/tracker/adapters/linear.js +50 -0
- package/dist/tracker/adapters/local/index.js +10 -0
- package/dist/tracker/adapters/local-file/index.js +159 -0
- package/dist/tracker/adapters/mcp-bridge.js +158 -0
- package/dist/tracker/adapters/mcp-bridge.test.js +12 -0
- package/dist/tracker/adapters/spec/index.js +146 -0
- package/dist/tracker/capabilities.js +2 -0
- package/dist/tracker/index.js +49 -0
- package/dist/tracker/lifecycle-policy.js +139 -0
- package/dist/tracker/lifecycle-transition.js +164 -0
- package/dist/tracker/local-graph.js +98 -0
- package/dist/tracker/migration.js +56 -0
- package/dist/tracker/schema.js +86 -0
- package/dist/tracker/sync/index.js +404 -0
- package/dist/tracker/sync/queue-store.js +95 -0
- package/dist/tracker/sync/queue-store.test.js +104 -0
- package/dist/tracker/types.js +11 -0
- package/dist/types/linear.js +3 -0
- package/dist/types/result-packet.js +12 -0
- package/dist/types/runtime-state.js +2 -0
- package/dist/types/work-contract.js +2 -0
- package/dist/utils/monotonic-timestamp.js +13 -0
- package/dist/utils/monotonic-timestamp.test.js +19 -0
- 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
|
+
}
|