@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
@@ -86,6 +86,28 @@ const ROUTER_FAILURE_FIX_ZONE_MAP = {
86
86
  hint: "Worker role is repeatedly disabled by policy. Update providerPolicy.worker and role routing defaults.",
87
87
  },
88
88
  };
89
+ const ROUTER_ANOMALY_FIX_ZONE_MAP = {
90
+ "provider-monopoly": {
91
+ artifact_type: "provider-role-recommendation",
92
+ hint: "Repeated same-provider selection when policy evidence shows multiple providers were eligible. Review provider role assignment and policy ordering to avoid routing concentration.",
93
+ },
94
+ "missing-evidence": {
95
+ artifact_type: "runtime-config",
96
+ hint: "Routing evidence gaps detected (missing exhausted reason, router candidates, or child completion). Improve telemetry and dispatch evidence capture.",
97
+ },
98
+ "missing-sealed-result": {
99
+ artifact_type: "medic-template",
100
+ hint: "Missing sealed result file indicates a state-repair review signal. Investigate worker result writing and dispatch boundary.",
101
+ },
102
+ "stale-dispatch-abort": {
103
+ artifact_type: "medic-template",
104
+ hint: "Stale dispatch abort indicates a state-repair review signal. Investigate dispatch lifecycle and orphan recovery.",
105
+ },
106
+ "invalid-inline-attempt": {
107
+ artifact_type: "medic-template",
108
+ hint: "Invalid inline attempt indicates a state-repair review signal. Investigate dispatch boundary enforcement.",
109
+ },
110
+ };
89
111
  // ──────────────────────────────────────────────
90
112
  // Build proposals from a DiagnosisReport
91
113
  // ──────────────────────────────────────────────
@@ -202,6 +224,33 @@ function buildProposals(report) {
202
224
  fix_zone: `${entry.artifact_type}/router-failure-${failure.reason}`,
203
225
  });
204
226
  }
227
+ // Routing anomaly signals (provider monopoly, evidence gaps, state-repair).
228
+ const routerOutcomes = report.router_outcomes;
229
+ const anomalySignals = [
230
+ ...(routerOutcomes?.provider_monopoly_signals ?? []),
231
+ ...(routerOutcomes?.evidence_gap_signals ?? []),
232
+ ...(routerOutcomes?.state_repair_signals ?? []),
233
+ ];
234
+ for (const signal of anomalySignals) {
235
+ // Evidence gaps and provider monopoly must be recurring to be worth a proposal.
236
+ if (signal.signal === "missing-evidence" && signal.occurrences < 2)
237
+ continue;
238
+ if (signal.signal === "provider-monopoly" && signal.occurrences < 2)
239
+ continue;
240
+ const entry = ROUTER_ANOMALY_FIX_ZONE_MAP[signal.signal] ?? {
241
+ artifact_type: "runtime-config",
242
+ hint: "Recurring routing anomaly detected. Review dispatch telemetry and routing policy.",
243
+ };
244
+ proposals.push({
245
+ gate_id: `router-anomaly:${signal.signal}:${signal.reason}`,
246
+ artifact_type: entry.artifact_type,
247
+ hint: `${entry.hint} Observed ${signal.occurrences} times across children: ${signal.child_ids.join(", ") || "unknown"}.`,
248
+ run_id: report.run_id,
249
+ evidence_run_ids: [report.run_id],
250
+ confidence: report.score,
251
+ fix_zone: `${entry.artifact_type}/router-anomaly-${signal.signal}`,
252
+ });
253
+ }
205
254
  return proposals;
206
255
  }
207
256
  // ──────────────────────────────────────────────
@@ -291,6 +340,15 @@ function validateDiagnosisReport(raw) {
291
340
  recurring_failures: Array.isArray(routerOutcomesRaw["recurring_failures"])
292
341
  ? routerOutcomesRaw["recurring_failures"]
293
342
  : [],
343
+ provider_monopoly_signals: Array.isArray(routerOutcomesRaw["provider_monopoly_signals"])
344
+ ? routerOutcomesRaw["provider_monopoly_signals"]
345
+ : [],
346
+ evidence_gap_signals: Array.isArray(routerOutcomesRaw["evidence_gap_signals"])
347
+ ? routerOutcomesRaw["evidence_gap_signals"]
348
+ : [],
349
+ state_repair_signals: Array.isArray(routerOutcomesRaw["state_repair_signals"])
350
+ ? routerOutcomesRaw["state_repair_signals"]
351
+ : [],
294
352
  }
