@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.
Files changed (41) hide show
  1. package/dist/autoresearch/proposal.js +61 -0
  2. package/dist/autoresearch/score.js +124 -0
  3. package/dist/cli/adopt-canon.js +22 -8
  4. package/dist/cli/index.js +4 -0
  5. package/dist/cli/qc.js +97 -0
  6. package/dist/config/validator.js +16 -6
  7. package/dist/finalize/artifact-policy.js +12 -3
  8. package/dist/finalize/index.js +36 -21
  9. package/dist/finalize/run-report.js +189 -19
  10. package/dist/finalize/steps/05-generate-report.js +5 -1
  11. package/dist/finalize/steps/12-archive.js +6 -0
  12. package/dist/loop/adapters/terminal-cli.js +159 -25
  13. package/dist/loop/body-parser.js +73 -6
  14. package/dist/loop/continue.js +22 -4
  15. package/dist/loop/dispatch.js +103 -68
  16. package/dist/loop/orphan-recovery.js +2 -1
  17. package/dist/loop/parent.js +50 -15
  18. package/dist/loop/router/engine.js +1 -0
  19. package/dist/loop/run-bootstrap.js +3 -1
  20. package/dist/loop/run-preflight.js +4 -1
  21. package/dist/loop/worker-packet.js +81 -2
  22. package/dist/map/inference.js +4 -1
  23. package/dist/medic/routing-signals.js +60 -0
  24. package/dist/medic/run-health-consult.js +5 -4
  25. package/dist/medic/treatment-packets.js +5 -1
  26. package/dist/qc/policy.js +2 -0
  27. package/dist/qc/providers/coderabbit.js +140 -10
  28. package/dist/qc/repair-loop.js +147 -1
  29. package/dist/qc/repair-packets.js +16 -3
  30. package/dist/qc/routing.js +6 -0
  31. package/dist/qc/types.js +3 -0
  32. package/dist/run-health/index.js +12 -0
  33. package/dist/skill-packet/generator.js +234 -3
  34. package/dist/smartdocs-engine/canon-check.js +5 -5
  35. package/dist/smartdocs-engine/index.js +54 -0
  36. package/dist/smartdocs-engine/seed-instructions.js +159 -4
  37. package/dist/smartdocs-engine/validate-instructions.js +46 -4
  38. package/dist/tracker/local-graph.js +106 -3
  39. package/dist/workspace/POLARIS.md +4 -0
  40. package/dist/workspace/SUMMARY.md +56 -0
  41. 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 + 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;
@@ -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 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
+ 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
- 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,
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; falls back to the last 500 chars.
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
- return trimmed;
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
@@ -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 allowed_scope entries in
176
- * worker packets contain only bare file paths or valid glob patterns.
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 paths with parentheticals removed, or an empty array if no Scope section is found
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 = rawScope.filter((item) => !TBD_BLOCKED_RE.test(item));
298
+ const filteredScope = scopeBlocked
299
+ ? []
300
+ : rawScope.filter((item) => !TBD_BLOCKED_RE.test(item)).flatMap(parseScopeItem);
234
301
  return {
235
- scope: scopeBlocked ? [] : filteredScope,
302
+ scope: filteredScope,
236
303
  scopeBlocked,
237
304
  validationCommands: findSection(sections, VALIDATION_HEADERS),
238
305
  requirements: findSection(sections, REQUIREMENTS_HEADERS),