@lsctech/polaris 0.5.11 → 0.5.13
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/adopt-canon.js +22 -8
- 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 +12 -3
- package/dist/finalize/index.js +36 -21
- package/dist/finalize/run-report.js +189 -19
- package/dist/finalize/steps/05-generate-report.js +5 -1
- package/dist/finalize/steps/12-archive.js +6 -0
- package/dist/loop/adapters/terminal-cli.js +159 -25
- package/dist/loop/body-parser.js +73 -6
- package/dist/loop/continue.js +22 -4
- package/dist/loop/dispatch.js +103 -68
- package/dist/loop/orphan-recovery.js +2 -1
- package/dist/loop/parent.js +50 -15
- package/dist/loop/router/engine.js +1 -0
- package/dist/loop/run-bootstrap.js +3 -1
- package/dist/loop/run-preflight.js +4 -1
- package/dist/loop/worker-packet.js +81 -2
- package/dist/map/inference.js +4 -1
- package/dist/medic/routing-signals.js +60 -0
- package/dist/medic/run-health-consult.js +5 -4
- package/dist/medic/treatment-packets.js +5 -1
- package/dist/qc/policy.js +2 -0
- package/dist/qc/providers/coderabbit.js +140 -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/run-health/index.js +12 -0
- package/dist/skill-packet/generator.js +234 -3
- package/dist/smartdocs-engine/canon-check.js +5 -5
- package/dist/smartdocs-engine/index.js +54 -0
- package/dist/smartdocs-engine/seed-instructions.js +159 -4
- package/dist/smartdocs-engine/validate-instructions.js +46 -4
- package/dist/tracker/local-graph.js +106 -3
- package/dist/workspace/POLARIS.md +4 -0
- package/dist/workspace/SUMMARY.md +56 -0
- 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 | Title | 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;
|
|
@@ -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.resolve)(repoRoot, 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,81 @@ 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
|
-
|
|
501
|
+
let finalStatus = isValidCompactReturn
|
|
502
|
+
? compact["status"]
|
|
503
|
+
: "failed";
|
|
504
|
+
let finalValidation = isValidCompactReturn
|
|
505
|
+
? compact["validation"]
|
|
506
|
+
: "failed";
|
|
507
|
+
let finalNext = isValidCompactReturn
|
|
508
|
+
? compact["next_recommended_action"]
|
|
509
|
+
: "stop";
|
|
510
|
+
// Reconcile validation/next_recommended_action with run-health symptoms.
|
|
511
|
+
// A validation-failed symptom overrides a passing validation string and
|
|
512
|
+
// prevents a continue recommendation when the build/tests did not pass.
|
|
513
|
+
const rawSymptoms = compact["run_health_symptoms"];
|
|
514
|
+
const hasValidationFailedSymptom = Array.isArray(rawSymptoms) &&
|
|
515
|
+
rawSymptoms.some((s) => typeof s === "object" &&
|
|
516
|
+
s !== null &&
|
|
517
|
+
s["category"] === "validation-failed");
|
|
518
|
+
if (hasValidationFailedSymptom) {
|
|
519
|
+
finalValidation = "failed";
|
|
520
|
+
finalNext = "stop";
|
|
521
|
+
}
|
|
522
|
+
// "continue" requires a passing validation result. If validation failed,
|
|
523
|
+
// the recommendation must be a terminal stop. If validation was skipped,
|
|
524
|
+
// treat the successful exit as a pass for the completion contract.
|
|
525
|
+
if (finalStatus === "done" && finalNext === "continue") {
|
|
526
|
+
if (finalValidation === "failed") {
|
|
527
|
+
finalNext = "stop";
|
|
528
|
+
}
|
|
529
|
+
else if (finalValidation === "skipped") {
|
|
530
|
+
finalValidation = "passed";
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// A failed status should never recommend continuation.
|
|
534
|
+
if ((finalStatus === "failed" || finalStatus === "blocked") && finalNext === "continue") {
|
|
535
|
+
finalNext = "stop";
|
|
536
|
+
}
|
|
537
|
+
const errorMessage = finalStatus === "failed" || finalStatus === "blocked"
|
|
538
|
+
? isValidCompactReturn
|
|
539
|
+
? typeof compact["error_message"] === "string"
|
|
540
|
+
? compact["error_message"]
|
|
541
|
+
: typeof compact["blocker"] === "string"
|
|
542
|
+
? compact["blocker"]
|
|
543
|
+
: stdout || summary
|
|
544
|
+
: `CompactReturn validation failed: ${compactReturnErrors.join("; ")}`
|
|
545
|
+
: undefined;
|
|
504
546
|
const sealedResult = {
|
|
505
547
|
run_id: packet.run_id,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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,
|
|
548
|
+
cluster_id: packet.cluster_id,
|
|
549
|
+
child_id: compact["child_id"],
|
|
550
|
+
status: finalStatus,
|
|
551
|
+
commit: compact["commit"],
|
|
552
|
+
validation: finalValidation,
|
|
553
|
+
next_recommended_action: finalNext,
|
|
521
554
|
};
|
|
555
|
+
if (isValidCompactReturn) {
|
|
556
|
+
if (compact["result_data"] !== undefined) {
|
|
557
|
+
sealedResult["result_data"] = compact["result_data"];
|
|
558
|
+
}
|
|
559
|
+
if (compact["work_note_paths"] !== undefined) {
|
|
560
|
+
sealedResult["work_note_paths"] = compact["work_note_paths"];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (Array.isArray(compact["run_health_symptoms"])) {
|
|
564
|
+
sealedResult["run_health_symptoms"] = compact["run_health_symptoms"];
|
|
565
|
+
}
|
|
566
|
+
if (finalStatus === "blocked" && typeof compact["blocker"] === "string") {
|
|
567
|
+
sealedResult["blocker"] = compact["blocker"];
|
|
568
|
+
}
|
|
569
|
+
if (errorMessage !== undefined) {
|
|
570
|
+
sealedResult["error_message"] = errorMessage;
|
|
571
|
+
}
|
|
522
572
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(resultFile), { recursive: true });
|
|
523
573
|
node_fs_1.default.writeFileSync(resultFile, JSON.stringify(sealedResult, null, 2), "utf-8");
|
|
524
574
|
}
|
|
@@ -533,14 +583,26 @@ exports.TerminalCliAdapter = TerminalCliAdapter;
|
|
|
533
583
|
* Normalize a legacy CompactReturn shape to the current spec before validation.
|
|
534
584
|
* Handles pre-spec formats produced by older workers:
|
|
535
585
|
* - status:"success"|"completed" → status:"done"
|
|
586
|
+
* - status:"failure"|"error"|"in-progress" → status:"failed"
|
|
536
587
|
* - validation:{passed:[...],failed:[...]} → validation:"passed"|"failed"|"skipped"
|
|
588
|
+
* - next_action:"continue"|"stop"|"escalate" → next_recommended_action
|
|
537
589
|
* - missing boolean flags → false
|
|
590
|
+
* - missing or empty child_id → active_child from the bootstrap packet
|
|
538
591
|
*/
|
|
539
|
-
function normalizeLegacyCompactReturn(raw) {
|
|
592
|
+
function normalizeLegacyCompactReturn(raw, activeChild) {
|
|
540
593
|
const result = { ...raw };
|
|
594
|
+
if (typeof result['child_id'] !== 'string' || !result['child_id']) {
|
|
595
|
+
result['child_id'] = activeChild;
|
|
596
|
+
}
|
|
541
597
|
if (result['status'] === 'success' || result['status'] === 'completed') {
|
|
542
598
|
result['status'] = 'done';
|
|
543
599
|
}
|
|
600
|
+
else if (result['status'] === 'failure' || result['status'] === 'error' || result['status'] === 'in-progress') {
|
|
601
|
+
result['status'] = 'failed';
|
|
602
|
+
}
|
|
603
|
+
else if (result['status'] !== 'done' && result['status'] !== 'failed' && result['status'] !== 'blocked') {
|
|
604
|
+
result['status'] = 'failed';
|
|
605
|
+
}
|
|
544
606
|
if (typeof result['validation'] === 'object' && result['validation'] !== null) {
|
|
545
607
|
const v = result['validation'];
|
|
546
608
|
if (Array.isArray(v['failed']) && v['failed'].length > 0) {
|
|
@@ -553,6 +615,19 @@ function normalizeLegacyCompactReturn(raw) {
|
|
|
553
615
|
result['validation'] = 'skipped';
|
|
554
616
|
}
|
|
555
617
|
}
|
|
618
|
+
if (typeof result['next_action'] === 'string' && !result['next_recommended_action']) {
|
|
619
|
+
const action = result['next_action'];
|
|
620
|
+
if (action === 'continue') {
|
|
621
|
+
result['next_recommended_action'] = 'continue';
|
|
622
|
+
}
|
|
623
|
+
else if (action === 'stop') {
|
|
624
|
+
result['next_recommended_action'] = 'stop';
|
|
625
|
+
}
|
|
626
|
+
else if (action === 'escalate') {
|
|
627
|
+
result['next_recommended_action'] = 'investigate';
|
|
628
|
+
}
|
|
629
|
+
delete result['next_action'];
|
|
630
|
+
}
|
|
556
631
|
for (const flag of ['tracker_updated', 'state_updated', 'telemetry_updated']) {
|
|
557
632
|
if (typeof result[flag] !== 'boolean') {
|
|
558
633
|
result[flag] = false;
|
|
@@ -560,9 +635,63 @@ function normalizeLegacyCompactReturn(raw) {
|
|
|
560
635
|
}
|
|
561
636
|
return result;
|
|
562
637
|
}
|
|
638
|
+
function compactReturnDefaults(status) {
|
|
639
|
+
return {
|
|
640
|
+
validation: status === 'done' ? 'passed' : status === 'failed' ? 'failed' : 'skipped',
|
|
641
|
+
next_recommended_action: status === 'done' ? 'continue' : 'stop',
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function resolveCommit(raw) {
|
|
645
|
+
if (typeof raw['commit'] === 'string') {
|
|
646
|
+
return raw['commit'];
|
|
647
|
+
}
|
|
648
|
+
if (typeof raw['commit_hash'] === 'string') {
|
|
649
|
+
return raw['commit_hash'];
|
|
650
|
+
}
|
|
651
|
+
if (typeof raw['commit_sha'] === 'string') {
|
|
652
|
+
return raw['commit_sha'];
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
function isValidValidation(value) {
|
|
657
|
+
return value === 'passed' || value === 'failed' || value === 'skipped';
|
|
658
|
+
}
|
|
659
|
+
function isValidNextAction(value) {
|
|
660
|
+
return value === 'continue' || value === 'stop' || value === 'investigate';
|
|
661
|
+
}
|
|
662
|
+
function buildCompactReturnForSealedResult(normalized, exitCode) {
|
|
663
|
+
const rawStatus = normalized['status'];
|
|
664
|
+
let baseStatus;
|
|
665
|
+
if (rawStatus === 'done' || rawStatus === 'failed' || rawStatus === 'blocked') {
|
|
666
|
+
baseStatus = rawStatus;
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
baseStatus = 'failed';
|
|
670
|
+
}
|
|
671
|
+
const effectiveStatus = exitCode === 0
|
|
672
|
+
? baseStatus
|
|
673
|
+
: baseStatus === 'failed' || baseStatus === 'blocked'
|
|
674
|
+
? baseStatus
|
|
675
|
+
: 'failed';
|
|
676
|
+
const defaults = compactReturnDefaults(effectiveStatus);
|
|
677
|
+
const compact = {
|
|
678
|
+
...normalized,
|
|
679
|
+
child_id: String(normalized['child_id'] || ''),
|
|
680
|
+
status: effectiveStatus,
|
|
681
|
+
commit: resolveCommit(normalized),
|
|
682
|
+
validation: isValidValidation(normalized['validation']) ? normalized['validation'] : defaults.validation,
|
|
683
|
+
next_recommended_action: isValidNextAction(normalized['next_recommended_action'])
|
|
684
|
+
? normalized['next_recommended_action']
|
|
685
|
+
: defaults.next_recommended_action,
|
|
686
|
+
tracker_updated: typeof normalized['tracker_updated'] === 'boolean' ? normalized['tracker_updated'] : false,
|
|
687
|
+
state_updated: typeof normalized['state_updated'] === 'boolean' ? normalized['state_updated'] : false,
|
|
688
|
+
telemetry_updated: typeof normalized['telemetry_updated'] === 'boolean' ? normalized['telemetry_updated'] : false,
|
|
689
|
+
};
|
|
690
|
+
return compact;
|
|
691
|
+
}
|
|
563
692
|
/**
|
|
564
693
|
* Extract a worker summary from stdout.
|
|
565
|
-
* Looks for the last line that is valid JSON
|
|
694
|
+
* Looks for the last line that is a valid JSON *object*; falls back to the last 500 chars.
|
|
566
695
|
*/
|
|
567
696
|
function extractSummary(stdout) {
|
|
568
697
|
if (!stdout)
|
|
@@ -573,8 +702,13 @@ function extractSummary(stdout) {
|
|
|
573
702
|
if (!trimmed)
|
|
574
703
|
continue;
|
|
575
704
|
try {
|
|
576
|
-
JSON.parse(trimmed);
|
|
577
|
-
|
|
705
|
+
const parsed = JSON.parse(trimmed);
|
|
706
|
+
if (typeof parsed === 'object' &&
|
|
707
|
+
parsed !== null &&
|
|
708
|
+
!Array.isArray(parsed)) {
|
|
709
|
+
return trimmed;
|
|
710
|
+
}
|
|
711
|
+
// not an object-shaped JSON value, keep looking
|
|
578
712
|
}
|
|
579
713
|
catch {
|
|
580
714
|
// not JSON, keep looking
|
package/dist/loop/body-parser.js
CHANGED
|
@@ -150,6 +150,71 @@ 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
|
+
// Dot-files that are files, not directories, must not get a trailing `/**`.
|
|
172
|
+
if (finalSegment === '.gitignore' || finalSegment === '.gitattributes') {
|
|
173
|
+
return pattern;
|
|
174
|
+
}
|
|
175
|
+
// Check if final segment has a file extension (not counting leading dot in dot-directories)
|
|
176
|
+
const hasExtension = finalSegment.startsWith('.')
|
|
177
|
+
? finalSegment.slice(1).includes('.') // For .github, .vscode: no extension. For .env.local: has extension
|
|
178
|
+
: finalSegment.includes('.');
|
|
179
|
+
if (!hasExtension &&
|
|
180
|
+
!pattern.endsWith('*') &&
|
|
181
|
+
!pattern.endsWith('?')) {
|
|
182
|
+
return pattern + '/**';
|
|
183
|
+
}
|
|
184
|
+
return pattern;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Extracts bare file/glob patterns from a raw scope bullet line.
|
|
188
|
+
*
|
|
189
|
+
* Strips backticks and trailing prose (e.g. `src/foo.ts some comment`), splits
|
|
190
|
+
* comma/semicolon separated entries, and returns only path-like leading tokens.
|
|
191
|
+
*/
|
|
192
|
+
function parseScopeItem(raw) {
|
|
193
|
+
const cleaned = raw.replace(/`/g, '').trim();
|
|
194
|
+
if (cleaned.length === 0)
|
|
195
|
+
return [];
|
|
196
|
+
const parts = cleaned
|
|
197
|
+
.split(/[,;]+/)
|
|
198
|
+
.map((s) => stripTrailingParenthetical(s.trim()))
|
|
199
|
+
.filter((s) => s.length > 0);
|
|
200
|
+
const results = [];
|
|
201
|
+
for (const part of parts) {
|
|
202
|
+
const tokens = part.split(/\s+/);
|
|
203
|
+
const pathTokens = [];
|
|
204
|
+
for (const token of tokens) {
|
|
205
|
+
if (isPathLikeToken(token)) {
|
|
206
|
+
pathTokens.push(token);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const pathToken of pathTokens) {
|
|
213
|
+
results.push(normalizeDirectoryPattern(pathToken));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
153
218
|
/**
|
|
154
219
|
* Extracts list items from the first section whose header matches a provided set.
|
|
155
220
|
*
|
|
@@ -169,15 +234,15 @@ function findSection(sections, headers) {
|
|
|
169
234
|
return [];
|
|
170
235
|
}
|
|
171
236
|
/**
|
|
172
|
-
* Extracts list items from the Scope section, stripping trailing parenthetical annotations.
|
|
237
|
+
* Extracts raw list items from the Scope section, stripping trailing parenthetical annotations.
|
|
173
238
|
*
|
|
174
239
|
* Scope section items often include human-readable annotations like "(new)" or
|
|
175
|
-
* "(thread flag through...)". These are stripped so that
|
|
176
|
-
*
|
|
240
|
+
* "(thread flag through...)". These are stripped so that callers can then parse
|
|
241
|
+
* bare paths/globs with parseScopeItem.
|
|
177
242
|
*
|
|
178
243
|
* @param sections - Map of normalized header names to their section content
|
|
179
244
|
* @param headers - Set of normalized Scope header variants (e.g., SCOPE_HEADERS)
|
|
180
|
-
* @returns An array of scope
|
|
245
|
+
* @returns An array of raw scope strings with parentheticals removed, or an empty array if no Scope section is found
|
|
181
246
|
*/
|
|
182
247
|
function findScopeSection(sections, headers) {
|
|
183
248
|
for (const [header, content] of sections) {
|
|
@@ -230,9 +295,11 @@ function parseIssueBody(body) {
|
|
|
230
295
|
const sections = parseSections(body);
|
|
231
296
|
const rawScope = findScopeSection(sections, SCOPE_HEADERS);
|
|
232
297
|
const scopeBlocked = rawScope.length > 0 && rawScope.every((item) => TBD_BLOCKED_RE.test(item));
|
|
233
|
-
const filteredScope =
|
|
298
|
+
const filteredScope = scopeBlocked
|
|
299
|
+
? []
|
|
300
|
+
: rawScope.filter((item) => !TBD_BLOCKED_RE.test(item)).flatMap(parseScopeItem);
|
|
234
301
|
return {
|
|
235
|
-
scope:
|
|
302
|
+
scope: filteredScope,
|
|
236
303
|
scopeBlocked,
|
|
237
304
|
validationCommands: findSection(sections, VALIDATION_HEADERS),
|
|
238
305
|
requirements: findSection(sections, REQUIREMENTS_HEADERS),
|