295
353
  : {
296
354
  total_decisions: 0,
@@ -298,6 +356,9 @@ function validateDiagnosisReport(raw) {
298
356
  fallback_attempts: 0,
299
357
  successful_fallbacks: 0,
300
358
  recurring_failures: [],
359
+ provider_monopoly_signals: [],
360
+ evidence_gap_signals: [],
361
+ state_repair_signals: [],
301
362
  };
302
363
  const normalizedQcSummary = normalizeQcScoreSummary(r["qc_summary"]);
303
364
  return {
@@ -24,6 +24,7 @@ exports.summarizeRouterOutcomes = summarizeRouterOutcomes;
24
24
  exports.scoreRun = scoreRun;
25
25
  const node_fs_1 = require("node:fs");
26
26
  const node_path_1 = require("node:path");
27
+ const routing_signals_js_1 = require("../medic/routing-signals.js");
27
28
  const artifacts_js_1 = require("../qc/artifacts.js");
28
29
  const store_js_1 = require("../cluster-state/store.js");
29
30
  const gates_js_1 = require("./gates.js");
@@ -639,12 +640,135 @@ function summarizeRouterOutcomes(artifacts) {
639
640
  child_ids: Array.from(value.childIds).sort(),
640
641
  }))
641
642
  .sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
643
+ // ── Provider monopoly detection ─────────────────────────────────────────────
644
+ // Repeated same-provider selection when policy evidence shows multiple
645
+ // providers were eligible or configured.
646
+ const hasMultiProviderEvidence = (event) => {
647
+ const candidates = Array.isArray(event["router_candidates"]) ? event["router_candidates"] : [];
648
+ const eligibleCount = candidates.filter((candidate) => asRecord(candidate)?.["eligible"] === true).length;
649
+ const routingSummary = asRecord(event["routing_summary"]);
650
+ const effectivePolicyOrder = asStringArray(routingSummary?.["effective_policy_order"]);
651
+ const providersTried = asStringArray(event["providers_tried"]);
652
+ const fallbackAttempts = Array.isArray(event["fallback_attempts"]) ? event["fallback_attempts"] : [];
653
+ return (eligibleCount > 1 ||
654
+ effectivePolicyOrder.length > 1 ||
655
+ providersTried.length > 1 ||
656
+ fallbackAttempts.length > 0 ||
657
+ routingSummary?.["fallback_eligible"] === true);
658
+ };
659
+ const providerSelections = new Map();
660
+ for (const event of selectedEvents) {
661
+ const selectedProvider = asString(event["selected_provider"]);
662
+ if (!selectedProvider || !hasMultiProviderEvidence(event))
663
+ continue;
664
+ const childId = asString(event["child_id"]) ?? undefined;
665
+ const existing = providerSelections.get(selectedProvider);
666
+ if (existing) {
667
+ existing.occurrences += 1;
668
+ if (childId)
669
+ existing.child_ids.push(childId);
670
+ }
671
+ else {
672
+ providerSelections.set(selectedProvider, {
673
+ signal: "provider-monopoly",
674
+ reason: selectedProvider,
675
+ occurrences: 1,
676
+ child_ids: childId ? [childId] : [],
677
+ });
678
+ }
679
+ }
680
+ const providerMonopolySignals = Array.from(providerSelections.values())
681
+ .filter((signal) => signal.occurrences >= 2)
682
+ .map((signal) => ({
683
+ ...signal,
684
+ child_ids: [...new Set(signal.child_ids)].sort(),
685
+ }))
686
+ .sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
687
+ // ── Evidence gap detection ──────────────────────────────────────────────────
688
+ // Missing evidence that prevents distinguishing true provider failures from
689
+ // routing/telemetry gaps.
690
+ const evidenceGapMap = new Map();
691
+ const countEvidenceGap = (reason, childId) => {
692
+ const existing = evidenceGapMap.get(reason);
693
+ if (existing) {
694
+ existing.occurrences += 1;
695
+ if (childId)
696
+ existing.child_ids.push(childId);
697
+ }
698
+ else {
699
+ evidenceGapMap.set(reason, {
700
+ signal: "missing-evidence",
701
+ reason,
702
+ occurrences: 1,
703
+ child_ids: childId ? [childId] : [],
704
+ });
705
+ }
706
+ };
707
+ for (const event of selectedEvents) {
708
+ const selectedProvider = asString(event["selected_provider"]);
709
+ const childId = asString(event["child_id"]) ?? undefined;
710
+ const candidates = Array.isArray(event["router_candidates"]) ? event["router_candidates"] : [];
711
+ const routingSummary = asRecord(event["routing_summary"]);
712
+ const fallbackAttempts = Array.isArray(event["fallback_attempts"]) ? event["fallback_attempts"] : [];
713
+ if (selectedProvider === null) {
714
+ const exhaustedReason = asString(event["router_exhausted_reason"]);
715
+ if (!exhaustedReason && candidates.length === 0) {
716
+ countEvidenceGap("missing-exhausted-reason", childId);
717
+ }
718
+ }
719
+ else if (selectedProvider !== null) {
720
+ const registryPresent = routingSummary?.["registry_present"] === true;
721
+ if (registryPresent && candidates.length === 0) {
722
+ countEvidenceGap("missing-router-candidates", childId);
723
+ }
724
+ if (fallbackAttempts.length > 0 && childId && !childCompletionStatus.has(childId)) {
725
+ countEvidenceGap("missing-child-completion", childId);
726
+ }
727
+ }
728
+ }
729
+ for (const event of exhaustedEvents) {
730
+ const childId = asString(event["child_id"]) ?? undefined;
731
+ const reason = asString(event["reason"]);
732
+ if (!reason && childId) {
733
+ countEvidenceGap("missing-exhausted-reason", childId);
734
+ }
735
+ }
736
+ const evidenceGapSignals = Array.from(evidenceGapMap.values())
737
+ .map((signal) => ({
738
+ ...signal,
739
+ child_ids: [...new Set(signal.child_ids)].sort(),
740
+ }))
741
+ .sort((a, b) => b.occurrences - a.occurrences || a.reason.localeCompare(b.reason));
742
+ // ── State-repair / Medic review signal classification ───────────────────────
743
+ const stateRepairMap = new Map();
744
+ for (const event of telemetry) {
745
+ const classified = (0, routing_signals_js_1.classifyRoutingTelemetryEvent)(event);
746
+ if (!classified)
747
+ continue;
748
+ const existing = stateRepairMap.get(classified.signal);
749
+ if (existing) {
750
+ existing.occurrences += classified.occurrences;
751
+ existing.child_ids.push(...classified.child_ids);
752
+ }
753
+ else {
754
+ stateRepairMap.set(classified.signal, { ...classified });
755
+ }
756
+ }
757
+ const stateRepairSignals = Array.from(stateRepairMap.values())
758
+ .map((signal) => ({
759
+ ...signal,
760
+ child_ids: [...new Set(signal.child_ids)].sort(),
761
+ }))
762
+ .sort((a, b) => b.occurrences - a.occurrences || a.signal.localeCompare(b.signal));
642
763
  return {
643
764
  total_decisions: selectedEvents.length,
644
765
  exhausted_decisions: exhaustedEvents.length,
645
766
  fallback_attempts: fallbackEvents.length,
646
767
  successful_fallbacks: successfulFallbacks,
647
768
  recurring_failures: recurringFailures,
769
+ provider_monopoly_signals: providerMonopolySignals,
770
+ evidence_gap_signals: evidenceGapSignals,
771
+ state_repair_signals: stateRepairSignals,
648
772
  };
