@lsctech/polaris 0.5.11 → 0.5.12
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/proposal.js +61 -0
- package/dist/autoresearch/score.js +124 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/qc.js +97 -0
- package/dist/config/validator.js +16 -6
- package/dist/finalize/artifact-policy.js +35 -3
- package/dist/finalize/index.js +23 -4
- package/dist/finalize/run-report.js +189 -19
- package/dist/finalize/steps/05-generate-report.js +5 -1
- package/dist/finalize/steps/06-commit.js +2 -1
- package/dist/finalize/steps/12-archive.js +6 -0
- package/dist/loop/adapters/terminal-cli.js +131 -25
- package/dist/loop/body-parser.js +69 -6
- package/dist/loop/continue.js +19 -1
- package/dist/loop/dispatch.js +102 -67
- package/dist/loop/parent.js +22 -3
- package/dist/loop/router/engine.js +1 -0
- package/dist/loop/run-preflight.js +4 -1
- package/dist/loop/worker-packet.js +81 -2
- package/dist/medic/routing-signals.js +60 -0
- package/dist/qc/policy.js +2 -0
- package/dist/qc/providers/coderabbit.js +138 -10
- package/dist/qc/repair-loop.js +147 -1
- package/dist/qc/repair-packets.js +16 -3
- package/dist/qc/routing.js +6 -0
- package/dist/qc/types.js +3 -0
- package/dist/tracker/local-graph.js +106 -3
- package/package.json +1 -1
|
@@ -1,12 +1,179 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateRunReport = generateRunReport;
|
|
4
|
+
const score_js_1 = require("../autoresearch/score.js");
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
function asString(value) {
|
|
11
|
+
return typeof value === "string" ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
function asStringArray(value) {
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return [];
|
|
16
|
+
return value.filter((v) => typeof v === "string");
|
|
17
|
+
}
|
|
18
|
+
function inferCompatibilityMode(selectionReason) {
|
|
19
|
+
if (!selectionReason)
|
|
20
|
+
return null;
|
|
21
|
+
if (selectionReason === "policy-router" || selectionReason.startsWith("router-")) {
|
|
22
|
+
return "router";
|
|
23
|
+
}
|
|
24
|
+
return "compatibility";
|
|
25
|
+
}
|
|
26
|
+
function buildRoutingCell(childId, state, telemetryEvents) {
|
|
27
|
+
const meta = asRecord(state.open_children_meta?.[childId]);
|
|
28
|
+
const dispatchRecord = asRecord(meta?.["dispatch_record"]);
|
|
29
|
+
const result = asRecord(state.completed_children_results?.[childId]);
|
|
30
|
+
const completed = state.completed_children.includes(childId);
|
|
31
|
+
const status = completed ? "Done" : "Open";
|
|
32
|
+
const findEvent = (eventName) => {
|
|
33
|
+
const found = telemetryEvents.find((e) => {
|
|
34
|
+
const rec = asRecord(e);
|
|
35
|
+
return rec?.["event"] === eventName && rec?.["child_id"] === childId;
|
|
36
|
+
});
|
|
37
|
+
return asRecord(found);
|
|
38
|
+
};
|
|
39
|
+
const completeEvent = findEvent("child-complete");
|
|
40
|
+
const dispatchedEvent = findEvent("child-dispatched");
|
|
41
|
+
const selectedEvent = findEvent("provider-selected");
|
|
42
|
+
const selectedSlotClaim = asRecord(dispatchedEvent?.["selected_slot_claim"]);
|
|
43
|
+
const routingSummary = asRecord(completeEvent?.["routing_summary"] ??
|
|
44
|
+
selectedEvent?.["routing_summary"] ??
|
|
45
|
+
dispatchedEvent?.["routing_summary"] ??
|
|
46
|
+
dispatchRecord?.["routing_summary"]);
|
|
47
|
+
const provider = asString(completeEvent?.["provider"] ??
|
|
48
|
+
selectedEvent?.["selected_provider"] ??
|
|
49
|
+
dispatchedEvent?.["provider"] ??
|
|
50
|
+
dispatchRecord?.["provider"] ??
|
|
51
|
+
result?.["provider"]);
|
|
52
|
+
const selectionReason = asString(completeEvent?.["router_selection_reason"] ??
|
|
53
|
+
selectedEvent?.["selection_reason"] ??
|
|
54
|
+
routingSummary?.["selection_reason"] ??
|
|
55
|
+
dispatchRecord?.["provider_selection_reason"] ??
|
|
56
|
+
selectedSlotClaim?.["selection_reason"]) ?? "—";
|
|
57
|
+
const compatibilityMode = typeof routingSummary?.["compatibility_mode"] === "boolean"
|
|
58
|
+
? routingSummary["compatibility_mode"]
|
|
59
|
+
: typeof selectedEvent?.["router_compatibility_mode"] === "boolean"
|
|
60
|
+
? selectedEvent["router_compatibility_mode"]
|
|
61
|
+
: null;
|
|
62
|
+
const mode = compatibilityMode === true
|
|
63
|
+
? "compatibility"
|
|
64
|
+
: compatibilityMode === false
|
|
65
|
+
? "router"
|
|
66
|
+
: inferCompatibilityMode(selectionReason) ?? "—";
|
|
67
|
+
const policyOrder = asStringArray(routingSummary?.["effective_policy_order"] ??
|
|
68
|
+
selectedEvent?.["providers_tried"] ??
|
|
69
|
+
completeEvent?.["providers_tried"] ??
|
|
70
|
+
dispatchedEvent?.["providers_tried"] ??
|
|
71
|
+
dispatchRecord?.["providers_tried"]);
|
|
72
|
+
const candidatesRaw = selectedEvent?.["router_candidates"];
|
|
73
|
+
const candidateCount = Array.isArray(candidatesRaw) ? candidatesRaw.length : null;
|
|
74
|
+
const fallbackRaw = selectedEvent?.["fallback_attempts"];
|
|
75
|
+
const fallbackAttempts = Array.isArray(fallbackRaw) ? fallbackRaw : [];
|
|
76
|
+
const fallbackCount = fallbackAttempts.filter((a) => asRecord(a)?.["outcome"] === "rejected").length;
|
|
77
|
+
const title = asString(meta?.["title"]) ?? "—";
|
|
78
|
+
return {
|
|
79
|
+
childId,
|
|
80
|
+
title,
|
|
81
|
+
provider: provider ?? "delegated",
|
|
82
|
+
selectionReason,
|
|
83
|
+
mode,
|
|
84
|
+
policyOrder,
|
|
85
|
+
candidateCount,
|
|
86
|
+
fallbackCount,
|
|
87
|
+
status,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function renderRoutingSection(state, telemetryEvents) {
|
|
91
|
+
const allChildren = [...state.completed_children, ...state.open_children];
|
|
92
|
+
const cells = allChildren.map((id) => buildRoutingCell(id, state, telemetryEvents));
|
|
93
|
+
const rows = cells
|
|
94
|
+
.map((c) => `| ${c.childId} | ${c.title} | ${c.provider} | ${c.selectionReason} | ${c.mode} | ${c.policyOrder.join(", ") || "—"} | ${c.candidateCount === null ? "—" : c.candidateCount} | ${c.fallbackCount} | ${c.status} |`)
|
|
95
|
+
.join("\n");
|
|
96
|
+
const providerCounts = new Map();
|
|
97
|
+
for (const cell of cells) {
|
|
98
|
+
if (cell.provider === "delegated")
|
|
99
|
+
continue;
|
|
100
|
+
providerCounts.set(cell.provider, (providerCounts.get(cell.provider) ?? 0) + 1);
|
|
101
|
+
}
|
|
102
|
+
const distribution = Array.from(providerCounts.entries())
|
|
103
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
104
|
+
.map(([provider, count]) => `${provider}: ${count}`)
|
|
105
|
+
.join(", ") || "—";
|
|
106
|
+
const artifacts = {
|
|
107
|
+
runId: state.run_id,
|
|
108
|
+
runDir: null,
|
|
109
|
+
clusterDir: null,
|
|
110
|
+
currentState: state,
|
|
111
|
+
ledgerEvents: [],
|
|
112
|
+
resultPackets: [],
|
|
113
|
+
workerResultContracts: [],
|
|
114
|
+
telemetryEvents,
|
|
115
|
+
qcResults: [],
|
|
116
|
+
clusterState: null,
|
|
117
|
+
};
|
|
118
|
+
const outcomes = (0, score_js_1.summarizeRouterOutcomes)(artifacts);
|
|
119
|
+
const anomalyLines = [];
|
|
120
|
+
anomalyLines.push(`- Total routing decisions: ${outcomes.total_decisions}`);
|
|
121
|
+
anomalyLines.push(`- Exhausted decisions: ${outcomes.exhausted_decisions}`);
|
|
122
|
+
anomalyLines.push(`- Fallback attempts: ${outcomes.fallback_attempts} (${outcomes.successful_fallbacks} successful)`);
|
|
123
|
+
const hasAnomaly = outcomes.exhausted_decisions > 0 ||
|
|
124
|
+
outcomes.fallback_attempts > 0 ||
|
|
125
|
+
outcomes.recurring_failures.length > 0 ||
|
|
126
|
+
outcomes.provider_monopoly_signals.length > 0 ||
|
|
127
|
+
outcomes.evidence_gap_signals.length > 0 ||
|
|
128
|
+
outcomes.state_repair_signals.length > 0;
|
|
129
|
+
if (outcomes.recurring_failures.length > 0) {
|
|
130
|
+
anomalyLines.push(`- Recurring router failures:`);
|
|
131
|
+
for (const failure of outcomes.recurring_failures) {
|
|
132
|
+
anomalyLines.push(` - ${failure.reason}: ${failure.occurrences} occurrence(s) — children: ${failure.child_ids.join(", ") || "none"}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (outcomes.provider_monopoly_signals.length > 0) {
|
|
136
|
+
anomalyLines.push(`- Provider monopoly (repeated same-provider selection):`);
|
|
137
|
+
for (const signal of outcomes.provider_monopoly_signals) {
|
|
138
|
+
anomalyLines.push(` - ${signal.reason}: ${signal.occurrences} occurrence(s) — children: ${signal.child_ids.join(", ") || "none"}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (outcomes.evidence_gap_signals.length > 0) {
|
|
142
|
+
anomalyLines.push(`- Evidence gaps:`);
|
|
143
|
+
for (const signal of outcomes.evidence_gap_signals) {
|
|
144
|
+
anomalyLines.push(` - ${signal.reason}: ${signal.occurrences} occurrence(s) — children: ${signal.child_ids.join(", ") || "none"}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (outcomes.state_repair_signals.length > 0) {
|
|
148
|
+
anomalyLines.push(`- State repair / runtime review signals:`);
|
|
149
|
+
for (const signal of outcomes.state_repair_signals) {
|
|
150
|
+
anomalyLines.push(` - ${signal.signal} (${signal.reason}): ${signal.occurrences} occurrence(s) — children: ${signal.child_ids.join(", ") || "none"}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const anomaliesText = hasAnomaly
|
|
154
|
+
? anomalyLines.join("\n")
|
|
155
|
+
: "No routing anomalies or evidence gaps detected.";
|
|
156
|
+
return `### Provider distribution
|
|
157
|
+
|
|
158
|
+
| Child | Provider | Selection reason | Mode | Policy order | Candidates | Fallbacks | Status |
|
|
159
|
+
|---|---|---|---|---|---|---|---|
|
|
160
|
+
${rows || "| _No children recorded_ | — | — | — | — | — | — | — |"}
|
|
161
|
+
|
|
162
|
+
**Provider summary:** ${distribution}
|
|
163
|
+
|
|
164
|
+
### Routing review
|
|
165
|
+
|
|
166
|
+
${anomaliesText}`;
|
|
167
|
+
}
|
|
4
168
|
function renderQcSection(qcSummary) {
|
|
5
169
|
if (!qcSummary)
|
|
6
170
|
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 +
|
|
9
|
-
open_by_severity.
|
|
171
|
+
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;
|
|
172
|
+
const openTotal = open_by_severity.critical +
|
|
173
|
+
open_by_severity.high +
|
|
174
|
+
open_by_severity.medium +
|
|
175
|
+
open_by_severity.low +
|
|
176
|
+
open_by_severity.info;
|
|
10
177
|
const deliveryStatus = blocks_delivery
|
|
11
178
|
? "**BLOCKED** — unresolved critical/high findings must be addressed before delivery"
|
|
12
179
|
: "Not blocking delivery";
|
|
@@ -51,11 +218,10 @@ ${providers || "| _none_ | — | — | — |"}
|
|
|
51
218
|
|
|
52
219
|
| Decision | Count |
|
|
53
220
|
|---|---|
|
|
54
|
-
${routing}
|
|
55
|
-
`;
|
|
221
|
+
${routing}`;
|
|
56
222
|
}
|
|
57
223
|
function generateRunReport(data) {
|
|
58
|
-
const { state, branch, validationPassed, prUrl, artifacts, notes, qcSummary } = data;
|
|
224
|
+
const { state, branch, validationPassed, prUrl, artifacts, notes, qcSummary, telemetryEvents } = data;
|
|
59
225
|
const total = state.completed_children.length + state.open_children.length;
|
|
60
226
|
const completedCount = state.completed_children.length;
|
|
61
227
|
const childRows = state.completed_children
|
|
@@ -72,21 +238,25 @@ function generateRunReport(data) {
|
|
|
72
238
|
? `\n**Blocker:** ${state.blocker.reason} (child: ${state.blocker.child_id})\n`
|
|
73
239
|
: "";
|
|
74
240
|
const qcSection = renderQcSection(qcSummary);
|
|
241
|
+
const routingSection = renderRoutingSection(state, telemetryEvents ?? []);
|
|
75
242
|
return `# Run Report: ${state.run_id}
|
|
76
243
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
244
|
+
| Field | Value |
|
|
245
|
+
|---|---|
|
|
246
|
+
| **Status** | ${state.status} |
|
|
247
|
+
| **Branch** | ${branch} |
|
|
248
|
+
| **PR** | ${prUrl ?? "TBD — set at delivery step 9"} |
|
|
249
|
+
| **Children completed** | ${completedCount} of ${total} |
|
|
250
|
+
| **Validation** | ${validationPassed ? "passed" : "failed"} |
|
|
251
|
+
${blockerNote}## Children
|
|
252
|
+
|
|
253
|
+
| ID | Title | Commit | Status |
|
|
254
|
+
|---|---|---|---|
|
|
255
|
+
${childRows || "_No children recorded_"}
|
|
256
|
+
|
|
257
|
+
## Provider routing
|
|
258
|
+
|
|
259
|
+
${routingSection}
|
|
90
260
|
|
|
91
261
|
## Artifacts produced
|
|
92
262
|
|
|
@@ -3,11 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.stepGenerateReport = stepGenerateReport;
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
|
+
const continue_js_1 = require("../../loop/continue.js");
|
|
7
|
+
const gates_js_1 = require("../../autoresearch/gates.js");
|
|
6
8
|
const run_report_js_1 = require("../run-report.js");
|
|
7
9
|
function stepGenerateReport(repoRoot, state, branch, validationPassed) {
|
|
8
10
|
const reportPath = (0, node_path_1.resolve)(repoRoot, ".polaris", "runs", "run-report.md");
|
|
9
11
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(reportPath), { recursive: true });
|
|
10
|
-
const
|
|
12
|
+
const telemetryFile = (0, continue_js_1.resolveTelemetryFilePath)(state, repoRoot);
|
|
13
|
+
const telemetryEvents = (0, gates_js_1.readJsonLines)(telemetryFile);
|
|
14
|
+
const content = (0, run_report_js_1.generateRunReport)({ state, branch, validationPassed, telemetryEvents });
|
|
11
15
|
(0, node_fs_1.writeFileSync)(reportPath, content, "utf-8");
|
|
12
16
|
console.log(`Run report written: ${reportPath}`);
|
|
13
17
|
return reportPath;
|
|
@@ -7,7 +7,8 @@ const node_path_1 = require("node:path");
|
|
|
7
7
|
const artifact_policy_js_1 = require("../artifact-policy.js");
|
|
8
8
|
function stepCommit(repoRoot, state, _stateFile, _reportPath) {
|
|
9
9
|
const msg = `polaris finalize: ${state.run_id}\n\nChildren: ${state.completed_children.length} completed\nBranch: ${getBranch(repoRoot)}`;
|
|
10
|
-
const
|
|
10
|
+
const runArchiveTarget = `.polaris/runs/${state.run_id}`;
|
|
11
|
+
const promotedTargets = [...(0, artifact_policy_js_1.getArtifactPromotionStageTargets)(state.cluster_id), runArchiveTarget]
|
|
11
12
|
.filter((target) => (0, node_fs_1.existsSync)((0, node_path_1.join)(repoRoot, target)));
|
|
12
13
|
if (promotedTargets.length > 0) {
|
|
13
14
|
(0, node_child_process_1.execFileSync)("git", ["add", "--", ...promotedTargets], { cwd: repoRoot, stdio: "inherit" });
|
|
@@ -17,5 +17,11 @@ function stepArchive(repoRoot, state, stateFile, reportPath) {
|
|
|
17
17
|
(0, node_fs_1.copyFileSync)(src, (0, node_path_1.join)(archiveDir, file));
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
const artifactDir = state.artifact_dir
|
|
21
|
+
?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
22
|
+
const telemetryFile = (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
|
|
23
|
+
if ((0, node_fs_1.existsSync)(telemetryFile)) {
|
|
24
|
+
(0, node_fs_1.copyFileSync)(telemetryFile, (0, node_path_1.join)(archiveDir, "telemetry.jsonl"));
|
|
25
|
+
}
|
|
20
26
|
console.log(`Run archived to .polaris/runs/${state.run_id}/`);
|
|
21
27
|
}
|
|
@@ -494,31 +494,53 @@ class TerminalCliAdapter {
|
|
|
494
494
|
// Normalize legacy CompactReturn shapes before validation so that
|
|
495
495
|
// workers using pre-spec formats (status:"success", validation:{passed:[...]})
|
|
496
496
|
// are accepted rather than silently marked as failures.
|
|
497
|
-
const normalized = normalizeLegacyCompactReturn(parsed);
|
|
498
|
-
const
|
|
497
|
+
const normalized = normalizeLegacyCompactReturn(parsed, packet.active_child);
|
|
498
|
+
const compact = buildCompactReturnForSealedResult(normalized, exitCode);
|
|
499
|
+
const compactReturnErrors = (0, compact_return_js_1.validateCompactReturn)(compact);
|
|
499
500
|
const isValidCompactReturn = compactReturnErrors.length === 0;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
501
|
+
const finalStatus = isValidCompactReturn
|
|
502
|
+
? compact["status"]
|
|
503
|
+
: "failed";
|
|
504
|
+
const finalValidation = isValidCompactReturn
|
|
505
|
+
? compact["validation"]
|
|
506
|
+
: "failed";
|
|
507
|
+
const finalNext = isValidCompactReturn
|
|
508
|
+
? compact["next_recommended_action"]
|
|
509
|
+
: "stop";
|
|
510
|
+
const errorMessage = finalStatus === "failed" || finalStatus === "blocked"
|
|
511
|
+
? isValidCompactReturn
|
|
512
|
+
? typeof compact["error_message"] === "string"
|
|
513
|
+
? compact["error_message"]
|
|
514
|
+
: typeof compact["blocker"] === "string"
|
|
515
|
+
? compact["blocker"]
|
|
516
|
+
: stdout || summary
|
|
517
|
+
: `CompactReturn validation failed: ${compactReturnErrors.join("; ")}`
|
|
518
|
+
: undefined;
|
|
504
519
|
const sealedResult = {
|
|
505
520
|
run_id: packet.run_id,
|
|
506
|
-
child_id:
|
|
507
|
-
status:
|
|
508
|
-
commit:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
? normalized["commit_hash"]
|
|
512
|
-
: undefined,
|
|
513
|
-
validation: normalized["validation"] ?? normalized["validation_summary"],
|
|
514
|
-
error_message: effectiveStatus === "failure"
|
|
515
|
-
? (!isValidCompactReturn
|
|
516
|
-
? `CompactReturn validation failed: ${compactReturnErrors.join("; ")}`
|
|
517
|
-
: typeof normalized["error_message"] === "string"
|
|
518
|
-
? normalized["error_message"]
|
|
519
|
-
: stdout || summary)
|
|
520
|
-
: undefined,
|
|
521
|
+
child_id: compact["child_id"],
|
|
522
|
+
status: finalStatus,
|
|
523
|
+
commit: compact["commit"],
|
|
524
|
+
validation: finalValidation,
|
|
525
|
+
next_recommended_action: finalNext,
|
|
521
526
|
};
|
|
527
|
+
if (isValidCompactReturn) {
|
|
528
|
+
if (compact["result_data"] !== undefined) {
|
|
529
|
+
sealedResult["result_data"] = compact["result_data"];
|
|
530
|
+
}
|
|
531
|
+
if (compact["work_note_paths"] !== undefined) {
|
|
532
|
+
sealedResult["work_note_paths"] = compact["work_note_paths"];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (Array.isArray(compact["run_health_symptoms"])) {
|
|
536
|
+
sealedResult["run_health_symptoms"] = compact["run_health_symptoms"];
|
|
537
|
+
}
|
|
538
|
+
if (finalStatus === "blocked" && typeof compact["blocker"] === "string") {
|
|
539
|
+
sealedResult["blocker"] = compact["blocker"];
|
|
540
|
+
}
|
|
541
|
+
if (errorMessage !== undefined) {
|
|
542
|
+
sealedResult["error_message"] = errorMessage;
|
|
543
|
+
}
|
|
522
544
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(resultFile), { recursive: true });
|
|
523
545
|
node_fs_1.default.writeFileSync(resultFile, JSON.stringify(sealedResult, null, 2), "utf-8");
|
|
524
546
|
}
|
|
@@ -533,14 +555,26 @@ exports.TerminalCliAdapter = TerminalCliAdapter;
|
|
|
533
555
|
* Normalize a legacy CompactReturn shape to the current spec before validation.
|
|
534
556
|
* Handles pre-spec formats produced by older workers:
|
|
535
557
|
* - status:"success"|"completed" → status:"done"
|
|
558
|
+
* - status:"failure"|"error"|"in-progress" → status:"failed"
|
|
536
559
|
* - validation:{passed:[...],failed:[...]} → validation:"passed"|"failed"|"skipped"
|
|
560
|
+
* - next_action:"continue"|"stop"|"escalate" → next_recommended_action
|
|
537
561
|
* - missing boolean flags → false
|
|
562
|
+
* - missing or empty child_id → active_child from the bootstrap packet
|
|
538
563
|
*/
|
|
539
|
-
function normalizeLegacyCompactReturn(raw) {
|
|
564
|
+
function normalizeLegacyCompactReturn(raw, activeChild) {
|
|
540
565
|
const result = { ...raw };
|
|
566
|
+
if (typeof result['child_id'] !== 'string' || !result['child_id']) {
|
|
567
|
+
result['child_id'] = activeChild;
|
|
568
|
+
}
|
|
541
569
|
if (result['status'] === 'success' || result['status'] === 'completed') {
|
|
542
570
|
result['status'] = 'done';
|
|
543
571
|
}
|
|
572
|
+
else if (result['status'] === 'failure' || result['status'] === 'error' || result['status'] === 'in-progress') {
|
|
573
|
+
result['status'] = 'failed';
|
|
574
|
+
}
|
|
575
|
+
else if (result['status'] !== 'done' && result['status'] !== 'failed' && result['status'] !== 'blocked') {
|
|
576
|
+
result['status'] = 'failed';
|
|
577
|
+
}
|
|
544
578
|
if (typeof result['validation'] === 'object' && result['validation'] !== null) {
|
|
545
579
|
const v = result['validation'];
|
|
546
580
|
if (Array.isArray(v['failed']) && v['failed'].length > 0) {
|
|
@@ -553,6 +587,19 @@ function normalizeLegacyCompactReturn(raw) {
|
|
|
553
587
|
result['validation'] = 'skipped';
|
|
554
588
|
}
|
|
555
589
|
}
|
|
590
|
+
if (typeof result['next_action'] === 'string' && !result['next_recommended_action']) {
|
|
591
|
+
const action = result['next_action'];
|
|
592
|
+
if (action === 'continue') {
|
|
593
|
+
result['next_recommended_action'] = 'continue';
|
|
594
|
+
}
|
|
595
|
+
else if (action === 'stop') {
|
|
596
|
+
result['next_recommended_action'] = 'stop';
|
|
597
|
+
}
|
|
598
|
+
else if (action === 'escalate') {
|
|
599
|
+
result['next_recommended_action'] = 'investigate';
|
|
600
|
+
}
|
|
601
|
+
delete result['next_action'];
|
|
602
|
+
}
|
|
556
603
|
for (const flag of ['tracker_updated', 'state_updated', 'telemetry_updated']) {
|
|
557
604
|
if (typeof result[flag] !== 'boolean') {
|
|
558
605
|
result[flag] = false;
|
|
@@ -560,9 +607,63 @@ function normalizeLegacyCompactReturn(raw) {
|
|
|
560
607
|
}
|
|
561
608
|
return result;
|
|
562
609
|
}
|
|
610
|
+
function compactReturnDefaults(status) {
|
|
611
|
+
return {
|
|
612
|
+
validation: status === 'done' ? 'passed' : status === 'failed' ? 'failed' : 'skipped',
|
|
613
|
+
next_recommended_action: status === 'done' ? 'continue' : 'stop',
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function resolveCommit(raw) {
|
|
617
|
+
if (typeof raw['commit'] === 'string') {
|
|
618
|
+
return raw['commit'];
|
|
619
|
+
}
|
|
620
|
+
if (typeof raw['commit_hash'] === 'string') {
|
|
621
|
+
return raw['commit_hash'];
|
|
622
|
+
}
|
|
623
|
+
if (typeof raw['commit_sha'] === 'string') {
|
|
624
|
+
return raw['commit_sha'];
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
function isValidValidation(value) {
|
|
629
|
+
return value === 'passed' || value === 'failed' || value === 'skipped';
|
|
630
|
+
}
|
|
631
|
+
function isValidNextAction(value) {
|
|
632
|
+
return value === 'continue' || value === 'stop' || value === 'investigate';
|
|
633
|
+
}
|
|
634
|
+
function buildCompactReturnForSealedResult(normalized, exitCode) {
|
|
635
|
+
const rawStatus = normalized['status'];
|
|
636
|
+
let baseStatus;
|
|
637
|
+
if (rawStatus === 'done' || rawStatus === 'failed' || rawStatus === 'blocked') {
|
|
638
|
+
baseStatus = rawStatus;
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
baseStatus = 'failed';
|
|
642
|
+
}
|
|
643
|
+
const effectiveStatus = exitCode === 0
|
|
644
|
+
? baseStatus
|
|
645
|
+
: baseStatus === 'failed' || baseStatus === 'blocked'
|
|
646
|
+
? baseStatus
|
|
647
|
+
: 'failed';
|
|
648
|
+
const defaults = compactReturnDefaults(effectiveStatus);
|
|
649
|
+
const compact = {
|
|
650
|
+
...normalized,
|
|
651
|
+
child_id: String(normalized['child_id'] || ''),
|
|
652
|
+
status: effectiveStatus,
|
|
653
|
+
commit: resolveCommit(normalized),
|
|
654
|
+
validation: isValidValidation(normalized['validation']) ? normalized['validation'] : defaults.validation,
|
|
655
|
+
next_recommended_action: isValidNextAction(normalized['next_recommended_action'])
|
|
656
|
+
? normalized['next_recommended_action']
|
|
657
|
+
: defaults.next_recommended_action,
|
|
658
|
+
tracker_updated: typeof normalized['tracker_updated'] === 'boolean' ? normalized['tracker_updated'] : false,
|
|
659
|
+
state_updated: typeof normalized['state_updated'] === 'boolean' ? normalized['state_updated'] : false,
|
|
660
|
+
telemetry_updated: typeof normalized['telemetry_updated'] === 'boolean' ? normalized['telemetry_updated'] : false,
|
|
661
|
+
};
|
|
662
|
+
return compact;
|
|
663
|
+
}
|
|
563
664
|
/**
|
|
564
665
|
* Extract a worker summary from stdout.
|
|
565
|
-
* Looks for the last line that is valid JSON
|
|
666
|
+
* Looks for the last line that is a valid JSON *object*; falls back to the last 500 chars.
|
|
566
667
|
*/
|
|
567
668
|
function extractSummary(stdout) {
|
|
568
669
|
if (!stdout)
|
|
@@ -573,8 +674,13 @@ function extractSummary(stdout) {
|
|
|
573
674
|
if (!trimmed)
|
|
574
675
|
continue;
|
|
575
676
|
try {
|
|
576
|
-
JSON.parse(trimmed);
|
|
577
|
-
|
|
677
|
+
const parsed = JSON.parse(trimmed);
|
|
678
|
+
if (typeof parsed === 'object' &&
|
|
679
|
+
parsed !== null &&
|
|
680
|
+
!Array.isArray(parsed)) {
|
|
681
|
+
return trimmed;
|
|
682
|
+
}
|
|
683
|
+
// not an object-shaped JSON value, keep looking
|
|
578
684
|
}
|
|
579
685
|
catch {
|
|
580
686
|
// not JSON, keep looking
|
package/dist/loop/body-parser.js
CHANGED
|
@@ -150,6 +150,67 @@ function parseListItems(text) {
|
|
|
150
150
|
function stripTrailingParenthetical(s) {
|
|
151
151
|
return s.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
|
152
152
|
}
|
|
153
|
+
/** Returns true for tokens that look like a file path or glob pattern. */
|
|
154
|
+
function isPathLikeToken(token) {
|
|
155
|
+
return (token.includes('/') ||
|
|
156
|
+
token.includes('*') ||
|
|
157
|
+
token.includes('.') ||
|
|
158
|
+
token.includes('?'));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Normalizes a bare directory path into a glob pattern so that
|
|
162
|
+
* matchesAllowedScope() treats it as the directory and its contents.
|
|
163
|
+
*/
|
|
164
|
+
function normalizeDirectoryPattern(pattern) {
|
|
165
|
+
if (pattern.endsWith('/') && !pattern.endsWith('/**')) {
|
|
166
|
+
return pattern + '**';
|
|
167
|
+
}
|
|
168
|
+
// Extract the final path segment to check for extensions
|
|
169
|
+
const lastSlashIndex = pattern.lastIndexOf('/');
|
|
170
|
+
const finalSegment = lastSlashIndex >= 0 ? pattern.slice(lastSlashIndex + 1) : pattern;
|
|
171
|
+
// Check if final segment has a file extension (not counting leading dot in dot-directories)
|
|
172
|
+
const hasExtension = finalSegment.startsWith('.')
|
|
173
|
+
? finalSegment.slice(1).includes('.') // For .github, .vscode: no extension. For .env.local: has extension
|
|
174
|
+
: finalSegment.includes('.');
|
|
175
|
+
if (!hasExtension &&
|
|
176
|
+
!pattern.endsWith('*') &&
|
|
177
|
+
!pattern.endsWith('?')) {
|
|
178
|
+
return pattern + '/**';
|
|
179
|
+
}
|
|
180
|
+
return pattern;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Extracts bare file/glob patterns from a raw scope bullet line.
|
|
184
|
+
*
|
|
185
|
+
* Strips backticks and trailing prose (e.g. `src/foo.ts some comment`), splits
|
|
186
|
+
* comma/semicolon separated entries, and returns only path-like leading tokens.
|
|
187
|
+
*/
|
|
188
|
+
function parseScopeItem(raw) {
|
|
189
|
+
const cleaned = raw.replace(/`/g, '').trim();
|
|
190
|
+
if (cleaned.length === 0)
|
|
191
|
+
return [];
|
|
192
|
+
const parts = cleaned
|
|
193
|
+
.split(/[,;]+/)
|
|
194
|
+
.map((s) => stripTrailingParenthetical(s.trim()))
|
|
195
|
+
.filter((s) => s.length > 0);
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const part of parts) {
|
|
198
|
+
const tokens = part.split(/\s+/);
|
|
199
|
+
const pathTokens = [];
|
|
200
|
+
for (const token of tokens) {
|
|
201
|
+
if (isPathLikeToken(token)) {
|
|
202
|
+
pathTokens.push(token);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const pathToken of pathTokens) {
|
|
209
|
+
results.push(normalizeDirectoryPattern(pathToken));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return results;
|
|
213
|
+
}
|
|
153
214
|
/**
|
|
154
215
|
* Extracts list items from the first section whose header matches a provided set.
|
|
155
216
|
*
|
|
@@ -169,15 +230,15 @@ function findSection(sections, headers) {
|
|
|
169
230
|
return [];
|
|
170
231
|
}
|
|
171
232
|
/**
|
|
172
|
-
* Extracts list items from the Scope section, stripping trailing parenthetical annotations.
|
|
233
|
+
* Extracts raw list items from the Scope section, stripping trailing parenthetical annotations.
|
|
173
234
|
*
|
|
174
235
|
* Scope section items often include human-readable annotations like "(new)" or
|
|
175
|
-
* "(thread flag through...)". These are stripped so that
|
|
176
|
-
*
|
|
236
|
+
* "(thread flag through...)". These are stripped so that callers can then parse
|
|
237
|
+
* bare paths/globs with parseScopeItem.
|
|
177
238
|
*
|
|
178
239
|
* @param sections - Map of normalized header names to their section content
|
|
179
240
|
* @param headers - Set of normalized Scope header variants (e.g., SCOPE_HEADERS)
|
|
180
|
-
* @returns An array of scope
|
|
241
|
+
* @returns An array of raw scope strings with parentheticals removed, or an empty array if no Scope section is found
|
|
181
242
|
*/
|
|
182
243
|
function findScopeSection(sections, headers) {
|
|
183
244
|
for (const [header, content] of sections) {
|
|
@@ -230,9 +291,11 @@ function parseIssueBody(body) {
|
|
|
230
291
|
const sections = parseSections(body);
|
|
231
292
|
const rawScope = findScopeSection(sections, SCOPE_HEADERS);
|
|
232
293
|
const scopeBlocked = rawScope.length > 0 && rawScope.every((item) => TBD_BLOCKED_RE.test(item));
|
|
233
|
-
const filteredScope =
|
|
294
|
+
const filteredScope = scopeBlocked
|
|
295
|
+
? []
|
|
296
|
+
: rawScope.filter((item) => !TBD_BLOCKED_RE.test(item)).flatMap(parseScopeItem);
|
|
234
297
|
return {
|
|
235
|
-
scope:
|
|
298
|
+
scope: filteredScope,
|
|
236
299
|
scopeBlocked,
|
|
237
300
|
validationCommands: findSection(sections, VALIDATION_HEADERS),
|
|
238
301
|
requirements: findSection(sections, REQUIREMENTS_HEADERS),
|
package/dist/loop/continue.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveTelemetryFilePath = resolveTelemetryFilePath;
|
|
3
4
|
exports.runLoopContinue = runLoopContinue;
|
|
4
5
|
const node_path_1 = require("node:path");
|
|
5
6
|
const node_fs_1 = require("node:fs");
|
|
@@ -280,12 +281,15 @@ function bridgeEvidenceToClusterState(repoRoot, clusterId, childId, commit, rawV
|
|
|
280
281
|
: [...existing.child_states, { id: childId, status: "done", commit: commit || undefined }];
|
|
281
282
|
// Evict any stale commit entry for this child before conditionally re-adding
|
|
282
283
|
const { [childId]: _staleCommit, ...remainingCommits } = existing.commits;
|
|
284
|
+
const relativeResultFile = resultFile
|
|
285
|
+
? (0, node_path_1.relative)(repoRoot, (0, node_path_1.isAbsolute)(resultFile) ? resultFile : (0, node_path_1.resolve)(repoRoot, resultFile))
|
|
286
|
+
: resultFile;
|
|
283
287
|
const updated = {
|
|
284
288
|
...existing,
|
|
285
289
|
state_generation: existing.state_generation + 1,
|
|
286
290
|
child_states: updatedChildStates,
|
|
287
291
|
commits: commit ? { ...remainingCommits, [childId]: commit } : remainingCommits,
|
|
288
|
-
result_pointers:
|
|
292
|
+
result_pointers: relativeResultFile ? { ...existing.result_pointers, [childId]: relativeResultFile } : existing.result_pointers,
|
|
289
293
|
validation_results: { ...existing.validation_results, [childId]: toValidationResult(rawValidation) },
|
|
290
294
|
};
|
|
291
295
|
(0, store_js_1.writeClusterStateSync)(clusterId, updated, repoRoot);
|
|
@@ -601,6 +605,20 @@ function runLoopContinue(options) {
|
|
|
601
605
|
}
|
|
602
606
|
// Step 1 (cont): Atomic write of updated current-state.json (open_children_meta pruned after bridge)
|
|
603
607
|
const sha = (0, checkpoint_js_1.writeStateAtomic)(stateFile, updatedState);
|
|
608
|
+
// Also keep the canonical cluster state snapshot in sync so finalize and
|
|
609
|
+
// direct cluster-state reads see the same completed children.
|
|
610
|
+
const canonicalStatePath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", updatedState.cluster_id, "state.json");
|
|
611
|
+
if ((0, node_path_1.resolve)(stateFile) !== (0, node_path_1.resolve)(canonicalStatePath)) {
|
|
612
|
+
try {
|
|
613
|
+
(0, checkpoint_js_1.writeStateAtomic)(canonicalStatePath, updatedState);
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
617
|
+
console.error(`Error: primary state for cluster ${updatedState.cluster_id} was persisted, but the canonical ` +
|
|
618
|
+
`cluster state snapshot at ${canonicalStatePath} failed to update and remains stale: ${msg}`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
604
622
|
appendContinueLedgerEvents(repoRoot, updatedState, completedChild);
|
|
605
623
|
// Step 2: Append JSONL checkpoint event
|
|
606
624
|
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|