@lsctech/polaris 0.4.9 → 0.5.2
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/dist/autoresearch/gates.js +47 -0
- package/dist/autoresearch/proposal.js +183 -1
- package/dist/autoresearch/score.js +276 -0
- package/dist/cli/init.js +17 -0
- package/dist/cluster-state/store.js +70 -1
- package/dist/config/defaults.js +25 -0
- package/dist/config/validator.js +390 -0
- package/dist/finalize/artifact-policy.js +3 -1
- package/dist/finalize/index.js +73 -17
- package/dist/finalize/linear.js +45 -0
- package/dist/finalize/run-report.js +68 -12
- package/dist/loop/adapters/agent-subtask.js +19 -0
- package/dist/loop/adapters/cli-subtask-bridge.js +0 -1
- package/dist/loop/adapters/terminal-cli.js +194 -14
- package/dist/loop/checkpoint.js +40 -0
- package/dist/loop/dispatch-boundary.js +29 -0
- package/dist/loop/dispatch.js +193 -22
- package/dist/loop/orphan-recovery.js +56 -0
- package/dist/loop/parent.js +197 -22
- package/dist/loop/router/engine.js +229 -0
- package/dist/loop/router/index.js +5 -0
- package/dist/loop/router/types.js +2 -0
- package/dist/loop/worker-packet.js +16 -0
- package/dist/qc/artifacts.js +117 -0
- package/dist/qc/attribution.js +266 -0
- package/dist/qc/autofix.js +77 -0
- package/dist/qc/index.js +30 -0
- package/dist/qc/orchestration.js +150 -0
- package/dist/qc/policy.js +70 -0
- package/dist/qc/provider.js +28 -0
- package/dist/qc/providers/coderabbit.js +214 -0
- package/dist/qc/providers/index.js +5 -0
- package/dist/qc/registry.js +16 -0
- package/dist/qc/routing.js +55 -0
- package/dist/qc/runner.js +137 -0
- package/dist/qc/schemas.js +110 -0
- package/dist/qc/security-category.js +11 -0
- package/dist/qc/severity.js +78 -0
- package/dist/qc/triggers.js +92 -0
- package/dist/qc/types.js +9 -0
- package/dist/runtime/scheduling/child-selector.js +81 -0
- package/package.json +1 -1
package/dist/finalize/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const local_graph_js_1 = require("../tracker/local-graph.js");
|
|
|
29
29
|
const index_js_1 = require("../tracker/sync/index.js");
|
|
30
30
|
const finalize_evidence_js_1 = require("../loop/finalize-evidence.js");
|
|
31
31
|
const delivery_integrity_js_1 = require("./delivery-integrity.js");
|
|
32
|
+
const index_js_2 = require("../qc/index.js");
|
|
32
33
|
function getBranch(repoRoot) {
|
|
33
34
|
try {
|
|
34
35
|
return (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
@@ -178,6 +179,36 @@ function checkLibrarianGate(repoRoot, clusterId) {
|
|
|
178
179
|
return `Librarian result gate error: ${err instanceof Error ? err.message : String(err)}`;
|
|
179
180
|
}
|
|
180
181
|
}
|
|
182
|
+
function resolveQcTelemetryFile(state, repoRoot) {
|
|
183
|
+
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
184
|
+
return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
|
|
185
|
+
}
|
|
186
|
+
async function runQcGate(options) {
|
|
187
|
+
const { config, state, repoRoot, branch, trigger, prUrl, stepLabel } = options;
|
|
188
|
+
const registry = (0, index_js_2.createDefaultQcRegistry)();
|
|
189
|
+
const result = await (0, index_js_2.runQcAtTrigger)({
|
|
190
|
+
config,
|
|
191
|
+
registry,
|
|
192
|
+
trigger,
|
|
193
|
+
prUrl,
|
|
194
|
+
repoRoot,
|
|
195
|
+
runId: state.run_id,
|
|
196
|
+
clusterId: state.cluster_id,
|
|
197
|
+
branch,
|
|
198
|
+
telemetryFile: resolveQcTelemetryFile(state, repoRoot),
|
|
199
|
+
state,
|
|
200
|
+
});
|
|
201
|
+
if (result.action === "pass") {
|
|
202
|
+
console.log(`${stepLabel} QC ${trigger} passed: ${result.summary}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (result.action === "follow-up") {
|
|
206
|
+
console.warn(`${stepLabel} QC ${trigger} produced follow-up work: ${result.summary}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
process.stderr.write(`${stepLabel} QC ${trigger} blocked finalize: ${result.summary}\n`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
181
212
|
async function runFinalize(options) {
|
|
182
213
|
const { repoRoot, stateFile, dryRun, skipDelivery, skipLibrarian } = options;
|
|
183
214
|
const config = (0, loader_js_1.loadConfig)(repoRoot);
|
|
@@ -356,6 +387,16 @@ async function runFinalize(options) {
|
|
|
356
387
|
}
|
|
357
388
|
}
|
|
358
389
|
}
|
|
390
|
+
// Step 5.8: Completed-cluster QC trigger (when configured)
|
|
391
|
+
// Runs after all authoritative gates and before final commit/delivery.
|
|
392
|
+
await runQcGate({
|
|
393
|
+
config: config.qc,
|
|
394
|
+
state,
|
|
395
|
+
repoRoot,
|
|
396
|
+
branch,
|
|
397
|
+
trigger: "completed-cluster",
|
|
398
|
+
stepLabel: "[5.8/14]",
|
|
399
|
+
});
|
|
359
400
|
// Step 6: Tracker Reconciliation
|
|
360
401
|
// LinearAdapter is sync-in only; only McpBridgeAdapter supports full reconciliation.
|
|
361
402
|
const trackerType = config.tracker?.adapter;
|
|
@@ -392,28 +433,32 @@ async function runFinalize(options) {
|
|
|
392
433
|
else {
|
|
393
434
|
console.warn(`[6/14] Unknown tracker adapter '${trackerType}' — skipping reconciliation.`);
|
|
394
435
|
}
|
|
395
|
-
// Step 7:
|
|
396
|
-
|
|
436
|
+
// Step 7: Closeout Librarian gate (pre-commit preflight)
|
|
437
|
+
// Must run before the finalize commit so that a missing or invalid librarian
|
|
438
|
+
// result aborts before any mutating git I/O.
|
|
439
|
+
if (!skipDelivery) {
|
|
440
|
+
if (!skipLibrarian) {
|
|
441
|
+
console.log("[7/14] Checking Closeout Librarian gate...");
|
|
442
|
+
const librarianBlocker = checkLibrarianGate(repoRoot, state.cluster_id);
|
|
443
|
+
if (librarianBlocker) {
|
|
444
|
+
process.stderr.write(`finalize aborted: Closeout Librarian gate failed.\n${librarianBlocker}\n`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
console.log("[7/14] Closeout Librarian gate passed.");
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
console.log("[7/14] Closeout Librarian gate skipped (--skip-librarian).");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Step 8: Single final commit: source changes + durable Polaris artifacts
|
|
454
|
+
console.log("[8/14] Committing durable Polaris state + map..."); // Step count updated
|
|
397
455
|
const resolvedStateFile = (0, node_path_1.resolve)(stateFile);
|
|
398
456
|
(0, _06_commit_js_1.stepCommit)(repoRoot, state, resolvedStateFile, reportPath);
|
|
399
457
|
if (skipDelivery) {
|
|
400
|
-
console.log("[
|
|
401
|
-
console.log("polaris finalize steps 1–
|
|
458
|
+
console.log("[9–14/14] Delivery skipped (--skip-delivery).");
|
|
459
|
+
console.log("polaris finalize steps 1–8 complete.");
|
|
402
460
|
return;
|
|
403
461
|
}
|
|
404
|
-
// Step 8: Closeout Librarian gate
|
|
405
|
-
if (!skipLibrarian) {
|
|
406
|
-
console.log("[8/14] Checking Closeout Librarian gate...");
|
|
407
|
-
const librarianBlocker = checkLibrarianGate(repoRoot, state.cluster_id);
|
|
408
|
-
if (librarianBlocker) {
|
|
409
|
-
process.stderr.write(`finalize aborted: Closeout Librarian gate failed.\n${librarianBlocker}\n`);
|
|
410
|
-
process.exit(1);
|
|
411
|
-
}
|
|
412
|
-
console.log("[8/14] Closeout Librarian gate passed.");
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
console.log("[8/14] Closeout Librarian gate skipped (--skip-librarian).");
|
|
416
|
-
}
|
|
417
462
|
// Step 9: git push
|
|
418
463
|
console.log("[9/14] Pushing branch...");
|
|
419
464
|
(0, _07_push_js_1.stepPush)(repoRoot, branch);
|
|
@@ -421,6 +466,17 @@ async function runFinalize(options) {
|
|
|
421
466
|
const prDraft = config.finalize?.prDraft ?? true;
|
|
422
467
|
console.log("[10/14] Creating draft PR...");
|
|
423
468
|
const prUrl = (0, _08_create_pr_js_1.stepCreatePr)(repoRoot, branch, state, prDraft);
|
|
469
|
+
// Step 10.5: PR-required QC trigger (when configured)
|
|
470
|
+
// Providers that require a PR URL run here after the PR is created.
|
|
471
|
+
await runQcGate({
|
|
472
|
+
config: config.qc,
|
|
473
|
+
state,
|
|
474
|
+
repoRoot,
|
|
475
|
+
branch,
|
|
476
|
+
trigger: "pr",
|
|
477
|
+
prUrl,
|
|
478
|
+
stepLabel: "[10.5/14]",
|
|
479
|
+
});
|
|
424
480
|
// Step 11: Write PR URL to current-state.json
|
|
425
481
|
console.log("[11/14] Writing PR URL to state...");
|
|
426
482
|
state = (0, _09_update_state_js_1.stepUpdateState)(resolvedStateFile, state, prUrl);
|
package/dist/finalize/linear.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.assertNotDoneState = assertNotDoneState;
|
|
|
4
4
|
exports.findReviewState = findReviewState;
|
|
5
5
|
exports.postLinearComment = postLinearComment;
|
|
6
6
|
exports.updateLinearIssueAfterFinalize = updateLinearIssueAfterFinalize;
|
|
7
|
+
exports.buildFollowUpIssuePayload = buildFollowUpIssuePayload;
|
|
8
|
+
exports.createLinearFollowUpIssue = createLinearFollowUpIssue;
|
|
7
9
|
const node_https_1 = require("node:https");
|
|
8
10
|
const lifecycle_policy_js_1 = require("../tracker/lifecycle-policy.js");
|
|
9
11
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -211,3 +213,46 @@ async function updateLinearIssueAfterFinalize(options) {
|
|
|
211
213
|
throw new Error(`Linear commentCreate failed: ${JSON.stringify(commentData.commentCreate)}`);
|
|
212
214
|
}
|
|
213
215
|
}
|
|
216
|
+
async function findIssueTeamId(issueId, apiKey) {
|
|
217
|
+
const issueData = await linearGraphQL(`query GetIssueTeam($id: String!) { issue(id: $id) { team { id } } }`, { id: issueId }, apiKey);
|
|
218
|
+
return issueData.issue?.team?.id ?? null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Build the GraphQL variables for a Linear follow-up issue.
|
|
222
|
+
* Exported separately so tests can inspect the payload shape.
|
|
223
|
+
*/
|
|
224
|
+
function buildFollowUpIssuePayload(options) {
|
|
225
|
+
const input = {
|
|
226
|
+
title: options.title,
|
|
227
|
+
description: options.description,
|
|
228
|
+
parentId: options.parentIssueId,
|
|
229
|
+
teamId: options.teamId,
|
|
230
|
+
};
|
|
231
|
+
if (options.stateId)
|
|
232
|
+
input.stateId = options.stateId;
|
|
233
|
+
if (options.labelIds && options.labelIds.length > 0)
|
|
234
|
+
input.labelIds = options.labelIds;
|
|
235
|
+
return {
|
|
236
|
+
query: `mutation CreateFollowUpIssue($input: IssueCreateInput!) {
|
|
237
|
+
issueCreate(input: $input) { success issue { id identifier url } }
|
|
238
|
+
}`,
|
|
239
|
+
variables: { input },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Create a Linear follow-up issue linked to a parent issue.
|
|
244
|
+
*
|
|
245
|
+
* This is the repair path for QC findings that are routed to follow-up work.
|
|
246
|
+
*/
|
|
247
|
+
async function createLinearFollowUpIssue(options) {
|
|
248
|
+
const teamId = options.teamId ?? await findIssueTeamId(options.parentIssueId, options.apiKey);
|
|
249
|
+
if (!teamId) {
|
|
250
|
+
throw new Error(`Unable to resolve Linear team for parent issue ${options.parentIssueId}`);
|
|
251
|
+
}
|
|
252
|
+
const payload = buildFollowUpIssuePayload({ ...options, teamId });
|
|
253
|
+
const data = await linearGraphQL(payload.query, payload.variables, options.apiKey);
|
|
254
|
+
if (data.issueCreate?.success !== true || !data.issueCreate?.issue) {
|
|
255
|
+
throw new Error(`Linear issueCreate failed: ${JSON.stringify(data.issueCreate)}`);
|
|
256
|
+
}
|
|
257
|
+
return data.issueCreate.issue;
|
|
258
|
+
}
|
|
@@ -1,8 +1,61 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateRunReport = generateRunReport;
|
|
4
|
+
function renderQcSection(qcSummary) {
|
|
5
|
+
if (!qcSummary)
|
|
6
|
+
return "";
|
|
7
|
+
const { total_findings, blocking_findings, autofixed_findings, repaired_findings, waived_findings, unvalidated_findings, open_by_severity, blocks_delivery, qc_run_count, weighted_open_score, qc_penalty, provider_breakdown, routing_breakdown } = qcSummary;
|
|
8
|
+
const openTotal = open_by_severity.critical + open_by_severity.high +
|
|
9
|
+
open_by_severity.medium + open_by_severity.low + open_by_severity.info;
|
|
10
|
+
const deliveryStatus = blocks_delivery
|
|
11
|
+
? "**BLOCKED** — unresolved critical/high findings must be addressed before delivery"
|
|
12
|
+
: "Not blocking delivery";
|
|
13
|
+
const providers = Object.entries(provider_breakdown)
|
|
14
|
+
.map(([provider, summary]) => `| ${provider} | ${summary.total} | ${summary.blocking} | ${summary.unvalidated} |`)
|
|
15
|
+
.join("\n");
|
|
16
|
+
const routing = `| original-worker | ${routing_breakdown.original_worker} |
|
|
17
|
+
| repair-worker | ${routing_breakdown.repair_worker} |
|
|
18
|
+
| follow-up | ${routing_breakdown.follow_up} |
|
|
19
|
+
| operator-review | ${routing_breakdown.operator_review} |
|
|
20
|
+
| unset | ${routing_breakdown.unset} |`;
|
|
21
|
+
const solImpactValue = qc_penalty > 0
|
|
22
|
+
? `-${(qc_penalty * 100).toFixed(1)}% (weighted open score ${weighted_open_score.toFixed(2)})`
|
|
23
|
+
: `none (weighted open score ${weighted_open_score.toFixed(2)})`;
|
|
24
|
+
return `
|
|
25
|
+
## QC summary
|
|
26
|
+
|
|
27
|
+
| Metric | Value |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **QC runs** | ${qc_run_count} |
|
|
30
|
+
| **Delivery status** | ${deliveryStatus} |
|
|
31
|
+
| **Total findings** | ${total_findings} (${unvalidated_findings} unvalidated/provider-noise excluded from scoring) |
|
|
32
|
+
| **SOL score impact** | ${solImpactValue} |
|
|
33
|
+
|
|
34
|
+
| Status | Count |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Blocking (critical/high, open) | ${blocking_findings} |
|
|
37
|
+
| Open (all severities) | ${openTotal} |
|
|
38
|
+
| Autofixed | ${autofixed_findings} |
|
|
39
|
+
| Repaired | ${repaired_findings} |
|
|
40
|
+
| Waived | ${waived_findings} |
|
|
41
|
+
|
|
42
|
+
| **Open by severity** | critical=${open_by_severity.critical} high=${open_by_severity.high} medium=${open_by_severity.medium} low=${open_by_severity.low} info=${open_by_severity.info} |
|
|
43
|
+
|
|
44
|
+
### Providers
|
|
45
|
+
|
|
46
|
+
| Provider | Total | Blocking | Unvalidated |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
${providers || "| _none_ | — | — | — |"}
|
|
49
|
+
|
|
50
|
+
### Repair routing
|
|
51
|
+
|
|
52
|
+
| Decision | Count |
|
|
53
|
+
|---|---|
|
|
54
|
+
${routing}
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
4
57
|
function generateRunReport(data) {
|
|
5
|
-
const { state, branch, validationPassed, prUrl, artifacts, notes } = data;
|
|
58
|
+
const { state, branch, validationPassed, prUrl, artifacts, notes, qcSummary } = data;
|
|
6
59
|
const total = state.completed_children.length + state.open_children.length;
|
|
7
60
|
const completedCount = state.completed_children.length;
|
|
8
61
|
const childRows = state.completed_children
|
|
@@ -18,19 +71,22 @@ function generateRunReport(data) {
|
|
|
18
71
|
const blockerNote = state.blocker
|
|
19
72
|
? `\n**Blocker:** ${state.blocker.reason} (child: ${state.blocker.child_id})\n`
|
|
20
73
|
: "";
|
|
74
|
+
const qcSection = renderQcSection(qcSummary);
|
|
21
75
|
return `# Run Report: ${state.run_id}
|
|
22
76
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
**
|
|
26
|
-
**
|
|
27
|
-
**
|
|
28
|
-
${
|
|
29
|
-
|
|
77
|
+
| Field | Value |
|
|
78
|
+
|---|---|
|
|
79
|
+
| **Status** | ${state.status} |
|
|
80
|
+
| **Branch** | ${branch} |
|
|
81
|
+
| **PR** | ${prUrl ?? "TBD — set at delivery step 9"} |
|
|
82
|
+
| **Children completed** | ${completedCount} of ${total} |
|
|
83
|
+
| **Validation** | ${validationPassed ? "passed" : "failed"} |
|
|
84
|
+
${blockerNote}
|
|
85
|
+
## Children
|
|
30
86
|
|
|
31
|
-
| ID | Title | Commit | Status |
|
|
32
|
-
|---|---|---|---|
|
|
33
|
-
${childRows || "_No children recorded_"}
|
|
87
|
+
| ID | Title | Commit | Status |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
${childRows || "_No children recorded_"}
|
|
34
90
|
|
|
35
91
|
## Artifacts produced
|
|
36
92
|
|
|
@@ -39,7 +95,7 @@ ${artifactList}
|
|
|
39
95
|
## Validation summary
|
|
40
96
|
|
|
41
97
|
${validationPassed ? "Map validate passed. Schema validate passed." : "One or more validations failed — see step output above."}
|
|
42
|
-
|
|
98
|
+
${qcSection}
|
|
43
99
|
## Notes
|
|
44
100
|
|
|
45
101
|
${notesList}
|
|
@@ -50,6 +50,7 @@ class AgentSubtaskAdapter {
|
|
|
50
50
|
const label = packet.active_child || ((0, worker_packet_js_1.isWorkerPacket)(packet) ? packet.worker_role : 'worker');
|
|
51
51
|
const commandRun = `agent-subtask:${label}`;
|
|
52
52
|
const provider = options.provider || "agent-subtask";
|
|
53
|
+
const routerEvidence = options.routerDecision;
|
|
53
54
|
if (!this.dispatcher) {
|
|
54
55
|
const error = "Native ephemeral agent subtask dispatch is unavailable in this host environment. " +
|
|
55
56
|
"Use manual handoff or a configured terminal-cli adapter.";
|
|
@@ -60,6 +61,10 @@ class AgentSubtaskAdapter {
|
|
|
60
61
|
command_run: commandRun,
|
|
61
62
|
summary: error,
|
|
62
63
|
stderr: error,
|
|
64
|
+
failure_origin: "provider-launch",
|
|
65
|
+
failure_category: "provider-unavailable",
|
|
66
|
+
fallback_eligible: true,
|
|
67
|
+
router_evidence: routerEvidence,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
70
|
if ((0, worker_packet_js_1.isWorkerPacket)(packet) && packet.worker_role === 'impl') {
|
|
@@ -78,6 +83,10 @@ class AgentSubtaskAdapter {
|
|
|
78
83
|
warnings: ["empty-allowed-scope"],
|
|
79
84
|
}),
|
|
80
85
|
stderr: blockedMsg,
|
|
86
|
+
failure_origin: "provider-launch",
|
|
87
|
+
failure_category: "launch-error",
|
|
88
|
+
fallback_eligible: false,
|
|
89
|
+
router_evidence: routerEvidence,
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
92
|
}
|
|
@@ -94,6 +103,7 @@ class AgentSubtaskAdapter {
|
|
|
94
103
|
next_action: "resume-parent",
|
|
95
104
|
warnings: ["dry-run"],
|
|
96
105
|
}),
|
|
106
|
+
router_evidence: routerEvidence,
|
|
97
107
|
};
|
|
98
108
|
}
|
|
99
109
|
const instructions = (0, worker_instructions_js_1.buildWorkerInstructions)(packet);
|
|
@@ -113,6 +123,10 @@ class AgentSubtaskAdapter {
|
|
|
113
123
|
command_run: commandRun,
|
|
114
124
|
summary: validationError,
|
|
115
125
|
stderr: validationError,
|
|
126
|
+
failure_origin: "worker-execution",
|
|
127
|
+
failure_category: "worker-failure",
|
|
128
|
+
fallback_eligible: false,
|
|
129
|
+
router_evidence: routerEvidence,
|
|
116
130
|
};
|
|
117
131
|
}
|
|
118
132
|
return {
|
|
@@ -120,6 +134,7 @@ class AgentSubtaskAdapter {
|
|
|
120
134
|
provider_used: provider,
|
|
121
135
|
command_run: commandRun,
|
|
122
136
|
summary,
|
|
137
|
+
router_evidence: routerEvidence,
|
|
123
138
|
};
|
|
124
139
|
}
|
|
125
140
|
catch (err) {
|
|
@@ -130,6 +145,10 @@ class AgentSubtaskAdapter {
|
|
|
130
145
|
command_run: commandRun,
|
|
131
146
|
summary: `Native ephemeral agent subtask dispatch failed: ${msg}`,
|
|
132
147
|
stderr: msg,
|
|
148
|
+
failure_origin: "worker-execution",
|
|
149
|
+
failure_category: "worker-failure",
|
|
150
|
+
fallback_eligible: false,
|
|
151
|
+
router_evidence: routerEvidence,
|
|
133
152
|
};
|
|
134
153
|
}
|
|
135
154
|
}
|
|
@@ -65,7 +65,6 @@ function writeSealedResultFromSummary(packet, parsedSummary) {
|
|
|
65
65
|
const validation = parsedSummary["validation"] ?? parsedSummary["validation_summary"];
|
|
66
66
|
const sealedResult = {
|
|
67
67
|
run_id: packet.run_id,
|
|
68
|
-
cluster_id: packet.cluster_id,
|
|
69
68
|
child_id: packet.active_child,
|
|
70
69
|
status: normalizeSealedStatus(parsedSummary["status"]),
|
|
71
70
|
};
|
|
@@ -46,6 +46,71 @@ function resolveCommand(cmd) {
|
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
function isQuotaExhaustedSignal(...parts) {
|
|
50
|
+
const text = parts.filter((value) => typeof value === "string" && value.length > 0).join("\n").toLowerCase();
|
|
51
|
+
if (!text)
|
|
52
|
+
return false;
|
|
53
|
+
return (text.includes("quota") ||
|
|
54
|
+
text.includes("rate limit") ||
|
|
55
|
+
text.includes("429") ||
|
|
56
|
+
text.includes("resource exhausted") ||
|
|
57
|
+
text.includes("insufficient_quota"));
|
|
58
|
+
}
|
|
59
|
+
function hasWorkerExecutionEvidence(packet, resultFilePath) {
|
|
60
|
+
if (resultFilePath && node_fs_1.default.existsSync(resultFilePath))
|
|
61
|
+
return true;
|
|
62
|
+
if (!packet.telemetry_file || !packet.active_child || !node_fs_1.default.existsSync(packet.telemetry_file))
|
|
63
|
+
return false;
|
|
64
|
+
try {
|
|
65
|
+
const telemetry = node_fs_1.default.readFileSync(packet.telemetry_file, "utf-8").trim();
|
|
66
|
+
if (!telemetry)
|
|
67
|
+
return false;
|
|
68
|
+
const lines = telemetry.split("\n");
|
|
69
|
+
const packetDispatchId = packet.dispatch_id;
|
|
70
|
+
// Scan backwards: when both the packet and the parsed event carry a dispatch_id,
|
|
71
|
+
// use that as the primary scope gate so stale events from a previous attempt are
|
|
72
|
+
// never attributed to the current dispatch. Fall back to the child-dispatched
|
|
73
|
+
// boundary for older telemetry that lacks dispatch_id on individual events.
|
|
74
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
75
|
+
const line = lines[i]?.trim();
|
|
76
|
+
if (!line)
|
|
77
|
+
continue;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(line);
|
|
80
|
+
if (parsed.child_id !== packet.active_child)
|
|
81
|
+
continue;
|
|
82
|
+
if (parsed.event === "worker-acknowledged" ||
|
|
83
|
+
parsed.event === "worker-heartbeat" ||
|
|
84
|
+
parsed.event === "worker-result") {
|
|
85
|
+
const eventDispatchId = parsed.dispatch_id;
|
|
86
|
+
if (packetDispatchId && eventDispatchId) {
|
|
87
|
+
// Primary gate: dispatch_id available on both sides — match determines
|
|
88
|
+
// whether this event belongs to the current dispatch.
|
|
89
|
+
if (eventDispatchId === packetDispatchId)
|
|
90
|
+
return true;
|
|
91
|
+
// Mismatch — event belongs to a different dispatch; keep scanning.
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Fallback: dispatch_id not available on one or both sides — accept the
|
|
95
|
+
// event as evidence for the current dispatch (scoped by the
|
|
96
|
+
// child-dispatched boundary below).
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
// Boundary fallback: stop at the child-dispatched event so events from a
|
|
100
|
+
// prior attempt are not counted when dispatch_id matching is unavailable.
|
|
101
|
+
if (parsed.event === "child-dispatched")
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
49
114
|
class TerminalCliAdapter {
|
|
50
115
|
config;
|
|
51
116
|
name = 'terminal-cli';
|
|
@@ -102,6 +167,8 @@ class TerminalCliAdapter {
|
|
|
102
167
|
}
|
|
103
168
|
async dispatch(packet, options) {
|
|
104
169
|
const primaryProvider = options.provider || "terminal-cli";
|
|
170
|
+
const routerEvidence = options.routerDecision;
|
|
171
|
+
const providerAttempts = [];
|
|
105
172
|
if ((0, worker_packet_js_1.isWorkerPacket)(packet) && packet.worker_role === "impl") {
|
|
106
173
|
const allowed = Array.isArray(packet.instructions?.allowed_scope) ? packet.instructions.allowed_scope : [];
|
|
107
174
|
if (allowed.length === 0) {
|
|
@@ -119,6 +186,20 @@ class TerminalCliAdapter {
|
|
|
119
186
|
}),
|
|
120
187
|
stderr: blockedMsg,
|
|
121
188
|
pre_dispatch_failure: true,
|
|
189
|
+
failure_origin: "provider-launch",
|
|
190
|
+
failure_category: "launch-error",
|
|
191
|
+
fallback_eligible: false,
|
|
192
|
+
router_evidence: routerEvidence,
|
|
193
|
+
provider_attempts: [
|
|
194
|
+
{
|
|
195
|
+
provider: primaryProvider,
|
|
196
|
+
failure_origin: "provider-launch",
|
|
197
|
+
failure_category: "launch-error",
|
|
198
|
+
pre_dispatch_failure: true,
|
|
199
|
+
fallback_eligible: false,
|
|
200
|
+
message: blockedMsg,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
122
203
|
};
|
|
123
204
|
}
|
|
124
205
|
}
|
|
@@ -132,11 +213,15 @@ class TerminalCliAdapter {
|
|
|
132
213
|
const policyProviders = (canFallback && Array.isArray(workerPolicy?.providers))
|
|
133
214
|
? workerPolicy.providers
|
|
134
215
|
: [];
|
|
216
|
+
const routerProviders = canFallback && Array.isArray(routerEvidence?.providersTried)
|
|
217
|
+
? routerEvidence.providersTried
|
|
218
|
+
: [];
|
|
219
|
+
const fallbackOrder = routerProviders.length > 0 ? routerProviders : policyProviders;
|
|
135
220
|
const providersToTry = [
|
|
136
221
|
{ name: primaryProvider, cfg: primaryCfg, isPrimary: true },
|
|
137
222
|
];
|
|
138
223
|
if (canFallback) {
|
|
139
|
-
for (const p of
|
|
224
|
+
for (const p of fallbackOrder) {
|
|
140
225
|
if (p !== primaryProvider && p in (this.config.providers ?? {})) {
|
|
141
226
|
try {
|
|
142
227
|
providersToTry.push({ name: p, cfg: this.getProvider(p), isPrimary: false });
|
|
@@ -167,12 +252,30 @@ class TerminalCliAdapter {
|
|
|
167
252
|
provider_used: provider,
|
|
168
253
|
command_run: provider,
|
|
169
254
|
stderr: err instanceof Error ? err.message : String(err),
|
|
255
|
+
pre_dispatch_failure: true,
|
|
256
|
+
failure_origin: "provider-launch",
|
|
257
|
+
failure_category: "launch-error",
|
|
258
|
+
fallback_eligible: true,
|
|
259
|
+
router_evidence: routerEvidence,
|
|
170
260
|
};
|
|
261
|
+
providerAttempts.push({
|
|
262
|
+
provider,
|
|
263
|
+
failure_origin: "provider-launch",
|
|
264
|
+
failure_category: "launch-error",
|
|
265
|
+
pre_dispatch_failure: true,
|
|
266
|
+
fallback_eligible: true,
|
|
267
|
+
message: lastResult.stderr,
|
|
268
|
+
});
|
|
171
269
|
continue;
|
|
172
270
|
}
|
|
173
271
|
const commandLine = formatCommandLine(command, args);
|
|
174
272
|
if (options.dryRun) {
|
|
175
|
-
|
|
273
|
+
const dryRun = this.dryRun(provider, command, args, commandLine, packet, packetFile, workerPrompt);
|
|
274
|
+
return {
|
|
275
|
+
...dryRun,
|
|
276
|
+
router_evidence: routerEvidence,
|
|
277
|
+
provider_attempts: providerAttempts,
|
|
278
|
+
};
|
|
176
279
|
}
|
|
177
280
|
if (!resolveCommand(command)) {
|
|
178
281
|
lastResult = {
|
|
@@ -181,28 +284,108 @@ class TerminalCliAdapter {
|
|
|
181
284
|
command_run: commandLine,
|
|
182
285
|
stderr: `Provider command "${command}" not found on PATH. ` +
|
|
183
286
|
`Install it or update the "command" field for provider "${provider}" in polaris.config.json.`,
|
|
287
|
+
pre_dispatch_failure: true,
|
|
288
|
+
failure_origin: "provider-launch",
|
|
289
|
+
failure_category: "provider-unavailable",
|
|
290
|
+
fallback_eligible: true,
|
|
291
|
+
router_evidence: routerEvidence,
|
|
292
|
+
};
|
|
293
|
+
providerAttempts.push({
|
|
294
|
+
provider,
|
|
295
|
+
failure_origin: "provider-launch",
|
|
296
|
+
failure_category: "provider-unavailable",
|
|
297
|
+
pre_dispatch_failure: true,
|
|
298
|
+
fallback_eligible: true,
|
|
299
|
+
message: lastResult.stderr,
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
let result;
|
|
304
|
+
try {
|
|
305
|
+
result = await this.runProcess(command, args, commandLine, packet, packetFile, provider, workerPrompt);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
309
|
+
lastResult = {
|
|
310
|
+
exit_code: 1,
|
|
311
|
+
provider_used: provider,
|
|
312
|
+
command_run: commandLine,
|
|
313
|
+
stderr: message,
|
|
314
|
+
pre_dispatch_failure: true,
|
|
315
|
+
failure_origin: "provider-launch",
|
|
316
|
+
failure_category: "provider-unavailable",
|
|
317
|
+
fallback_eligible: true,
|
|
318
|
+
router_evidence: routerEvidence,
|
|
184
319
|
};
|
|
320
|
+
providerAttempts.push({
|
|
321
|
+
provider,
|
|
322
|
+
failure_origin: "provider-launch",
|
|
323
|
+
failure_category: "provider-unavailable",
|
|
324
|
+
pre_dispatch_failure: true,
|
|
325
|
+
fallback_eligible: true,
|
|
326
|
+
message,
|
|
327
|
+
});
|
|
185
328
|
continue;
|
|
186
329
|
}
|
|
187
|
-
const result = await this.runProcess(command, args, commandLine, packet, packetFile, provider, workerPrompt);
|
|
188
330
|
// Worker succeeded — return immediately.
|
|
189
331
|
if (result.exit_code === 0) {
|
|
190
|
-
return
|
|
332
|
+
return {
|
|
333
|
+
...result,
|
|
334
|
+
router_evidence: routerEvidence,
|
|
335
|
+
provider_attempts: providerAttempts,
|
|
336
|
+
};
|
|
191
337
|
}
|
|
192
|
-
// If the worker wrote a result file it actually ran (and failed) — don't
|
|
193
|
-
// retry with another provider; that would double-dispatch the same child.
|
|
194
338
|
const resultFilePath = (0, worker_packet_js_1.isWorkerPacket)(packet) ? packet.result_file_contract?.result_file : undefined;
|
|
195
|
-
|
|
196
|
-
|
|
339
|
+
const workerStarted = hasWorkerExecutionEvidence(packet, resultFilePath);
|
|
340
|
+
if (workerStarted) {
|
|
341
|
+
return {
|
|
342
|
+
...result,
|
|
343
|
+
failure_origin: "worker-execution",
|
|
344
|
+
failure_category: "worker-failure",
|
|
345
|
+
fallback_eligible: false,
|
|
346
|
+
router_evidence: routerEvidence,
|
|
347
|
+
provider_attempts: providerAttempts,
|
|
348
|
+
};
|
|
197
349
|
}
|
|
198
|
-
|
|
199
|
-
|
|
350
|
+
const category = isQuotaExhaustedSignal(result.stderr, result.stdout, result.summary)
|
|
351
|
+
? "quota-exhausted"
|
|
352
|
+
: "provider-unavailable";
|
|
353
|
+
const classified = {
|
|
354
|
+
...result,
|
|
355
|
+
pre_dispatch_failure: true,
|
|
356
|
+
failure_origin: "provider-launch",
|
|
357
|
+
failure_category: category,
|
|
358
|
+
fallback_eligible: true,
|
|
359
|
+
router_evidence: routerEvidence,
|
|
360
|
+
};
|
|
361
|
+
providerAttempts.push({
|
|
362
|
+
provider,
|
|
363
|
+
failure_origin: "provider-launch",
|
|
364
|
+
failure_category: category,
|
|
365
|
+
pre_dispatch_failure: true,
|
|
366
|
+
fallback_eligible: true,
|
|
367
|
+
message: result.stderr ?? result.summary,
|
|
368
|
+
});
|
|
369
|
+
lastResult = classified;
|
|
200
370
|
}
|
|
201
|
-
|
|
371
|
+
const exhausted = lastResult ?? {
|
|
202
372
|
exit_code: 1,
|
|
203
373
|
provider_used: primaryProvider,
|
|
204
374
|
command_run: '',
|
|
205
375
|
stderr: 'All configured providers exhausted without starting a worker',
|
|
376
|
+
pre_dispatch_failure: true,
|
|
377
|
+
failure_origin: "provider-launch",
|
|
378
|
+
failure_category: "provider-unavailable",
|
|
379
|
+
fallback_eligible: false,
|
|
380
|
+
router_evidence: routerEvidence,
|
|
381
|
+
provider_attempts: providerAttempts,
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
...exhausted,
|
|
385
|
+
// All providers have been exhausted — no further fallback is possible.
|
|
386
|
+
fallback_eligible: false,
|
|
387
|
+
router_evidence: exhausted.router_evidence ?? routerEvidence,
|
|
388
|
+
provider_attempts: exhausted.provider_attempts ?? providerAttempts,
|
|
206
389
|
};
|
|
207
390
|
}
|
|
208
391
|
finally {
|
|
@@ -318,9 +501,7 @@ class TerminalCliAdapter {
|
|
|
318
501
|
// that return minimal status objects instead of proper CompactReturn structs.
|
|
319
502
|
const effectiveStatus = (exitCode === 0 && isValidCompactReturn) ? "success" : "failure";
|
|
320
503
|
const sealedResult = {
|
|
321
|
-
...normalized,
|
|
322
504
|
run_id: packet.run_id,
|
|
323
|
-
cluster_id: packet.cluster_id,
|
|
324
505
|
child_id: String(normalized["child_id"] ?? packet.active_child),
|
|
325
506
|
status: effectiveStatus,
|
|
326
507
|
commit: typeof normalized["commit"] === "string"
|
|
@@ -336,7 +517,6 @@ class TerminalCliAdapter {
|
|
|
336
517
|
? normalized["error_message"]
|
|
337
518
|
: stdout || summary)
|
|
338
519
|
: undefined,
|
|
339
|
-
...(compactReturnErrors.length > 0 ? { compact_return_errors: compactReturnErrors } : {}),
|
|
340
520
|
};
|
|
341
521
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(resultFile), { recursive: true });
|
|
342
522
|
node_fs_1.default.writeFileSync(resultFile, JSON.stringify(sealedResult, null, 2), "utf-8");
|