649
773
  }
650
774
  // ──────────────────────────────────────────────
@@ -6,6 +6,7 @@ const node_path_1 = require("node:path");
6
6
  const node_child_process_1 = require("node:child_process");
7
7
  const loader_js_1 = require("../config/loader.js");
8
8
  const librarian_dispatch_js_1 = require("../smartdocs-engine/librarian-dispatch.js");
9
+ const seed_instructions_js_1 = require("../smartdocs-engine/seed-instructions.js");
9
10
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".polaris", "smartdocs"]);
10
11
  function loadInventoryCanonicalFolders(repoRoot) {
11
12
  const inventoryPath = (0, node_path_1.join)(repoRoot, ".polaris", "adoption-inventory.json");
@@ -29,7 +30,7 @@ function scaffoldDraftSummaryFiles(repoRoot, canonicalFolders) {
29
30
  if ((0, node_fs_1.existsSync)(summaryPath))
30
31
  continue;
31
32
  (0, node_fs_1.mkdirSync)(dir, { recursive: true });
32
- (0, node_fs_1.writeFileSync)(summaryPath, `# ${folder}\n\n<!-- polaris:draft -->\n`, "utf-8");
33
+ (0, node_fs_1.writeFileSync)(summaryPath, (0, seed_instructions_js_1.generateSummaryDraft)(folder, repoRoot, {}), "utf-8");
33
34
  console.log(` Scaffolded draft SUMMARY.md: ${folder}`);
34
35
  }
35
36
  }
