@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.
@@ -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
  // ──────────────────────────────────────────────
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,8 +20,27 @@ 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
  ]);
27
+ function isPromotedRunArchivePath(relativePath) {
28
+ if (!relativePath.startsWith(".polaris/runs/")) {
29
+ return false;
30
+ }
31
+ const rest = relativePath.slice(".polaris/runs/".length);
32
+ if (rest === "" || rest === "evo-run-archive") {
33
+ return false;
34
+ }
35
+ const slashIndex = rest.indexOf("/");
36
+ if (slashIndex === -1) {
37
+ // The path is a directory-style run archive path (e.g. `.polaris/runs/<run-id>`),
38
+ // not a top-level file like `ledger.jsonl`.
39
+ return !rest.includes(".") && !rest.startsWith(".");
40
+ }
41
+ const firstSegment = rest.slice(0, slashIndex);
42
+ return firstSegment !== "evo-run-archive";
43
+ }
25
44
  function normalizeArtifactPath(filePath) {
26
45
  const normalized = node_path_1.default.posix.normalize(filePath.replace(/\\/g, "/"));
27
46
  return normalized.startsWith("./") ? normalized.slice(2) : normalized;
@@ -59,6 +78,9 @@ function classifyArtifactPath(filePath, activeClusterId) {
59
78
  if (relativePath === PROMOTED_RUN_LEDGER) {
60
79
  return "promoted-run-ledger";
61
80
  }
81
+ if (isPromotedRunArchivePath(relativePath)) {
82
+ return "promoted-run-archive";
83
+ }
62
84
  if (relativePath.startsWith(PROMOTED_COGNITION_ARCHIVE_PREFIX)) {
63
85
  return "promoted-cognition-archive";
64
86
  }
@@ -80,6 +102,7 @@ function isPromotedArtifactPath(filePath, activeClusterId) {
80
102
  const classification = classifyArtifactPath(filePath, activeClusterId);
81
103
  return (classification === "promoted-cluster-artifact"
82
104
  || classification === "promoted-run-ledger"
105
+ || classification === "promoted-run-archive"
83
106
  || classification === "promoted-cognition-archive"
84
107
  || classification === "promoted-map-artifact");
85
108
  }
@@ -91,6 +114,8 @@ function explainArtifactPolicy(filePath, activeClusterId) {
91
114
  return "active cluster evidence is eligible for promotion into finalize commits";
92
115
  case "promoted-run-ledger":
93
116
  return "the run ledger is durable audit evidence and stays commit-eligible";
117
+ case "promoted-run-archive":
118
+ return "archived run snapshots under .polaris/runs/<run-id>/ are durable audit evidence and stay commit-eligible";
94
119
  case "promoted-cognition-archive":
95
120
  return "archived cognition reconciliation notes are durable provenance and stay commit-eligible";
96
121
  case "promoted-map-artifact":
@@ -114,6 +139,7 @@ function findArtifactPromotionViolations(stagedPaths, activeClusterId) {
114
139
  if (classification === "non-artifact"
115
140
  || classification === "promoted-cluster-artifact"
116
141
  || classification === "promoted-run-ledger"
142
+ || classification === "promoted-run-archive"
117
143
  || classification === "promoted-cognition-archive"
118
144
  || classification === "promoted-map-artifact") {
119
145
  continue;
@@ -143,9 +169,11 @@ function getArtifactPromotionPolicy(activeClusterId) {
143
169
  blocked: [
144
170
  `${WORKSPACE_SCRATCH_PREFIX}**`,
145
171
  "*.bak",
146
- ".polaris/runs/mutation-queue.json",
147
- ".polaris/runs/current-state.pre-pol-198.json",
172
+ ...LEGACY_RUN_ARTIFACTS,
148
173
  ".polaris/runs/evo-run-archive/**",
174
+ ".polaris/bootstrap/**",
175
+ ".polaris/session-type",
176
+ ".polaris/tmp/**",
149
177
  ".polaris/clusters/<other-cluster>/**",
150
178
  ],
151
179
  };
@@ -163,10 +191,13 @@ function getGitignorePatterns() {
163
191
  ".taskchain_artifacts/**",
164
192
  "*.bak",
165
193
  ".polaris/runs/mutation-queue.json",
194
+ ".polaris/runs/current-state.json",
195
+ ".polaris/runs/run-report.md",
166
196
  ".polaris/runs/current-state.pre-pol-198.json",
167
197
  ".polaris/runs/evo-run-archive/**",
168
198
  ".polaris/bootstrap/**",
169
199
  ".polaris/session-type",
200
+ ".polaris/tmp/**",
170
201
  "# Cognition staging — ephemeral, not committed",
171
202
  ".polaris/cognition/pending/**",
172
203
  ];
@@ -200,7 +231,8 @@ function isPathBlockedFromStaging(filePath) {
200
231
  if (relativePath === ".polaris/runs/mutation-queue.json" ||
201
232
  relativePath === ".polaris/runs/current-state.pre-pol-198.json" ||
202
233
  relativePath.startsWith(".polaris/bootstrap/") ||
203
- relativePath === ".polaris/session-type") {
234
+ relativePath === ".polaris/session-type" ||
235
+ relativePath.startsWith(".polaris/tmp/")) {
204
236
  return true;
205
237
  }
206
238
  // Block cognition pending staging
@@ -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`);
@@ -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);