@lsctech/polaris 0.5.10 → 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.
@@ -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 + open_by_severity.high +
9
- open_by_severity.medium + open_by_severity.low + open_by_severity.info;
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
- | 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
86
-
87
- | ID | Title | Commit | Status |
88
- |---|---|---|---|
89
- ${childRows || "_No children recorded_"}
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 content = (0, run_report_js_1.generateRunReport)({ state, branch, validationPassed });
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 promotedTargets = (0, artifact_policy_js_1.getArtifactPromotionStageTargets)(state.cluster_id)
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 compactReturnErrors = (0, compact_return_js_1.validateCompactReturn)(normalized);
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
- // If the parsed result isn't a valid CompactReturn, treat it as a failure
501
- // regardless of exit code — this prevents phantom successes from workers
502
- // that return minimal status objects instead of proper CompactReturn structs.
503
- const effectiveStatus = (exitCode === 0 && isValidCompactReturn) ? "success" : "failure";
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: String(normalized["child_id"] ?? packet.active_child),
507
- status: effectiveStatus,
508
- commit: typeof normalized["commit"] === "string"
509
- ? normalized["commit"]
510
- : typeof normalized["commit_hash"] === "string"
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; falls back to the last 500 chars.
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
- return trimmed;
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
@@ -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 allowed_scope entries in
176
- * worker packets contain only bare file paths or valid glob patterns.
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 paths with parentheticals removed, or an empty array if no Scope section is found
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 = rawScope.filter((item) => !TBD_BLOCKED_RE.test(item));
294
+ const filteredScope = scopeBlocked
295
+ ? []
296
+ : rawScope.filter((item) => !TBD_BLOCKED_RE.test(item)).flatMap(parseScopeItem);
234
297
  return {
235
- scope: scopeBlocked ? [] : filteredScope,
298
+ scope: filteredScope,
236
299
  scopeBlocked,
237
300
  validationCommands: findSection(sections, VALIDATION_HEADERS),
238
301
  requirements: findSection(sections, REQUIREMENTS_HEADERS),
@@ -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: resultFile ? { ...existing.result_pointers, [childId]: resultFile } : existing.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");