@@ -90,13 +91,19 @@ function injectLinkedDocs(content, docs, summaryLines) {
90
91
  const headingIdx = lines.findIndex((l) => l.startsWith("#"));
91
92
  if (headingIdx === -1)
92
93
  return content;
94
+ const heading = lines[headingIdx];
95
+ const hasSummaryContent = summaryLines.length > 0;
93
96
  const yamlLines = ["", "---", ...buildLinkedDocsBlock(docs), "---"];
94
- const before = lines.slice(0, headingIdx + 1);
95
- // If agent provided summary content, append after YAML block
96
- const after = summaryLines.length > 0
97
- ? ["", ...summaryLines]
98
- : lines.slice(headingIdx + 1);
99
- return [...before, ...yamlLines, ...after].join("\n");
97
+ const bodyLines = hasSummaryContent
98
+ ? [...yamlLines, "", ...summaryLines]
99
+ : [...yamlLines, ...lines.slice(headingIdx + 1).filter((l) => {
100
+ const t = l.trim();
101
+ return t !== seed_instructions_js_1.DRAFT_MARKER && t !== seed_instructions_js_1.GENERATED_START_MARKER && t !== seed_instructions_js_1.GENERATED_END_MARKER;
102
+ })];
103
+ const generatedRegion = [heading, ...bodyLines].join("\n");
104
+ return hasSummaryContent
105
+ ? [seed_instructions_js_1.GENERATED_START_MARKER, generatedRegion, seed_instructions_js_1.GENERATED_END_MARKER].join("\n")
106
+ : [seed_instructions_js_1.DRAFT_MARKER, seed_instructions_js_1.GENERATED_START_MARKER, generatedRegion, seed_instructions_js_1.GENERATED_END_MARKER].join("\n");
100
107
  }
