@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,1236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveProviderAndMode = resolveProviderAndMode;
|
|
4
|
+
exports.assertProviderAllowedForRole = assertProviderAllowedForRole;
|
|
5
|
+
exports.runLoopDispatch = runLoopDispatch;
|
|
6
|
+
exports.checkAcknowledgmentTimeout = checkAcknowledgmentTimeout;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const git_custody_js_1 = require("./git-custody.js");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const checkpoint_js_1 = require("./checkpoint.js");
|
|
13
|
+
const worker_packet_js_1 = require("./worker-packet.js");
|
|
14
|
+
const body_parser_js_1 = require("./body-parser.js");
|
|
15
|
+
const loader_js_1 = require("../config/loader.js");
|
|
16
|
+
const store_js_1 = require("../cluster-state/store.js");
|
|
17
|
+
const worker_prompt_js_1 = require("./worker-prompt.js");
|
|
18
|
+
const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
|
|
19
|
+
const run_bootstrap_js_1 = require("./run-bootstrap.js");
|
|
20
|
+
const ledger_js_1 = require("./ledger.js");
|
|
21
|
+
const index_js_1 = require("../tracker/index.js");
|
|
22
|
+
const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
|
|
23
|
+
const CLAIM_TTL_MS = 30 * 60 * 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Determine the effective provider and dispatch mode using the 4-scenario decision tree:
|
|
26
|
+
* 1. --provider flag set → direct-worker (no fallback)
|
|
27
|
+
* 2. Provider in .polaris/config → direct-worker (no fallback)
|
|
28
|
+
* 3. Internal subagent available → delegated dispatch with fallback chain
|
|
29
|
+
* 4. Nothing available → pending-escalation (handled in attemptDelegatedAssignment)
|
|
30
|
+
*/
|
|
31
|
+
function roleContextToExecutionRole(role) {
|
|
32
|
+
switch (role) {
|
|
33
|
+
case "foreman":
|
|
34
|
+
return "orchestrator";
|
|
35
|
+
case "analyst":
|
|
36
|
+
return "analyst";
|
|
37
|
+
case "librarian":
|
|
38
|
+
return "librarian";
|
|
39
|
+
case "worker":
|
|
40
|
+
default:
|
|
41
|
+
return "worker";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveProviderAndMode(options, role, config) {
|
|
45
|
+
const defaultAdapter = "terminal-cli";
|
|
46
|
+
if (options.provider) {
|
|
47
|
+
return {
|
|
48
|
+
provider: options.provider,
|
|
49
|
+
mode: "direct-worker",
|
|
50
|
+
adapter: defaultAdapter,
|
|
51
|
+
selectionReason: "cli-provider-override",
|
|
52
|
+
overrideSource: "dispatch-flag",
|
|
53
|
+
providersTried: [options.provider],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Only catch config-load failures; policy violations must propagate to the caller.
|
|
57
|
+
let loaded;
|
|
58
|
+
let adapter = defaultAdapter;
|
|
59
|
+
try {
|
|
60
|
+
loaded = config ?? (0, loader_js_1.loadConfig)(options.repoRoot);
|
|
61
|
+
adapter = loaded.execution?.adapter ?? defaultAdapter;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {
|
|
65
|
+
provider: undefined,
|
|
66
|
+
mode: "delegated",
|
|
67
|
+
adapter: defaultAdapter,
|
|
68
|
+
selectionReason: "delegated-config-unavailable",
|
|
69
|
+
fallbackFrom: "config",
|
|
70
|
+
fallbackReason: "config-unavailable",
|
|
71
|
+
providersTried: [],
|
|
72
|
+
exhaustedReason: "config-unavailable",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const exec = loaded.execution;
|
|
76
|
+
const rolePolicy = exec?.providerPolicy?.[role];
|
|
77
|
+
const roleProviders = rolePolicy?.providers ?? [];
|
|
78
|
+
const rotation = exec?.rotation ?? [];
|
|
79
|
+
if (roleProviders.length > 0) {
|
|
80
|
+
// When a role policy is configured, filter the rotation to only include
|
|
81
|
+
// providers that are allowed by the policy. This ensures that a provider
|
|
82
|
+
// present in rotation but absent from the policy is never selected.
|
|
83
|
+
const allowedByPolicy = new Set(roleProviders);
|
|
84
|
+
const configuredProviders = new Set(Object.keys(exec?.providers ?? {}));
|
|
85
|
+
const filteredRotation = rotation.filter((p) => allowedByPolicy.has(p));
|
|
86
|
+
if (filteredRotation.length > 0 && filteredRotation[0]) {
|
|
87
|
+
return {
|
|
88
|
+
provider: filteredRotation[0],
|
|
89
|
+
mode: "direct-worker",
|
|
90
|
+
adapter,
|
|
91
|
+
selectionReason: "policy-filtered-rotation",
|
|
92
|
+
providersTried: [filteredRotation[0]],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// No rotation entry is in the policy — fall back to first policy provider
|
|
96
|
+
// that is actually configured. If none is configured, fail before mutation.
|
|
97
|
+
const availablePolicyProviders = roleProviders.filter((p) => configuredProviders.has(p));
|
|
98
|
+
if (availablePolicyProviders.length > 0 && availablePolicyProviders[0]) {
|
|
99
|
+
return {
|
|
100
|
+
provider: availablePolicyProviders[0],
|
|
101
|
+
mode: "direct-worker",
|
|
102
|
+
adapter,
|
|
103
|
+
selectionReason: "role-policy",
|
|
104
|
+
providersTried: [availablePolicyProviders[0]],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// No configured provider satisfies the policy — fail clearly before dispatch.
|
|
108
|
+
throw new Error(`provider dispatch forbidden: no configured provider satisfies the "${role}" role policy (policy=${roleProviders.join(",")})`);
|
|
109
|
+
}
|
|
110
|
+
const roleProvider = exec?.roles?.[role]?.provider;
|
|
111
|
+
if (roleProvider) {
|
|
112
|
+
return {
|
|
113
|
+
provider: roleProvider,
|
|
114
|
+
mode: "direct-worker",
|
|
115
|
+
adapter,
|
|
116
|
+
selectionReason: "role-config",
|
|
117
|
+
providersTried: [roleProvider],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (rotation.length > 0 && rotation[0]) {
|
|
121
|
+
return {
|
|
122
|
+
provider: rotation[0],
|
|
123
|
+
mode: "direct-worker",
|
|
124
|
+
adapter,
|
|
125
|
+
selectionReason: "config-rotation",
|
|
126
|
+
providersTried: [rotation[0]],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const providers = Object.keys(exec?.providers ?? {});
|
|
130
|
+
if (providers.length > 0 && providers[0]) {
|
|
131
|
+
return {
|
|
132
|
+
provider: providers[0],
|
|
133
|
+
mode: "direct-worker",
|
|
134
|
+
adapter,
|
|
135
|
+
selectionReason: "config-first-provider",
|
|
136
|
+
fallbackFrom: "rotation",
|
|
137
|
+
fallbackReason: "rotation-empty",
|
|
138
|
+
providersTried: [providers[0]],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
provider: undefined,
|
|
143
|
+
mode: "delegated",
|
|
144
|
+
adapter,
|
|
145
|
+
selectionReason: "delegated-no-provider",
|
|
146
|
+
providersTried: [],
|
|
147
|
+
exhaustedReason: "no-configured-provider",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function fail(message) {
|
|
151
|
+
process.stderr.write(`Error: ${message}
|
|
152
|
+
`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
function probeProviderSync(resolvedProvider, loadedConfig) {
|
|
156
|
+
try {
|
|
157
|
+
const providers = loadedConfig?.execution?.providers ?? {};
|
|
158
|
+
const providerCfg = providers[resolvedProvider];
|
|
159
|
+
if (!providerCfg) {
|
|
160
|
+
return { ok: false, error: `Unknown provider "${resolvedProvider}"` };
|
|
161
|
+
}
|
|
162
|
+
const expandedCommand = providerCfg.command
|
|
163
|
+
.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (_, name) => process.env[name] ?? '')
|
|
164
|
+
.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, name) => process.env[name] ?? '');
|
|
165
|
+
const cmd = expandedCommand.split(' ')[0] ?? expandedCommand;
|
|
166
|
+
if (!cmd) {
|
|
167
|
+
return { ok: false, error: `Provider "${resolvedProvider}" has no command configured` };
|
|
168
|
+
}
|
|
169
|
+
// Check if command is on PATH
|
|
170
|
+
const isAbs = cmd.startsWith('/');
|
|
171
|
+
if (isAbs) {
|
|
172
|
+
return (0, node_fs_1.existsSync)(cmd)
|
|
173
|
+
? { ok: true }
|
|
174
|
+
: { ok: false, error: `Provider command "${providerCfg.command}" not found on PATH` };
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
(0, node_child_process_1.execFileSync)('which', [cmd], { stdio: 'ignore' });
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return { ok: false, error: `Provider command "${providerCfg.command}" not found on PATH` };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function hasValidationWaiver(value) {
|
|
189
|
+
if (typeof value === "boolean")
|
|
190
|
+
return value;
|
|
191
|
+
if (typeof value === "string")
|
|
192
|
+
return value.trim().length > 0;
|
|
193
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
194
|
+
return Object.keys(value).length > 0;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
function isPlaceholderGoalText(goal) {
|
|
199
|
+
return /^[\s\[(<{'"`-]*(?:tbd|todo|placeholder|to be determined)\b/i.test(goal);
|
|
200
|
+
}
|
|
201
|
+
function detectPacketGenerationFailure(packet) {
|
|
202
|
+
if (packet.worker_role !== "impl")
|
|
203
|
+
return null;
|
|
204
|
+
const childId = packet.active_child || "unknown-child";
|
|
205
|
+
const allowedScope = Array.isArray(packet.instructions.allowed_scope)
|
|
206
|
+
? packet.instructions.allowed_scope
|
|
207
|
+
: [];
|
|
208
|
+
if (allowedScope.length === 0) {
|
|
209
|
+
return {
|
|
210
|
+
missingField: "allowed_scope",
|
|
211
|
+
message: `Packet generation failed for ${childId}: missing actionable allowed_scope.`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const issueBody = packet.instructions.issue_context?.body?.trim() ?? "";
|
|
215
|
+
const parsedGoal = issueBody ? (0, body_parser_js_1.parseIssueBody)(issueBody).goal.trim() : "";
|
|
216
|
+
const issueTitle = packet.instructions.issue_context?.title ?? childId;
|
|
217
|
+
const fallbackPromptGoal = `## Goal\nImplement ${childId}: "${issueTitle}".`;
|
|
218
|
+
const missingGoalFromBody = issueBody.length > 0 && parsedGoal.length === 0;
|
|
219
|
+
const placeholderGoal = packet.instructions.primary_goal.includes(fallbackPromptGoal) ||
|
|
220
|
+
missingGoalFromBody ||
|
|
221
|
+
isPlaceholderGoalText(parsedGoal);
|
|
222
|
+
if (placeholderGoal) {
|
|
223
|
+
return {
|
|
224
|
+
missingField: "primary_goal",
|
|
225
|
+
message: `Packet generation failed for ${childId}: primary_goal is placeholder or missing.`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const validationCommands = Array.isArray(packet.instructions.validation_commands)
|
|
229
|
+
? packet.instructions.validation_commands
|
|
230
|
+
: [];
|
|
231
|
+
const packetWithFlags = packet;
|
|
232
|
+
const validationWaiver = packetWithFlags.validation_waiver
|
|
233
|
+
?? packetWithFlags.instructions?.validation_waiver;
|
|
234
|
+
if (validationCommands.length === 0 && !hasValidationWaiver(validationWaiver)) {
|
|
235
|
+
return {
|
|
236
|
+
missingField: "validation_commands",
|
|
237
|
+
message: `Packet generation failed for ${childId}: validation_commands is empty and validation_waiver is missing.`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
function appendTelemetry(telemetryFile, event) {
|
|
243
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
|
|
244
|
+
(0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
|
|
245
|
+
}
|
|
246
|
+
function resolveTelemetryFile(state, repoRoot) {
|
|
247
|
+
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
248
|
+
return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
|
|
249
|
+
}
|
|
250
|
+
function canonicalPath(path) {
|
|
251
|
+
try {
|
|
252
|
+
return (0, node_fs_1.realpathSync)(path);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return path;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function absoluteResultFile(repoRoot, filePath) {
|
|
259
|
+
return (0, node_path_1.isAbsolute)(filePath) ? filePath : (0, node_path_1.resolve)(repoRoot, filePath);
|
|
260
|
+
}
|
|
261
|
+
function getCurrentBranch(repoRoot) {
|
|
262
|
+
try {
|
|
263
|
+
return (0, node_child_process_1.execFileSync)("git", ["branch", "--show-current"], {
|
|
264
|
+
cwd: repoRoot,
|
|
265
|
+
encoding: "utf-8",
|
|
266
|
+
}).trim();
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return process.env["POLARIS_BRANCH"] ?? "unknown";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get cluster-scoped packet directory path.
|
|
274
|
+
* Format: .polaris/clusters/<cluster-id>/packets/
|
|
275
|
+
*/
|
|
276
|
+
function getClusterPacketDir(repoRoot, clusterId) {
|
|
277
|
+
return (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "packets");
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get cluster-scoped results directory path.
|
|
281
|
+
* Format: .polaris/clusters/<cluster-id>/results/
|
|
282
|
+
*/
|
|
283
|
+
function getClusterResultDir(repoRoot, clusterId) {
|
|
284
|
+
return (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "results");
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Build dispatch artifact paths in cluster-scoped layout.
|
|
288
|
+
* Returns packet path and expected result path.
|
|
289
|
+
*/
|
|
290
|
+
function buildClusterArtifactPaths(repoRoot, clusterId, childId, dispatchId) {
|
|
291
|
+
const packetDir = getClusterPacketDir(repoRoot, clusterId);
|
|
292
|
+
const resultDir = getClusterResultDir(repoRoot, clusterId);
|
|
293
|
+
const filename = `${childId}-${dispatchId}.json`;
|
|
294
|
+
const packetPath = (0, node_path_1.join)(packetDir, filename);
|
|
295
|
+
const resultPath = (0, node_path_1.join)(resultDir, filename);
|
|
296
|
+
return {
|
|
297
|
+
packetPath,
|
|
298
|
+
resultPath,
|
|
299
|
+
relativePacketPath: (0, node_path_1.relative)(repoRoot, packetPath),
|
|
300
|
+
relativeResultPath: (0, node_path_1.relative)(repoRoot, resultPath),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Write packet artifact to cluster-scoped layout.
|
|
305
|
+
* Creates directories if needed.
|
|
306
|
+
*/
|
|
307
|
+
function writePacketArtifact(packetPath, packet) {
|
|
308
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(packetPath), { recursive: true });
|
|
309
|
+
(0, node_fs_1.writeFileSync)(packetPath, JSON.stringify(packet, null, 2), "utf-8");
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Determine dispatch mode based on provider presence.
|
|
313
|
+
*/
|
|
314
|
+
function determineDispatchMode(provider) {
|
|
315
|
+
return provider ? "direct-worker" : "delegated";
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Determine initial runtime state based on dispatch mode.
|
|
319
|
+
*/
|
|
320
|
+
function determineRuntimeState(mode) {
|
|
321
|
+
return mode === "delegated" ? "delegated" : "packet-created";
|
|
322
|
+
}
|
|
323
|
+
function emitProviderSelected(telemetryFile, dispatchId, runId, childId, decision) {
|
|
324
|
+
appendTelemetry(telemetryFile, {
|
|
325
|
+
event: "provider-selected",
|
|
326
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
327
|
+
dispatch_id: dispatchId,
|
|
328
|
+
run_id: runId,
|
|
329
|
+
child_id: childId,
|
|
330
|
+
requested_role: "worker",
|
|
331
|
+
selected_provider: decision.provider ?? null,
|
|
332
|
+
selected_adapter: decision.adapter,
|
|
333
|
+
selection_reason: decision.selectionReason,
|
|
334
|
+
...(decision.overrideSource ? { override_source: decision.overrideSource } : {}),
|
|
335
|
+
...(decision.fallbackFrom ? { fallback_from: decision.fallbackFrom } : {}),
|
|
336
|
+
...(decision.fallbackReason ? { fallback_reason: decision.fallbackReason } : {}),
|
|
337
|
+
...(decision.providersTried.length > 0 ? { providers_tried: decision.providersTried } : {}),
|
|
338
|
+
timestamp: new Date().toISOString(),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function emitProviderFallbackAttempted(telemetryFile, dispatchId, runId, childId, decision) {
|
|
342
|
+
if (!decision.fallbackFrom || !decision.fallbackReason)
|
|
343
|
+
return;
|
|
344
|
+
appendTelemetry(telemetryFile, {
|
|
345
|
+
event: "provider-fallback-attempted",
|
|
346
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
347
|
+
dispatch_id: dispatchId,
|
|
348
|
+
run_id: runId,
|
|
349
|
+
child_id: childId,
|
|
350
|
+
requested_role: "worker",
|
|
351
|
+
fallback_from: decision.fallbackFrom,
|
|
352
|
+
fallback_reason: decision.fallbackReason,
|
|
353
|
+
...(decision.providersTried.length > 0 ? { providers_tried: decision.providersTried } : {}),
|
|
354
|
+
timestamp: new Date().toISOString(),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function emitProviderExhausted(telemetryFile, dispatchId, runId, childId, decision) {
|
|
358
|
+
if (decision.provider)
|
|
359
|
+
return;
|
|
360
|
+
appendTelemetry(telemetryFile, {
|
|
361
|
+
event: "provider-exhausted",
|
|
362
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
363
|
+
dispatch_id: dispatchId,
|
|
364
|
+
run_id: runId,
|
|
365
|
+
child_id: childId,
|
|
366
|
+
requested_role: "worker",
|
|
367
|
+
selected_adapter: decision.adapter,
|
|
368
|
+
reason: decision.exhaustedReason ?? "no-provider-selected",
|
|
369
|
+
...(decision.providersTried.length > 0 ? { providers_tried: decision.providersTried } : {}),
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function getProviderPolicy(repoRoot) {
|
|
374
|
+
try {
|
|
375
|
+
const config = (0, loader_js_1.loadConfig)(repoRoot);
|
|
376
|
+
return config.execution.providerPolicy;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function assertProviderAllowedForRole(role, provider, policy, telemetryFile, dispatchId, runId, childId) {
|
|
383
|
+
const rolePolicy = policy?.[role];
|
|
384
|
+
if (!rolePolicy)
|
|
385
|
+
return;
|
|
386
|
+
if (rolePolicy.providers.length === 0) {
|
|
387
|
+
appendTelemetry(telemetryFile, {
|
|
388
|
+
event: "provider-forbidden",
|
|
389
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
390
|
+
dispatch_id: dispatchId,
|
|
391
|
+
run_id: runId,
|
|
392
|
+
child_id: childId,
|
|
393
|
+
requested_role: "worker",
|
|
394
|
+
selected_provider: provider ?? null,
|
|
395
|
+
reason: "role-disabled",
|
|
396
|
+
policy_providers: [],
|
|
397
|
+
timestamp: new Date().toISOString(),
|
|
398
|
+
});
|
|
399
|
+
throw new Error(`provider dispatch forbidden: role "${role}" is disabled by provider policy`);
|
|
400
|
+
}
|
|
401
|
+
if (!provider || !rolePolicy.providers.includes(provider)) {
|
|
402
|
+
appendTelemetry(telemetryFile, {
|
|
403
|
+
event: "provider-forbidden",
|
|
404
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
405
|
+
dispatch_id: dispatchId,
|
|
406
|
+
run_id: runId,
|
|
407
|
+
child_id: childId,
|
|
408
|
+
requested_role: "worker",
|
|
409
|
+
selected_provider: provider ?? null,
|
|
410
|
+
reason: "not-in-policy",
|
|
411
|
+
policy_providers: rolePolicy.providers,
|
|
412
|
+
timestamp: new Date().toISOString(),
|
|
413
|
+
});
|
|
414
|
+
throw new Error(`provider dispatch forbidden: provider "${provider ?? "none"}" is not allowed for role "${role}"`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Create a dispatch record with all required evidence.
|
|
419
|
+
*/
|
|
420
|
+
function createDispatchRecord(runId, clusterId, childId, packetPath, resultPath, roleContext, provider, providerSelectionReason, providerOverrideSource, providersTried) {
|
|
421
|
+
const dispatchMode = determineDispatchMode(provider);
|
|
422
|
+
const runtimeState = determineRuntimeState(dispatchMode);
|
|
423
|
+
return {
|
|
424
|
+
dispatch_id: (0, node_crypto_1.randomUUID)(),
|
|
425
|
+
child_id: childId,
|
|
426
|
+
run_id: runId,
|
|
427
|
+
cluster_id: clusterId,
|
|
428
|
+
packet_path: packetPath,
|
|
429
|
+
expected_result_path: resultPath,
|
|
430
|
+
provider,
|
|
431
|
+
provider_selection_reason: providerSelectionReason,
|
|
432
|
+
provider_override_source: providerOverrideSource,
|
|
433
|
+
providers_tried: providersTried && providersTried.length > 0 ? providersTried : undefined,
|
|
434
|
+
dispatched_at: new Date().toISOString(),
|
|
435
|
+
status: "dispatched",
|
|
436
|
+
dispatch_mode: dispatchMode,
|
|
437
|
+
runtime_state: runtimeState,
|
|
438
|
+
worker_id: (0, node_crypto_1.randomUUID)(),
|
|
439
|
+
session_id: null,
|
|
440
|
+
attachment_capable: false,
|
|
441
|
+
role: roleContext?.role,
|
|
442
|
+
role_authority: roleContext?.role_authority,
|
|
443
|
+
may_implement: roleContext?.may_implement,
|
|
444
|
+
session_type: roleContext ? (roleContext.role === 'worker' ? 'implementation' :
|
|
445
|
+
roleContext.role === 'foreman' ? 'coordination' :
|
|
446
|
+
roleContext.role === 'analyst' ? 'analysis' :
|
|
447
|
+
roleContext.role === 'librarian' ? 'documentation' : undefined) : undefined,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function getSubagentDispatcher() {
|
|
451
|
+
const host = globalThis;
|
|
452
|
+
return host.__POLARIS_AGENT_SUBTASK_DISPATCH__;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Emit a worker-assignment-attempted event (before each spawn attempt).
|
|
456
|
+
*/
|
|
457
|
+
function emitAssignmentAttempted(telemetryFile, dispatchId, runId, childId, assignmentType) {
|
|
458
|
+
appendTelemetry(telemetryFile, {
|
|
459
|
+
event: "worker-assignment-attempted",
|
|
460
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
461
|
+
dispatch_id: dispatchId,
|
|
462
|
+
run_id: runId,
|
|
463
|
+
child_id: childId,
|
|
464
|
+
assignment_type: assignmentType,
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Emit a worker-assigned event on success.
|
|
470
|
+
*/
|
|
471
|
+
function emitAssigned(telemetryFile, dispatchId, runId, childId, assignmentType, subagentSessionId, processPid, handoffToken) {
|
|
472
|
+
appendTelemetry(telemetryFile, {
|
|
473
|
+
event: "worker-assigned",
|
|
474
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
475
|
+
dispatch_id: dispatchId,
|
|
476
|
+
run_id: runId,
|
|
477
|
+
child_id: childId,
|
|
478
|
+
assignment_type: assignmentType,
|
|
479
|
+
...(subagentSessionId !== undefined ? { subagent_session_id: subagentSessionId } : {}),
|
|
480
|
+
...(processPid !== undefined ? { process_pid: processPid } : {}),
|
|
481
|
+
...(handoffToken !== undefined ? { handoff_token: handoffToken } : {}),
|
|
482
|
+
timestamp: new Date().toISOString(),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Emit a worker-assignment-failed event on each mechanism failure.
|
|
487
|
+
*/
|
|
488
|
+
function emitAssignmentFailed(telemetryFile, dispatchId, runId, childId, reason) {
|
|
489
|
+
appendTelemetry(telemetryFile, {
|
|
490
|
+
event: "worker-assignment-failed",
|
|
491
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
492
|
+
dispatch_id: dispatchId,
|
|
493
|
+
run_id: runId,
|
|
494
|
+
child_id: childId,
|
|
495
|
+
reason,
|
|
496
|
+
timestamp: new Date().toISOString(),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Emit an escalation-initiated event when all mechanisms are exhausted.
|
|
501
|
+
*/
|
|
502
|
+
function emitEscalationInitiated(telemetryFile, dispatchId, runId, childId, reason) {
|
|
503
|
+
appendTelemetry(telemetryFile, {
|
|
504
|
+
event: "escalation-initiated",
|
|
505
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
506
|
+
dispatch_id: dispatchId,
|
|
507
|
+
run_id: runId,
|
|
508
|
+
child_id: childId,
|
|
509
|
+
reason,
|
|
510
|
+
recommended_action: "manual-dispatch",
|
|
511
|
+
timestamp: new Date().toISOString(),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Attempt the delegated assignment fallback chain:
|
|
516
|
+
* 1. subagent spawn attempt
|
|
517
|
+
* 2. external-process fallback
|
|
518
|
+
* 3. human-handoff fallback
|
|
519
|
+
* 4. pending-escalation (all mechanisms exhausted)
|
|
520
|
+
*
|
|
521
|
+
* Emits telemetry events at each decision point (before state transitions).
|
|
522
|
+
* Returns the assignment outcome with evidence.
|
|
523
|
+
*/
|
|
524
|
+
function attemptDelegatedAssignment(telemetryFile, dispatchId, runId, childId, packet, providerPolicy) {
|
|
525
|
+
const now = new Date().toISOString();
|
|
526
|
+
const governedRole = roleContextToExecutionRole(packet.role_context.role);
|
|
527
|
+
const rolePolicy = providerPolicy?.[governedRole];
|
|
528
|
+
const allowNativeSubagent = rolePolicy?.allowNativeSubagent !== false;
|
|
529
|
+
// Step 1: Try subagent spawn (unless policy disables native subagents for this role)
|
|
530
|
+
if (!allowNativeSubagent) {
|
|
531
|
+
emitAssignmentFailed(telemetryFile, dispatchId, runId, childId, "native-subagent-not-allowed-for-role");
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
emitAssignmentAttempted(telemetryFile, dispatchId, runId, childId, "subagent");
|
|
535
|
+
const dispatcher = getSubagentDispatcher();
|
|
536
|
+
if (dispatcher) {
|
|
537
|
+
// Subagent available - attempt dispatch
|
|
538
|
+
const sessionId = (0, node_crypto_1.randomUUID)();
|
|
539
|
+
try {
|
|
540
|
+
void dispatcher(packet, { dispatchId, runId, childId, sessionId });
|
|
541
|
+
emitAssigned(telemetryFile, dispatchId, runId, childId, "subagent", sessionId);
|
|
542
|
+
return {
|
|
543
|
+
assignment: {
|
|
544
|
+
assigned_at: now,
|
|
545
|
+
assignment_type: "subagent",
|
|
546
|
+
subagent_session_id: sessionId,
|
|
547
|
+
},
|
|
548
|
+
session_id: sessionId,
|
|
549
|
+
attachment_capable: false,
|
|
550
|
+
runtime_state: "delegated",
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
// Dispatcher invocation failed, fall through to next mechanism
|
|
555
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
556
|
+
emitAssignmentFailed(telemetryFile, dispatchId, runId, childId, "no-subagent-support");
|
|
557
|
+
appendTelemetry(telemetryFile, {
|
|
558
|
+
event: "subagent-spawn-error",
|
|
559
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
560
|
+
dispatch_id: dispatchId,
|
|
561
|
+
run_id: runId,
|
|
562
|
+
child_id: childId,
|
|
563
|
+
error: errorMsg,
|
|
564
|
+
timestamp: new Date().toISOString(),
|
|
565
|
+
});
|
|
566
|
+
// Continue to external-process fallback below
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// Subagent unavailable
|
|
571
|
+
emitAssignmentFailed(telemetryFile, dispatchId, runId, childId, "no-subagent-support");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Step 2: Try external-process fallback (not available in this wave)
|
|
575
|
+
emitAssignmentAttempted(telemetryFile, dispatchId, runId, childId, "external-process");
|
|
576
|
+
emitAssignmentFailed(telemetryFile, dispatchId, runId, childId, "provider-unavailable");
|
|
577
|
+
// Step 3: Try human-handoff fallback (not available in this wave)
|
|
578
|
+
emitAssignmentAttempted(telemetryFile, dispatchId, runId, childId, "human-handoff");
|
|
579
|
+
emitAssignmentFailed(telemetryFile, dispatchId, runId, childId, "provider-unavailable");
|
|
580
|
+
// Step 4: All mechanisms exhausted — pending-escalation
|
|
581
|
+
const escalationReason = "No assignment mechanism available: subagent not supported, no external-process or human-handoff configured";
|
|
582
|
+
emitEscalationInitiated(telemetryFile, dispatchId, runId, childId, escalationReason);
|
|
583
|
+
return {
|
|
584
|
+
assignment: {
|
|
585
|
+
assigned_at: now,
|
|
586
|
+
assignment_type: "pending-escalation",
|
|
587
|
+
escalation_reason: escalationReason,
|
|
588
|
+
},
|
|
589
|
+
session_id: null,
|
|
590
|
+
attachment_capable: false,
|
|
591
|
+
runtime_state: "delegated",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function normalizeRunType(sessionType) {
|
|
595
|
+
return sessionType === "analyze" ? "analyze" : "implement";
|
|
596
|
+
}
|
|
597
|
+
function ledgerLastCommit(state) {
|
|
598
|
+
return state.last_commit && state.last_commit.length > 0 ? state.last_commit : null;
|
|
599
|
+
}
|
|
600
|
+
function appendDispatchLedgerEvent(repoRoot, state, childId) {
|
|
601
|
+
new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH)).append({
|
|
602
|
+
schema_version: 1,
|
|
603
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
604
|
+
event: "child-dispatched",
|
|
605
|
+
run_id: state.run_id,
|
|
606
|
+
run_type: normalizeRunType(state.session_type),
|
|
607
|
+
cluster_id: state.cluster_id,
|
|
608
|
+
issue_id: childId,
|
|
609
|
+
branch: state.branch ?? getCurrentBranch(repoRoot),
|
|
610
|
+
status: "child-dispatched",
|
|
611
|
+
completed_children: state.completed_children,
|
|
612
|
+
open_children: state.open_children,
|
|
613
|
+
next_child: childId,
|
|
614
|
+
last_commit: ledgerLastCommit(state),
|
|
615
|
+
pr_url: null,
|
|
616
|
+
timestamp: new Date().toISOString(),
|
|
617
|
+
dispatch_epoch: state.dispatch_boundary?.dispatch_epoch ?? 0,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function selectChild(state, requestedChild) {
|
|
621
|
+
// Note: active_child check now handled by assertNoActiveChildBeforeDispatch
|
|
622
|
+
// before this function is called
|
|
623
|
+
if (state.status === "blocked") {
|
|
624
|
+
fail("current-state.json status is blocked");
|
|
625
|
+
}
|
|
626
|
+
if (state.status === "cluster-complete") {
|
|
627
|
+
fail("current-state.json status is cluster-complete");
|
|
628
|
+
}
|
|
629
|
+
if (state.open_children.length === 0) {
|
|
630
|
+
fail("no open children exist");
|
|
631
|
+
}
|
|
632
|
+
if (requestedChild) {
|
|
633
|
+
if (!state.open_children.includes(requestedChild)) {
|
|
634
|
+
fail(`child ${requestedChild} is not open`);
|
|
635
|
+
}
|
|
636
|
+
// Validate all open (non-completed) dependencies are satisfied
|
|
637
|
+
const childMeta = state.open_children_meta?.[requestedChild];
|
|
638
|
+
const deps = childMeta?.dependencies ?? [];
|
|
639
|
+
const unmetDeps = deps.filter((dep) => state.open_children.includes(dep) && !state.completed_children.includes(dep));
|
|
640
|
+
if (unmetDeps.length > 0) {
|
|
641
|
+
fail(`child ${requestedChild} has unmet dependencies that must complete first: ${unmetDeps.join(", ")}`);
|
|
642
|
+
}
|
|
643
|
+
return requestedChild;
|
|
644
|
+
}
|
|
645
|
+
return state.open_children[0];
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Constructs a WorkerPacket for dispatching a specific child using the current loop state and repository context.
|
|
649
|
+
*
|
|
650
|
+
* The packet includes run/cluster/child identifiers, branch (derived from state or git), canonicalized paths for
|
|
651
|
+
* state and optional result files, prompt mode, and an `allowedScope` resolved from the child's issue body with a
|
|
652
|
+
* fallback to the cluster/root issue body when present.
|
|
653
|
+
*
|
|
654
|
+
* @param state - The current LoopState used to populate packet metadata
|
|
655
|
+
* @param childId - The target child identifier to include in the packet
|
|
656
|
+
* @param stateFile - Path to the loop state file to embed in the packet (will be canonicalized)
|
|
657
|
+
* @param telemetryFile - Path to the telemetry file to include in the packet
|
|
658
|
+
* @param repoRoot - Repository root used to determine branch and resolve non-absolute result paths
|
|
659
|
+
* @param resultFile - Optional path for the expected result file; non-absolute paths are resolved against `repoRoot`
|
|
660
|
+
* @returns The constructed WorkerPacket ready for dispatch
|
|
661
|
+
*/
|
|
662
|
+
function buildPacket(state, childId, stateFile, telemetryFile, repoRoot, resultFile) {
|
|
663
|
+
const childMeta = state.open_children_meta?.[childId];
|
|
664
|
+
// Hydrate body from cluster snapshot when runtime state lacks it.
|
|
665
|
+
const cachedBody = childMeta?.body;
|
|
666
|
+
const resolvedChildBody = (cachedBody && cachedBody.trim().length > 0)
|
|
667
|
+
? cachedBody
|
|
668
|
+
: (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, childId, repoRoot);
|
|
669
|
+
const issueContext = (childMeta || resolvedChildBody)
|
|
670
|
+
? {
|
|
671
|
+
id: childId,
|
|
672
|
+
title: childMeta?.title ?? childId,
|
|
673
|
+
key_requirements: [],
|
|
674
|
+
body: resolvedChildBody,
|
|
675
|
+
}
|
|
676
|
+
: undefined;
|
|
677
|
+
// Scope precedence: child body → parent/cluster-root body fallback.
|
|
678
|
+
const childScope = resolvedChildBody ? (0, body_parser_js_1.parseIssueBody)(resolvedChildBody).scope : [];
|
|
679
|
+
const cachedParentBody = state.open_children_meta?.[state.cluster_id]?.body;
|
|
680
|
+
const parentBodyForScope = (cachedParentBody && cachedParentBody.trim().length > 0)
|
|
681
|
+
? cachedParentBody
|
|
682
|
+
: (0, checkpoint_js_1.readBodyFromClusterSnapshot)(state.cluster_id, state.cluster_id, repoRoot) ?? '';
|
|
683
|
+
const resolvedScope = childScope.length > 0
|
|
684
|
+
? childScope
|
|
685
|
+
: (0, body_parser_js_1.parseIssueBody)(parentBodyForScope).scope;
|
|
686
|
+
return (0, worker_packet_js_1.compileImplPacket)({
|
|
687
|
+
runId: state.run_id,
|
|
688
|
+
clusterId: state.cluster_id,
|
|
689
|
+
childId,
|
|
690
|
+
branch: state.branch || getCurrentBranch(repoRoot),
|
|
691
|
+
stateFile: canonicalPath(stateFile),
|
|
692
|
+
telemetryFile,
|
|
693
|
+
issueContext,
|
|
694
|
+
allowedScope: resolvedScope.length > 0 ? resolvedScope : undefined,
|
|
695
|
+
maxConcurrentWorkers: 1,
|
|
696
|
+
promptMode: (0, worker_prompt_js_1.selectPromptMode)(childId, state),
|
|
697
|
+
resultFile: canonicalPath(absoluteResultFile(repoRoot, resultFile)),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function buildClusterStateFromLoopState(state, previousGeneration) {
|
|
701
|
+
const childOrder = [
|
|
702
|
+
...state.completed_children,
|
|
703
|
+
...state.open_children,
|
|
704
|
+
...(state.active_child ? [state.active_child] : []),
|
|
705
|
+
];
|
|
706
|
+
const seen = new Set();
|
|
707
|
+
const child_states = [];
|
|
708
|
+
for (const childId of childOrder) {
|
|
709
|
+
if (!childId || seen.has(childId))
|
|
710
|
+
continue;
|
|
711
|
+
seen.add(childId);
|
|
712
|
+
let status;
|
|
713
|
+
if (state.completed_children.includes(childId)) {
|
|
714
|
+
status = "done";
|
|
715
|
+
}
|
|
716
|
+
else if (childId === state.active_child) {
|
|
717
|
+
status = "dispatched";
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
status = "ready";
|
|
721
|
+
}
|
|
722
|
+
child_states.push({
|
|
723
|
+
id: childId,
|
|
724
|
+
status,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
schema_version: "1.0",
|
|
729
|
+
cluster_id: state.cluster_id,
|
|
730
|
+
state_generation: previousGeneration !== undefined ? previousGeneration + 1 : 1,
|
|
731
|
+
child_states,
|
|
732
|
+
claim_metadata: {},
|
|
733
|
+
packet_pointers: {},
|
|
734
|
+
result_pointers: {},
|
|
735
|
+
validation_results: {},
|
|
736
|
+
commits: {},
|
|
737
|
+
tracker_mutations: {},
|
|
738
|
+
blockers: [],
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function syncClusterDispatchState(state, childId, dispatchRecord, packetPath, repoRoot, custodyRecord) {
|
|
742
|
+
const existingState = (0, store_js_1.readClusterStateSync)(state.cluster_id, repoRoot);
|
|
743
|
+
const previousGeneration = existingState?.state_generation;
|
|
744
|
+
const clusterState = existingState ?? buildClusterStateFromLoopState(state, previousGeneration);
|
|
745
|
+
const claimedAt = dispatchRecord.dispatched_at;
|
|
746
|
+
const claimedAtMs = new Date(claimedAt).getTime();
|
|
747
|
+
const expiresAt = new Date(Number.isFinite(claimedAtMs) ? claimedAtMs + CLAIM_TTL_MS : Date.now() + CLAIM_TTL_MS).toISOString();
|
|
748
|
+
const knownChild = clusterState.child_states.some((child) => child.id === childId);
|
|
749
|
+
const child_states = knownChild
|
|
750
|
+
? clusterState.child_states.map((child) => child.id === childId
|
|
751
|
+
? {
|
|
752
|
+
...child,
|
|
753
|
+
status: "dispatched",
|
|
754
|
+
}
|
|
755
|
+
: child)
|
|
756
|
+
: [...clusterState.child_states, { id: childId, status: "dispatched" }];
|
|
757
|
+
// Record delivery branch custody on first dispatch; never overwrite an existing record.
|
|
758
|
+
const custodyFields = custodyRecord && !existingState?.delivery_branch
|
|
759
|
+
? {
|
|
760
|
+
base_branch: custodyRecord.base_branch,
|
|
761
|
+
base_sha: custodyRecord.base_sha,
|
|
762
|
+
delivery_branch: custodyRecord.delivery_branch,
|
|
763
|
+
}
|
|
764
|
+
: {};
|
|
765
|
+
(0, store_js_1.writeClusterStateSync)(state.cluster_id, {
|
|
766
|
+
...clusterState,
|
|
767
|
+
...custodyFields,
|
|
768
|
+
state_generation: clusterState.state_generation + 1,
|
|
769
|
+
child_states,
|
|
770
|
+
claim_metadata: {
|
|
771
|
+
...clusterState.claim_metadata,
|
|
772
|
+
[childId]: {
|
|
773
|
+
worker_id: dispatchRecord.worker_id ?? dispatchRecord.dispatch_id,
|
|
774
|
+
claimed_at: claimedAt,
|
|
775
|
+
expires_at: expiresAt,
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
packet_pointers: {
|
|
779
|
+
...clusterState.packet_pointers,
|
|
780
|
+
[childId]: packetPath,
|
|
781
|
+
},
|
|
782
|
+
}, repoRoot);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Dispatches one child task from the loop state: prepares a worker packet, persists artifacts, updates state, and emits telemetry.
|
|
786
|
+
*
|
|
787
|
+
* Performs child selection (or uses the provided childId), builds the packet, resolves provider and dispatch mode, writes cluster-scoped packet/result artifacts, creates and persists a dispatch record, attempts delegated assignment when required, synchronizes cluster dispatch state, appends ledger and telemetry events, and writes the packet JSON to stdout.
|
|
788
|
+
*
|
|
789
|
+
* @param options - DispatchOptions controlling where to read/write state and artifacts (includes `stateFile`, `repoRoot`, optional `childId`, optional `resultFile`, and an optional `provider` override)
|
|
790
|
+
*/
|
|
791
|
+
function runLoopDispatch(options) {
|
|
792
|
+
let state;
|
|
793
|
+
try {
|
|
794
|
+
const rawState = (0, checkpoint_js_1.readState)(options.stateFile);
|
|
795
|
+
const errors = (0, checkpoint_js_1.validateState)(rawState);
|
|
796
|
+
if (errors.length > 0) {
|
|
797
|
+
fail(`current-state.json is invalid:\n${errors.join("\n")}`);
|
|
798
|
+
}
|
|
799
|
+
state = rawState;
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
fail(`cannot read state file ${options.stateFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
803
|
+
}
|
|
804
|
+
const telemetryFile = resolveTelemetryFile(state, options.repoRoot);
|
|
805
|
+
// Load config once up front so it is available for both custody and provider selection.
|
|
806
|
+
let loadedConfig;
|
|
807
|
+
try {
|
|
808
|
+
loadedConfig = (0, loader_js_1.loadConfig)(options.repoRoot);
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
loadedConfig = undefined;
|
|
812
|
+
}
|
|
813
|
+
// ── Bootstrap seal enforcement ─────────────────────────────────────────────
|
|
814
|
+
// The run MUST have been initialized through `polaris loop bootstrap`.
|
|
815
|
+
// Hand-crafted state (no seal) is refused before any child is dispatched.
|
|
816
|
+
try {
|
|
817
|
+
(0, run_bootstrap_js_1.assertBootstrapSeal)(state, telemetryFile);
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
821
|
+
}
|
|
822
|
+
// ── Dispatch boundary enforcement ─────────────────────────────────────────
|
|
823
|
+
// Halt immediately if active_child is already set (orphaned dispatch).
|
|
824
|
+
// The parent/orchestrator MUST NOT re-dispatch or complete inline.
|
|
825
|
+
// Single-emission guard: check if this invalid-inline condition was already recorded
|
|
826
|
+
const invalidInlineAlreadyRecorded = state.step_cursor === "invalid-inline-attempt";
|
|
827
|
+
try {
|
|
828
|
+
(0, dispatch_boundary_js_1.assertNoActiveChildBeforeDispatch)(state, telemetryFile);
|
|
829
|
+
}
|
|
830
|
+
catch (err) {
|
|
831
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
832
|
+
// Only emit telemetry once per invalid-inline condition
|
|
833
|
+
if (!invalidInlineAlreadyRecorded) {
|
|
834
|
+
const event = {
|
|
835
|
+
event: "invalid-inline-attempt",
|
|
836
|
+
run_id: state.run_id,
|
|
837
|
+
child_id: state.active_child,
|
|
838
|
+
reason: msg,
|
|
839
|
+
timestamp: new Date().toISOString(),
|
|
840
|
+
};
|
|
841
|
+
appendTelemetry(telemetryFile, event);
|
|
842
|
+
const defaultTelemetryFile = (0, node_path_1.join)(options.repoRoot, ".taskchain_artifacts", "polaris-run", "runs", state.run_id, "telemetry.jsonl");
|
|
843
|
+
if (defaultTelemetryFile !== telemetryFile) {
|
|
844
|
+
appendTelemetry(defaultTelemetryFile, event);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
fail(msg);
|
|
848
|
+
}
|
|
849
|
+
// ── Branch custody enforcement ─────────────────────────────────────────────
|
|
850
|
+
// Implementation workers must never run on a protected base branch (main,
|
|
851
|
+
// master, etc.). The delivery branch is recorded in cluster-state on first
|
|
852
|
+
// dispatch and every subsequent dispatch is verified against it.
|
|
853
|
+
//
|
|
854
|
+
// direct-main mode (loop.allowBranchDivergence: true) skips custody checks
|
|
855
|
+
// to support repositories that intentionally commit straight to their default
|
|
856
|
+
// branch.
|
|
857
|
+
let custodyRecord;
|
|
858
|
+
{
|
|
859
|
+
let dispatchCurrentBranch = getCurrentBranch(options.repoRoot);
|
|
860
|
+
const directMainMode = loadedConfig?.loop?.allowBranchDivergence === true;
|
|
861
|
+
if (!directMainMode) {
|
|
862
|
+
const existingForCustody = (0, store_js_1.readClusterStateSync)(state.cluster_id, options.repoRoot);
|
|
863
|
+
if (existingForCustody?.delivery_branch) {
|
|
864
|
+
// Custody already recorded — current branch must match.
|
|
865
|
+
try {
|
|
866
|
+
(0, git_custody_js_1.assertDeliveryBranchMatch)(dispatchCurrentBranch, existingForCustody.delivery_branch);
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
// First dispatch — ensure we are on a delivery (non-base) branch.
|
|
874
|
+
if ((0, git_custody_js_1.isProtectedBranch)(dispatchCurrentBranch)) {
|
|
875
|
+
// Auto-create and switch to the cluster's delivery branch.
|
|
876
|
+
const deliveryBranchName = (0, git_custody_js_1.buildDeliveryBranchName)(state.cluster_id);
|
|
877
|
+
try {
|
|
878
|
+
(0, git_custody_js_1.ensureDeliveryBranch)(options.repoRoot, deliveryBranchName);
|
|
879
|
+
dispatchCurrentBranch = deliveryBranchName;
|
|
880
|
+
}
|
|
881
|
+
catch (err) {
|
|
882
|
+
fail(`Cannot create delivery branch "${deliveryBranchName}": ` +
|
|
883
|
+
`${err instanceof Error ? err.message : String(err)}. ` +
|
|
884
|
+
`Switch to a delivery branch before dispatching.`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// Record custody for this cluster.
|
|
888
|
+
const configBaseBranch = loadedConfig?.finalize?.targetBranch ?? "main";
|
|
889
|
+
custodyRecord = (0, git_custody_js_1.buildCustodyRecord)(options.repoRoot, dispatchCurrentBranch, configBaseBranch);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const childId = selectChild(state, options.childId);
|
|
894
|
+
const dispatchId = (0, node_crypto_1.randomUUID)();
|
|
895
|
+
// Compute canonical artifact paths before building the packet so that
|
|
896
|
+
// result_file_contract is always present in the saved JSON.
|
|
897
|
+
// options.resultFile acts as an override; the cluster-scoped path is the default.
|
|
898
|
+
const { packetPath, resultPath, relativePacketPath, relativeResultPath } = buildClusterArtifactPaths(options.repoRoot, state.cluster_id, childId, dispatchId);
|
|
899
|
+
const canonicalResultFile = options.resultFile
|
|
900
|
+
? canonicalPath(absoluteResultFile(options.repoRoot, options.resultFile))
|
|
901
|
+
: resultPath;
|
|
902
|
+
// Use canonicalResultFile as the effective result path when options.resultFile is provided
|
|
903
|
+
const effectiveResultPath = canonicalResultFile;
|
|
904
|
+
const relativeEffectiveResultPath = (0, node_path_1.relative)(options.repoRoot, effectiveResultPath);
|
|
905
|
+
// ── Build packet before writing state ───────────────────────────────────────
|
|
906
|
+
// We need to build the packet first to know its content for the artifact.
|
|
907
|
+
// Use a preliminary state to build the packet (we'll update state after).
|
|
908
|
+
const preliminaryState = {
|
|
909
|
+
...state,
|
|
910
|
+
active_child: childId,
|
|
911
|
+
next_open_child: childId,
|
|
912
|
+
step_cursor: "dispatch",
|
|
913
|
+
dispatch_boundary: (0, dispatch_boundary_js_1.advanceDispatchEpoch)(state.dispatch_boundary, childId),
|
|
914
|
+
};
|
|
915
|
+
const packet = buildPacket(preliminaryState, childId, options.stateFile, telemetryFile, options.repoRoot, canonicalResultFile);
|
|
916
|
+
const packetFailure = detectPacketGenerationFailure(packet);
|
|
917
|
+
if (packetFailure) {
|
|
918
|
+
appendTelemetry(telemetryFile, {
|
|
919
|
+
event: "packet-generation-failed",
|
|
920
|
+
run_id: state.run_id,
|
|
921
|
+
child_id: childId,
|
|
922
|
+
missing_field: packetFailure.missingField,
|
|
923
|
+
error: packetFailure.message,
|
|
924
|
+
timestamp: new Date().toISOString(),
|
|
925
|
+
});
|
|
926
|
+
fail(`${packetFailure.message} (missing_field=${packetFailure.missingField}, child_id=${childId})`);
|
|
927
|
+
}
|
|
928
|
+
const targetRole = roleContextToExecutionRole(packet.role_context.role);
|
|
929
|
+
// ── Resolve provider and dispatch mode ─────────────────────────────────────
|
|
930
|
+
// loadedConfig was loaded earlier (above bootstrap seal check)
|
|
931
|
+
let providerDecision;
|
|
932
|
+
try {
|
|
933
|
+
providerDecision = resolveProviderAndMode(options, targetRole, loadedConfig);
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
937
|
+
}
|
|
938
|
+
const { provider: resolvedProvider } = providerDecision;
|
|
939
|
+
const providerPolicy = loadedConfig?.execution.providerPolicy ?? getProviderPolicy(options.repoRoot);
|
|
940
|
+
try {
|
|
941
|
+
assertProviderAllowedForRole(targetRole, resolvedProvider, providerPolicy, telemetryFile, dispatchId, state.run_id, childId);
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
945
|
+
}
|
|
946
|
+
// ── Provider probe on first dispatch ──────────────────────────────────────
|
|
947
|
+
// Catches provider configuration failures before packet commit. Only runs on
|
|
948
|
+
// the first dispatch for this run (no completed children) in direct-worker
|
|
949
|
+
// mode. Best-effort — never blocks dispatch if probe infrastructure fails.
|
|
950
|
+
if (resolvedProvider && providerDecision.mode === "direct-worker" && state.completed_children.length === 0) {
|
|
951
|
+
try {
|
|
952
|
+
const probeResult = probeProviderSync(resolvedProvider, loadedConfig);
|
|
953
|
+
if (!probeResult.ok) {
|
|
954
|
+
appendTelemetry(telemetryFile, {
|
|
955
|
+
event: "provider-probe-failed",
|
|
956
|
+
run_id: state.run_id,
|
|
957
|
+
child_id: childId,
|
|
958
|
+
provider: resolvedProvider,
|
|
959
|
+
error: probeResult.error,
|
|
960
|
+
timestamp: new Date().toISOString(),
|
|
961
|
+
});
|
|
962
|
+
// Only hard-fail when the provider is explicitly unknown (not just missing from PATH)
|
|
963
|
+
if (probeResult.error?.startsWith(`Unknown provider`)) {
|
|
964
|
+
fail(`Provider probe failed for "${resolvedProvider}": ${probeResult.error}. Resolve the provider configuration before dispatching.`);
|
|
965
|
+
}
|
|
966
|
+
// Command not on PATH — emit warning to stderr but allow dispatch to proceed
|
|
967
|
+
process.stderr.write(`Warning: provider command for "${resolvedProvider}" not found on PATH. ` +
|
|
968
|
+
`Dispatch will proceed but execution may fail.\n`);
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
appendTelemetry(telemetryFile, {
|
|
972
|
+
event: "provider-probe-passed",
|
|
973
|
+
run_id: state.run_id,
|
|
974
|
+
child_id: childId,
|
|
975
|
+
provider: resolvedProvider,
|
|
976
|
+
timestamp: new Date().toISOString(),
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch (err) {
|
|
981
|
+
// Probe infrastructure unavailable — log and continue
|
|
982
|
+
appendTelemetry(telemetryFile, {
|
|
983
|
+
event: "provider-probe-skipped",
|
|
984
|
+
run_id: state.run_id,
|
|
985
|
+
child_id: childId,
|
|
986
|
+
provider: resolvedProvider,
|
|
987
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
988
|
+
timestamp: new Date().toISOString(),
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
// ── Write durable dispatch evidence ────────────────────────────────────────
|
|
993
|
+
// Create cluster-scoped packet artifact BEFORE updating state.
|
|
994
|
+
// This ensures "dispatched" only means a durable record exists.
|
|
995
|
+
// (packetPath / resultPath etc. were already computed above before packet build)
|
|
996
|
+
// Write packet artifact - this MUST succeed before we report dispatch success
|
|
997
|
+
try {
|
|
998
|
+
writePacketArtifact(packetPath, packet);
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1002
|
+
// Record the failure in telemetry
|
|
1003
|
+
appendTelemetry(telemetryFile, {
|
|
1004
|
+
event: "dispatch-failed",
|
|
1005
|
+
run_id: state.run_id,
|
|
1006
|
+
child_id: childId,
|
|
1007
|
+
error: `Failed to write packet artifact: ${errorMsg}`,
|
|
1008
|
+
timestamp: new Date().toISOString(),
|
|
1009
|
+
});
|
|
1010
|
+
fail(`Failed to write packet artifact to ${packetPath}: ${errorMsg}`);
|
|
1011
|
+
}
|
|
1012
|
+
// Create dispatch record with all evidence
|
|
1013
|
+
const dispatchRecord = createDispatchRecord(state.run_id, state.cluster_id, childId, relativePacketPath, relativeEffectiveResultPath, packet.role_context, resolvedProvider, providerDecision.selectionReason, providerDecision.overrideSource, providerDecision.providersTried);
|
|
1014
|
+
emitProviderFallbackAttempted(telemetryFile, dispatchRecord.dispatch_id, state.run_id, childId, providerDecision);
|
|
1015
|
+
emitProviderExhausted(telemetryFile, dispatchRecord.dispatch_id, state.run_id, childId, providerDecision);
|
|
1016
|
+
emitProviderSelected(telemetryFile, dispatchRecord.dispatch_id, state.run_id, childId, providerDecision);
|
|
1017
|
+
// ── Delegated assignment: attempt spawn with fallback chain ────────────────
|
|
1018
|
+
// For delegated mode (no provider), attempt subagent spawn now.
|
|
1019
|
+
// Emit all telemetry events BEFORE updating dispatch state.
|
|
1020
|
+
// Write complete assignment evidence BEFORE worker begins executing.
|
|
1021
|
+
if (dispatchRecord.dispatch_mode === "delegated") {
|
|
1022
|
+
const assignmentOutcome = attemptDelegatedAssignment(telemetryFile, dispatchRecord.dispatch_id, state.run_id, childId, packet, providerPolicy);
|
|
1023
|
+
dispatchRecord.worker_assignment = assignmentOutcome.assignment;
|
|
1024
|
+
dispatchRecord.session_id = assignmentOutcome.session_id;
|
|
1025
|
+
dispatchRecord.attachment_capable = assignmentOutcome.attachment_capable;
|
|
1026
|
+
dispatchRecord.runtime_state = assignmentOutcome.runtime_state;
|
|
1027
|
+
}
|
|
1028
|
+
// ── Update state atomically with dispatch record ───────────────────────────
|
|
1029
|
+
// Assignment evidence is populated in dispatchRecord BEFORE this write.
|
|
1030
|
+
// This upholds the evidence-before-execution invariant.
|
|
1031
|
+
const updatedState = {
|
|
1032
|
+
...preliminaryState,
|
|
1033
|
+
// Store the dispatch record in open_children_meta for the child
|
|
1034
|
+
open_children_meta: {
|
|
1035
|
+
...state.open_children_meta,
|
|
1036
|
+
[childId]: {
|
|
1037
|
+
...state.open_children_meta?.[childId],
|
|
1038
|
+
dispatch_record: dispatchRecord,
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
};
|
|
1042
|
+
try {
|
|
1043
|
+
syncClusterDispatchState(state, childId, dispatchRecord, relativePacketPath, options.repoRoot, custodyRecord);
|
|
1044
|
+
}
|
|
1045
|
+
catch (err) {
|
|
1046
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1047
|
+
appendTelemetry(telemetryFile, {
|
|
1048
|
+
event: "dispatch-failed",
|
|
1049
|
+
run_id: state.run_id,
|
|
1050
|
+
child_id: childId,
|
|
1051
|
+
error: `Failed to sync cluster-state: ${errorMsg}`,
|
|
1052
|
+
timestamp: new Date().toISOString(),
|
|
1053
|
+
});
|
|
1054
|
+
fail(`Failed to sync cluster-state for ${childId}: ${errorMsg}`);
|
|
1055
|
+
}
|
|
1056
|
+
(0, checkpoint_js_1.writeStateAtomic)(options.stateFile, updatedState);
|
|
1057
|
+
appendDispatchLedgerEvent(options.repoRoot, updatedState, childId);
|
|
1058
|
+
appendTelemetry(telemetryFile, {
|
|
1059
|
+
event: "child-dispatched",
|
|
1060
|
+
run_id: updatedState.run_id,
|
|
1061
|
+
child_id: childId,
|
|
1062
|
+
prompt_mode: packet.prompt_mode,
|
|
1063
|
+
prompt_estimated_tokens: packet.prompt_metrics.estimated_tokens,
|
|
1064
|
+
packet_path: packetPath,
|
|
1065
|
+
expected_result_path: effectiveResultPath,
|
|
1066
|
+
dispatch_mode: dispatchRecord.dispatch_mode,
|
|
1067
|
+
runtime_state: dispatchRecord.runtime_state,
|
|
1068
|
+
provider: dispatchRecord.provider ?? null,
|
|
1069
|
+
timestamp: new Date().toISOString(),
|
|
1070
|
+
});
|
|
1071
|
+
// ── Apply lifecycle transition for child-dispatch event ────────────────────
|
|
1072
|
+
// This is fire-and-forget: dispatch must not block on tracker mutations.
|
|
1073
|
+
// Errors are logged to telemetry but do not fail the dispatch.
|
|
1074
|
+
if (loadedConfig) {
|
|
1075
|
+
let adapter;
|
|
1076
|
+
try {
|
|
1077
|
+
adapter = (0, index_js_1.loadTrackerAdapter)(loadedConfig);
|
|
1078
|
+
}
|
|
1079
|
+
catch (err) {
|
|
1080
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1081
|
+
appendTelemetry(telemetryFile, {
|
|
1082
|
+
event: "lifecycle-transition-error",
|
|
1083
|
+
run_id: updatedState.run_id,
|
|
1084
|
+
child_id: childId,
|
|
1085
|
+
transition_event: "child-dispatch",
|
|
1086
|
+
error: `Failed to load tracker adapter: ${errorMsg}`,
|
|
1087
|
+
timestamp: new Date().toISOString(),
|
|
1088
|
+
});
|
|
1089
|
+
adapter = null;
|
|
1090
|
+
}
|
|
1091
|
+
const lifecyclePolicy = loadedConfig.tracker?.lifecyclePolicy;
|
|
1092
|
+
if (adapter || lifecyclePolicy) {
|
|
1093
|
+
try {
|
|
1094
|
+
const transitionService = new lifecycle_transition_js_1.LifecycleTransitionService();
|
|
1095
|
+
transitionService
|
|
1096
|
+
.applyTransitionSafe({
|
|
1097
|
+
adapter,
|
|
1098
|
+
policy: lifecyclePolicy,
|
|
1099
|
+
taskId: childId,
|
|
1100
|
+
event: "child-dispatch",
|
|
1101
|
+
evidence: {
|
|
1102
|
+
packetFile: packetPath,
|
|
1103
|
+
},
|
|
1104
|
+
timestamp: new Date().toISOString(),
|
|
1105
|
+
})
|
|
1106
|
+
.then((result) => {
|
|
1107
|
+
appendTelemetry(telemetryFile, {
|
|
1108
|
+
event: "lifecycle-transition-attempt",
|
|
1109
|
+
run_id: updatedState.run_id,
|
|
1110
|
+
child_id: childId,
|
|
1111
|
+
transition_event: result.event,
|
|
1112
|
+
target_state: result.targetState,
|
|
1113
|
+
applied: result.applied,
|
|
1114
|
+
skipped: result.skipped,
|
|
1115
|
+
skip_reason: result.skipReason,
|
|
1116
|
+
error: result.error,
|
|
1117
|
+
timestamp: result.timestamp,
|
|
1118
|
+
});
|
|
1119
|
+
})
|
|
1120
|
+
.catch((err) => {
|
|
1121
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1122
|
+
appendTelemetry(telemetryFile, {
|
|
1123
|
+
event: "lifecycle-transition-error",
|
|
1124
|
+
run_id: updatedState.run_id,
|
|
1125
|
+
child_id: childId,
|
|
1126
|
+
transition_event: "child-dispatch",
|
|
1127
|
+
error: errorMsg,
|
|
1128
|
+
timestamp: new Date().toISOString(),
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1134
|
+
appendTelemetry(telemetryFile, {
|
|
1135
|
+
event: "lifecycle-transition-error",
|
|
1136
|
+
run_id: updatedState.run_id,
|
|
1137
|
+
child_id: childId,
|
|
1138
|
+
transition_event: "child-dispatch",
|
|
1139
|
+
error: `Failed to create lifecycle transition service: ${errorMsg}`,
|
|
1140
|
+
timestamp: new Date().toISOString(),
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
process.stdout.write(`${JSON.stringify(packet, null, 2)}\n`);
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Check if the active child's dispatch has exceeded the acknowledgment timeout.
|
|
1149
|
+
* Returns the result of the check and emits recovery telemetry if timeout exceeded.
|
|
1150
|
+
*
|
|
1151
|
+
* Scenario B: worker_id present, acknowledged_at null, >launch_to_first_heartbeat_ms elapsed.
|
|
1152
|
+
* Scenario E: dispatched_at present, state handoff-pending, >stale_dispatch_timeout elapsed.
|
|
1153
|
+
*/
|
|
1154
|
+
function checkAcknowledgmentTimeout(options) {
|
|
1155
|
+
const launchTimeout = options.launchToFirstHeartbeatMs ?? 30_000;
|
|
1156
|
+
const staleTimeout = options.staleDispatchTimeoutMs ?? 1_800_000; // 30 min
|
|
1157
|
+
let state;
|
|
1158
|
+
try {
|
|
1159
|
+
state = (0, checkpoint_js_1.readState)(options.stateFile);
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
if (!state.active_child)
|
|
1165
|
+
return null;
|
|
1166
|
+
const childMeta = state.open_children_meta?.[state.active_child];
|
|
1167
|
+
const dr = childMeta?.dispatch_record;
|
|
1168
|
+
if (!dr)
|
|
1169
|
+
return null;
|
|
1170
|
+
const now = Date.now();
|
|
1171
|
+
const dispatchedAt = new Date(dr.dispatched_at).getTime();
|
|
1172
|
+
const elapsed = now - dispatchedAt;
|
|
1173
|
+
const telemetryFile = resolveTelemetryFile(state, options.repoRoot);
|
|
1174
|
+
// Scenario B: worker dispatched but not acknowledged within launch timeout
|
|
1175
|
+
if (dr.worker_id && !dr.first_heartbeat_at && elapsed > launchTimeout && dr.runtime_state !== "orphaned") {
|
|
1176
|
+
const recoveryEvent = {
|
|
1177
|
+
event: "child-recovery-initiated",
|
|
1178
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
1179
|
+
child_id: state.active_child,
|
|
1180
|
+
dispatch_id: dr.dispatch_id,
|
|
1181
|
+
recovery_reason: "no-acknowledgment",
|
|
1182
|
+
detected_at: new Date().toISOString(),
|
|
1183
|
+
};
|
|
1184
|
+
appendTelemetry(telemetryFile, recoveryEvent);
|
|
1185
|
+
const orphanEvent = {
|
|
1186
|
+
event: "child-orphaned",
|
|
1187
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
1188
|
+
child_id: state.active_child,
|
|
1189
|
+
dispatch_id: dr.dispatch_id,
|
|
1190
|
+
last_heartbeat_at: dr.last_heartbeat_at ?? null,
|
|
1191
|
+
orphaned_at: new Date().toISOString(),
|
|
1192
|
+
};
|
|
1193
|
+
appendTelemetry(telemetryFile, orphanEvent);
|
|
1194
|
+
// Update runtime_state to orphaned
|
|
1195
|
+
const updatedMeta = {
|
|
1196
|
+
...state.open_children_meta,
|
|
1197
|
+
[state.active_child]: {
|
|
1198
|
+
...childMeta,
|
|
1199
|
+
dispatch_record: { ...dr, runtime_state: "orphaned" },
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
(0, checkpoint_js_1.writeStateAtomic)(options.stateFile, { ...state, open_children_meta: updatedMeta });
|
|
1203
|
+
return { childId: state.active_child, dispatchId: dr.dispatch_id, orphaned: true, reason: "no-acknowledgment" };
|
|
1204
|
+
}
|
|
1205
|
+
// Scenario E: stale dispatch — dispatched but state hasn't progressed
|
|
1206
|
+
if (dr.runtime_state === "packet-created" && elapsed > staleTimeout) {
|
|
1207
|
+
const recoveryEvent = {
|
|
1208
|
+
event: "child-recovery-initiated",
|
|
1209
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
1210
|
+
child_id: state.active_child,
|
|
1211
|
+
dispatch_id: dr.dispatch_id,
|
|
1212
|
+
recovery_reason: "stale-dispatch",
|
|
1213
|
+
detected_at: new Date().toISOString(),
|
|
1214
|
+
};
|
|
1215
|
+
appendTelemetry(telemetryFile, recoveryEvent);
|
|
1216
|
+
const orphanEvent = {
|
|
1217
|
+
event: "child-orphaned",
|
|
1218
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
1219
|
+
child_id: state.active_child,
|
|
1220
|
+
dispatch_id: dr.dispatch_id,
|
|
1221
|
+
last_heartbeat_at: null,
|
|
1222
|
+
orphaned_at: new Date().toISOString(),
|
|
1223
|
+
};
|
|
1224
|
+
appendTelemetry(telemetryFile, orphanEvent);
|
|
1225
|
+
const updatedMeta = {
|
|
1226
|
+
...state.open_children_meta,
|
|
1227
|
+
[state.active_child]: {
|
|
1228
|
+
...childMeta,
|
|
1229
|
+
dispatch_record: { ...dr, runtime_state: "orphaned" },
|
|
1230
|
+
},
|
|
1231
|
+
};
|
|
1232
|
+
(0, checkpoint_js_1.writeStateAtomic)(options.stateFile, { ...state, open_children_meta: updatedMeta });
|
|
1233
|
+
return { childId: state.active_child, dispatchId: dr.dispatch_id, orphaned: true, reason: "stale-dispatch" };
|
|
1234
|
+
}
|
|
1235
|
+
return { childId: state.active_child, dispatchId: dr.dispatch_id, orphaned: false };
|
|
1236
|
+
}
|