101
108
  function parseCanonResponse(text) {
102
109
  const trimmed = text.trim();
@@ -249,12 +256,19 @@ async function enrichCanonFiles(repoRoot) {
249
256
  if ((response.polaris_lines ?? []).length > 0) {
250
257
  const polarisPath = (0, node_path_1.join)(dir, "POLARIS.md");
251
258
  const polarisContent = [
259
+ seed_instructions_js_1.GENERATED_START_MARKER,
252
260
  `# POLARIS — ${routeFolder}`,
253
261
  "",
254
262
  ...response.polaris_lines,
255
263
  "",
264
+ seed_instructions_js_1.GENERATED_END_MARKER,
256
265
  ].join("\n");
257
- (0, node_fs_1.writeFileSync)(polarisPath, polarisContent, "utf-8");
266
+ if (!(0, node_fs_1.existsSync)(polarisPath) || (0, seed_instructions_js_1.hasDraftMarker)(polarisPath)) {
267
+ (0, node_fs_1.writeFileSync)(polarisPath, polarisContent, "utf-8");
268
+ }
269
+ else {
270
+ console.log(` Skipping POLARIS.md (existing non-draft): ${routeFolder}`);
271
+ }
258
272
  }
259
273
  enrichedCount++;
260
274
  console.log(` ✓ Enriched ${routeFolder} with ${response.relevant_docs.length} linked docs`);
package/dist/cli/index.js CHANGED
@@ -25,6 +25,7 @@ const graph_js_1 = require("./graph.js");
25
25
  const index_js_7 = require("../skill-packet/index.js");
26
26
  const librarian_js_1 = require("./librarian.js");
27
27
  const medic_js_1 = require("./medic.js");
28
+ const qc_js_1 = require("./qc.js");
28
29
  const welfare_js_1 = require("../map/welfare.js");
29
30
  const adopt_command_js_1 = require("./adopt-command.js");
30
31
  const upgrade_command_js_1 = require("./upgrade-command.js");
@@ -127,6 +128,9 @@ function createPolarisCommand(options = {}) {
127
128
  program.addCommand((0, medic_js_1.createMedicCommand)({
128
129
  repoRoot,
129
130
  }));
131
+ program.addCommand((0, qc_js_1.createQcCommand)({
132
+ repoRoot,
133
+ }));
130
134
  program.addCommand((0, adopt_command_js_1.createAdoptCommand)({ repoRoot }));
131
135
  program.addCommand((0, upgrade_command_js_1.createUpgradeCommand)({ repoRoot }));
132
136
  program.addCommand((0, autoresearch_js_1.createSolCommand)({ repoRoot }));
package/dist/cli/qc.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createQcCommand = createQcCommand;
4
+ const commander_1 = require("commander");
5
+ const node_path_1 = require("node:path");
6
+ const node_fs_1 = require("node:fs");
7
+ const types_js_1 = require("../qc/types.js");
8
+ const repair_loop_js_1 = require("../qc/repair-loop.js");
9
+ const repair_packets_js_1 = require("../qc/repair-packets.js");
10
+ function createQcCommand(options = {}) {
11
+ const repoRootDefault = options.repoRoot ?? (0, node_path_1.resolve)(process.cwd());
12
+ const qc = new commander_1.Command("qc")
13
+ .description("QC role tools")
14
+ .showHelpAfterError()
15
+ .showSuggestionAfterError();
16
+ qc
17
+ .command("resolve")
18
+ .description("Record a formal operator resolution for a QC repair-loop terminal state")
19
+ .option("-r, --repo-root <path>", "Repository root", repoRootDefault)
20
+ .requiredOption("--cluster-id <id>", "Cluster ID to resolve")
21
+ .requiredOption("--outcome <outcome>", "Resolved outcome (pass or no-repairable)")
22
+ .requiredOption("--reason <text>", "Reason for the resolution")
23
+ .option("--findings <ids>", "Comma-separated finding IDs covered by this resolution (defaults to all in the round manifest)")
24
+ .action((cmdOptions) => {
25
+ try {
26
+ resolveQcRepairLoop({
27
+ repoRoot: cmdOptions.repoRoot,
28
+ clusterId: cmdOptions.clusterId,
29
+ outcome: cmdOptions.outcome,
30
+ reason: cmdOptions.reason,
31
+ findings: cmdOptions.findings,
32
+ });
33
+ }
34
+ catch (err) {
35
+ process.stderr.write(`qc resolve error: ${err instanceof Error ? err.message : String(err)}\n`);
36
+ process.exit(1);
37
+ }
38
+ });
39
+ return qc;
40
+ }
41
+ function resolveQcRepairLoop(options) {
42
+ const { repoRoot, clusterId, outcome, reason, findings } = options;
43
+ const allowedOutcomes = new Set(types_js_1.QC_RESOLUTION_OUTCOMES);
44
+ if (!allowedOutcomes.has(outcome)) {
45
+ throw new Error(`Invalid outcome "${outcome}". Must be one of: ${types_js_1.QC_RESOLUTION_OUTCOMES.join(", ")}.`);
46
+ }
47
+ const resolvedReason = reason.trim();
48
+ if (resolvedReason === "") {
49
+ throw new Error("A non-empty --reason is required to record a resolution.");
50
+ }
51
+ const round = findCurrentRepairRound(repoRoot, clusterId);
52
+ if (round === null) {
53
+ throw new Error(`No repair-round manifest found for cluster ${clusterId}. Run the QC repair loop first.`);
54
+ }
55
+ const manifest = (0, repair_packets_js_1.readRepairPacketManifest)(clusterId, round, repoRoot);
56
+ if (!manifest) {
57
+ throw new Error(`Repair packet manifest for cluster ${clusterId} round ${round} is missing or invalid.`);
58
+ }
59
+ const explicitFindings = findings
60
+ ? findings
61
+ .split(",")
62
+ .map((id) => id.trim())
63
+ .filter(Boolean)
64
+ : undefined;
65
+ const resolvedFindings = (0, repair_loop_js_1.resolveQcResolutionFindings)(manifest, explicitFindings);
66
+ const resolver = (0, repair_loop_js_1.getResolverIdentity)(repoRoot);
67
+ const artifactPath = (0, repair_loop_js_1.writeQcResolutionArtifact)({
68
+ clusterId,
69
+ round,
70
+ resolver,
71
+ resolvedOutcome: outcome,
72
+ reason: resolvedReason,
73
+ findings: resolvedFindings,
74
+ repoRoot,
75
+ });
76
+ process.stdout.write(`Created resolution artifact: ${artifactPath}\n` +
77
+ `Resolved outcome: ${outcome}\n` +
78
+ `Resolved findings: ${resolvedFindings.join(", ") || "none"}\n`);
79
+ }
80
+ function findCurrentRepairRound(repoRoot, clusterId) {
81
+ const roundsDir = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds");
82
+ if (!(0, node_fs_1.existsSync)(roundsDir)) {
83
+ return null;
84
+ }
85
+ const entries = (0, node_fs_1.readdirSync)(roundsDir, { withFileTypes: true });
86
+ const rounds = entries
87
+ .filter((e) => e.isDirectory() && /^\d+$/.test(e.name))
88
+ .map((e) => Number(e.name))
89
+ .sort((a, b) => b - a);
90
+ for (const round of rounds) {
91
+ const manifestPath = (0, node_path_1.join)(roundsDir, String(round), "repair-packets.json");
92
+ if ((0, node_fs_1.existsSync)(manifestPath)) {
93
+ return round;
94
+ }
95
+ }
96
+ return null;
97
+ }
@@ -364,13 +364,23 @@ function validateConfig(config) {
364
364
  result.valid = false;
365
365
  result.errors.push(`execution.providerPolicy.${roleName}.providers must be an array of strings`);
366
366
  }
367
- else if (providerKeys) {
368
- for (const providerName of rolePolicy.providers) {
369
- if (!providerKeys.has(providerName)) {
370
- result.valid = false;
371
- result.errors.push(`execution.providerPolicy.${roleName}.providers contains unknown provider: ${providerName}`);
367
+ else {
368
+ if (providerKeys) {
369
+ for (const providerName of rolePolicy.providers) {
370
+ if (!providerKeys.has(providerName)) {
371
+ result.valid = false;
372
+ result.errors.push(`execution.providerPolicy.${roleName}.providers contains unknown provider: ${providerName}`);
373
+ }
372
374
  }
373
375
  }
376
+ const routerPolicyObj = isPlainObject(config.execution.routerPolicy)
377
+ ? config.execution.routerPolicy
378
+ : undefined;
379
+ const providerRegistry = routerPolicyObj?.providerRegistry;
380
+ const hasRegistry = isPlainObject(providerRegistry) && Object.keys(providerRegistry).length > 0;
381
+ if (!hasRegistry && rolePolicy.providers.length > 1) {
382
+ result.warnings.push(`execution.providerPolicy.${roleName}.providers lists multiple providers but execution.routerPolicy.providerRegistry is missing or empty; dispatch will use compatibility mode and only the selected provider will appear in providers_tried`);
383
+ }
374
384
  }
375
385
  if ("allowNativeSubagent" in rolePolicy && rolePolicy.allowNativeSubagent !== undefined && !isBoolean(rolePolicy.allowNativeSubagent)) {
376
386
  result.valid = false;
@@ -993,7 +1003,7 @@ function validateConfig(config) {
993
1003
  result.errors.push(`qc.providers.${providerName}.failurePolicy must be a plain object`);
994
1004
  }
995
1005
  else {
996
- for (const key of ["timeout", "parseFailure", "allProvidersFailed"]) {
1006
+ for (const key of ["timeout", "parseFailure", "rateLimited", "allProvidersFailed"]) {
997
1007
  const value = providerConfig.failurePolicy[key];
998
1008
  if (value !== undefined && (!isString(value) || !SUPPORTED_QC_FAILURE_ACTIONS.includes(value))) {
999
1009
  result.valid = false;
@@ -20,6 +20,8 @@ const PROMOTED_MAP_PREFIX = ".polaris/map/";
20
20
  const WORKSPACE_SCRATCH_PREFIX = ".taskchain_artifacts/";
21
21
  const LEGACY_RUN_ARTIFACTS = new Set([
22
22
  ".polaris/runs/mutation-queue.json",
23
+ ".polaris/runs/current-state.json",
24
+ ".polaris/runs/run-report.md",
23
25
  ".polaris/runs/current-state.pre-pol-198.json",
24
26
  ]);
25
27
  function normalizeArtifactPath(filePath) {
@@ -143,9 +145,11 @@ function getArtifactPromotionPolicy(activeClusterId) {
143
145
  blocked: [
144
146
  `${WORKSPACE_SCRATCH_PREFIX}**`,
145
147
  "*.bak",
146
- ".polaris/runs/mutation-queue.json",
147
- ".polaris/runs/current-state.pre-pol-198.json",
148
+ ...LEGACY_RUN_ARTIFACTS,
148
149
  ".polaris/runs/evo-run-archive/**",
150
+ ".polaris/bootstrap/**",
151
+ ".polaris/session-type",
152
+ ".polaris/tmp/**",
149
153
  ".polaris/clusters/<other-cluster>/**",
150
154
  ],
151
155
  };
@@ -163,10 +167,14 @@ function getGitignorePatterns() {
163
167
  ".taskchain_artifacts/**",
164
168
  "*.bak",
165
169
  ".polaris/runs/mutation-queue.json",
170
+ ".polaris/runs/current-state.json",
171
+ ".polaris/runs/run-report.md",
166
172
  ".polaris/runs/current-state.pre-pol-198.json",
173
+ ".polaris/runs/*/",
167
174
  ".polaris/runs/evo-run-archive/**",
168
175
  ".polaris/bootstrap/**",
169
176
  ".polaris/session-type",
177
+ ".polaris/tmp/**",
170
178
  "# Cognition staging — ephemeral, not committed",
171
179
  ".polaris/cognition/pending/**",
172
180
  ];
@@ -200,7 +208,8 @@ function isPathBlockedFromStaging(filePath) {
200
208
  if (relativePath === ".polaris/runs/mutation-queue.json" ||
201
209
  relativePath === ".polaris/runs/current-state.pre-pol-198.json" ||
202
210
  relativePath.startsWith(".polaris/bootstrap/") ||
203
- relativePath === ".polaris/session-type") {
211
+ relativePath === ".polaris/session-type" ||
212
+ relativePath.startsWith(".polaris/tmp/")) {
204
213
  return true;
205
214
  }
206
215
  // Block cognition pending staging
@@ -246,7 +246,7 @@ function checkLibrarianGate(repoRoot, clusterId) {
246
246
  }
247
247
  function resolveQcTelemetryFile(state, repoRoot) {
248
248
  const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
249
- return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
249
+ return (0, node_path_1.resolve)(repoRoot, artifactDir, "runs", state.run_id, "telemetry.jsonl");
250
250
  }
251
251
  // ── QC repair-loop terminal state gate ────────────────────────────────────────
252
252
  /**
@@ -259,8 +259,12 @@ const TRUSTED_QC_REPAIR_OUTCOMES = new Set(["pass", "qc-disabled", "no-repairabl
259
259
  * Validate QC repair-loop terminal state when QC is enabled and repair routing
260
260
  * is active. Returns null when finalize may proceed; returns a human-readable
261
261
  * blocker string otherwise.
262
+ *
263
+ * A valid operator resolution artifact for the current repair round is accepted
264
+ * as equivalent to a trusted terminal_outcome; finalize does not mutate the
265
+ * loop state.
262
266
  */
263
- function validateQcRepairLoopGate(state, config) {
267
+ function validateQcRepairLoopGate(state, config, repoRoot) {
264
268
  // Gate only applies when QC is enabled
265
269
  if (!config.enabled)
266
270
  return null;
@@ -283,9 +287,24 @@ function validateQcRepairLoopGate(state, config) {
283
287
  }
284
288
  if (TRUSTED_QC_REPAIR_OUTCOMES.has(outcome))
285
289
  return null;
290
+ // A valid operator resolution artifact for the current round overrides the
291
+ // untrusted terminal_outcome without mutating state.qc_repair_loop.
292
+ if (repoRoot &&
293
+ repairLoop.current_round &&
294
+ repairLoop.current_round > 0) {
295
+ const resolution = (0, index_js_2.readQcResolutionArtifact)(state.cluster_id, repairLoop.current_round, repoRoot);
296
+ if (resolution &&
297
+ TRUSTED_QC_REPAIR_OUTCOMES.has(resolution.resolvedOutcome)) {
298
+ return null;
299
+ }
300
+ }
301
+ const resolutionHint = outcome === "operator-review" || outcome === "medic-referral"
302
+ ? ` Resolve with: polaris qc resolve --cluster-id ${state.cluster_id} --outcome <pass|no-repairable> --reason "<text>" [--findings <id1,id2,...>].`
303
+ : "";
286
304
  return (`QC repair loop terminated with untrusted outcome: "${outcome}". ` +
287
305
  `Only ${Array.from(TRUSTED_QC_REPAIR_OUTCOMES).join(", ")} outcomes allow finalize to proceed. ` +
288
- `Resolve the repair loop before re-running finalize.`);
306
+ `A valid resolution artifact for the current round is also accepted.` +
307
+ resolutionHint);
289
308
  }
290
309
  // ── Authoritative completed-child state cross-check ───────────────────────────
291
310
  function warnOnMissingQcArtifacts(clusterState, repoRoot) {
@@ -546,7 +565,7 @@ async function runCompletedClusterQcWithRepair(options) {
546
565
  repoRoot,
547
566
  });
548
567
  }
549
- const repairLoopBlocker = validateQcRepairLoopGate(nextState, config.qc);
568
+ const repairLoopBlocker = validateQcRepairLoopGate(nextState, config.qc, repoRoot);
550
569
  if (repairLoopBlocker) {
551
570
  process.stderr.write(`${stepLabel} QC completed-cluster blocked finalize: ${qcResult.summary}\n` +
552
571
  `${repairLoopBlocker}\n`);
@@ -609,7 +628,7 @@ async function runFinalize(options) {
609
628
  throw new Error(`Canon check cannot proceed: git diff failed: ${msg}`);
610
629
  }
611
630
  const artifactDirForCheck = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
612
- const telemetryFileForCheck = (0, node_path_1.join)(artifactDirForCheck, "runs", state.run_id, "telemetry.jsonl");
631
+ const telemetryFileForCheck = (0, node_path_1.resolve)(repoRoot, artifactDirForCheck, "runs", state.run_id, "telemetry.jsonl");
613
632
  const canonResult = (0, canon_check_js_1.runCanonCheck)({
614
633
  repoRoot,
615
634
  changedFiles,
@@ -754,7 +773,7 @@ async function runFinalize(options) {
754
773
  // When QC is enabled and repair routing is active, require a trusted
755
774
  // terminal outcome from the QC repair loop before allowing PR creation.
756
775
  {
757
- const repairLoopBlocker = validateQcRepairLoopGate(state, config.qc);
776
+ const repairLoopBlocker = validateQcRepairLoopGate(state, config.qc, repoRoot);
758
777
  if (repairLoopBlocker) {
759
778
  process.stderr.write(`finalize aborted: QC repair-loop gate failed.\n${repairLoopBlocker}\n`);
760
779
  process.exit(1);
@@ -848,6 +867,13 @@ async function runFinalize(options) {
848
867
  console.log("[8/14] Committing durable Polaris state + map..."); // Step count updated
849
868
  const resolvedStateFile = (0, node_path_1.resolve)(stateFile);
850
869
  (0, _06_commit_js_1.stepCommit)(repoRoot, state, resolvedStateFile, reportPath);
870
+ // The run's local state file is the authoritative place for post-PR state
871
+ // (pr_url, status: complete). When the state file passed to finalize is the
872
+ // tracked cluster snapshot, redirect post-PR writes to the untracked per-run
873
+ // archive so the cluster snapshot is never written after PR creation.
874
+ const runLocalStateFile = isClusterStateSnapshotFile(resolvedStateFile)
875
+ ? (0, node_path_1.resolve)(repoRoot, ".polaris", "runs", state.run_id, "current-state.json")
876
+ : resolvedStateFile;
851
877
  if (skipDelivery) {
852
878
  console.log("[9–14/14] Delivery skipped (--skip-delivery).");
853
879
  console.log("polaris finalize steps 1–8 complete.");
@@ -871,24 +897,13 @@ async function runFinalize(options) {
871
897
  prUrl,
872
898
  stepLabel: "[10.5/14]",
873
899
  });
874
- // Step 11: Write PR URL to current-state.json
900
+ // Step 11: Write PR URL to the run's local state file
875
901
  console.log("[11/14] Writing PR URL to state...");
876
- state = (0, _09_update_state_js_1.stepUpdateState)(resolvedStateFile, state, prUrl);
877
- // Promote the authoritative run state into the cluster snapshot so that
878
- // `.polaris/clusters/<id>/state.json` preserves completed children, the
879
- // dispatch boundary, QC repair-loop terminal state, and the PR URL.
880
- try {
881
- const clusterStateSnapshotPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "state.json");
882
- (0, checkpoint_js_1.writeStateAtomic)(clusterStateSnapshotPath, state);
883
- }
884
- catch (snapshotError) {
885
- process.stderr.write(`[11/14] WARNING: Failed to write cluster state snapshot: ${snapshotError instanceof Error ? snapshotError.message : String(snapshotError)}\n`);
886
- process.stderr.write(`Delivery will proceed, but cluster state snapshot at .polaris/clusters/${state.cluster_id}/state.json may be incomplete.\n`);
887
- }
902
+ state = (0, _09_update_state_js_1.stepUpdateState)(runLocalStateFile, state, prUrl);
888
903
  // Step 12: Append JSONL events
889
904
  console.log("[12/14] Appending JSONL events...");
890
905
  const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
891
- const telemetryFile = (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
906
+ const telemetryFile = (0, node_path_1.resolve)(repoRoot, artifactDir, "runs", state.run_id, "telemetry.jsonl");
892
907
  (0, _10_append_jsonl_js_1.stepAppendJsonl)(telemetryFile, state, prUrl);
893
908
  // Step 13: Update Linear parent issue
894
909
  console.log("[13/14] Updating Linear...");
@@ -897,7 +912,7 @@ async function runFinalize(options) {
897
912
  await (0, _11_update_linear_js_1.stepUpdateLinear)(state, branch, prUrl, true, linearEnabled, state.cluster_id, lifecyclePolicy, authChildResult.authoritativeCount);
898
913
  // Step 14: Archive run snapshot
899
914
  console.log("[14/14] Archiving run snapshot...");
900
- (0, _12_archive_js_1.stepArchive)(repoRoot, state, resolvedStateFile, reportPath);
915
+ (0, _12_archive_js_1.stepArchive)(repoRoot, state, runLocalStateFile, reportPath);
901
916
  console.log("polaris finalize complete.");
902
917
  }
903
918
  function failMissingSubcommand(command, commandName) {