@lsctech/polaris 0.5.5 → 0.5.7

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,19 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAutoresearchCommand = createAutoresearchCommand;
3
+ exports.createAutoresearchCommand = void 0;
4
+ exports.createSolCommand = createSolCommand;
4
5
  const node_path_1 = require("node:path");
5
6
  const commander_1 = require("commander");
6
7
  const dev_gate_js_1 = require("../autoresearch/dev-gate.js");
7
8
  const score_js_1 = require("../autoresearch/score.js");
8
9
  const proposal_js_1 = require("../autoresearch/proposal.js");
9
10
  const routing_js_1 = require("../autoresearch/routing.js");
10
- function createAutoresearchCommand(options) {
11
+ const sol_evidence_loader_js_1 = require("../autoresearch/sol-evidence-loader.js");
12
+ const sol_scorer_js_1 = require("../autoresearch/sol-scorer.js");
13
+ const sol_history_js_1 = require("../autoresearch/sol-history.js");
14
+ const sol_report_js_1 = require("../autoresearch/sol-report.js");
15
+ const sol_recommendations_js_1 = require("../autoresearch/sol-recommendations.js");
16
+ function createSolCommand(options) {
11
17
  const repoRoot = options.repoRoot;
12
- const autoresearch = new commander_1.Command("autoresearch")
13
- .description("Autoresearch tools (dev-gated — Polaris development context only)")
18
+ const sol = new commander_1.Command("sol")
19
+ .alias("autoresearch")
20
+ .description("Self-Optimization Loop (SOL) tools — autoresearch compatibility alias (dev-gated — Polaris development context only)")
14
21
  .showHelpAfterError()
15
22
  .showSuggestionAfterError();
16
- autoresearch
23
+ sol
17
24
  .command("score <run-id>")
18
25
  .description("Score a completed Polaris run against the binary gate scorecard and output a diagnosis report")
19
26
  .option("-r, --repo-root <path>", "Repository root", repoRoot)
@@ -35,11 +42,39 @@ function createAutoresearchCommand(options) {
35
42
  process.stdout.write(`${output}\n`);
36
43
  }
37
44
  catch (err) {
38
- process.stderr.write(`autoresearch score error: ${err instanceof Error ? err.message : String(err)}\n`);
45
+ process.stderr.write(`sol score error: ${err instanceof Error ? err.message : String(err)}\n`);
39
46
  process.exit(1);
40
47
  }
41
48
  });
42
- autoresearch
49
+ sol
50
+ .command("score-report <run-id>")
51
+ .description("Compute a SOL score report with diagnostic sub-scores for Foreman and Workers")
52
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
53
+ .option("--json", "Output raw JSON (default: pretty-printed)")
54
+ .action((runId, cmdOptions) => {
55
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
56
+ try {
57
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
58
+ }
59
+ catch (err) {
60
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
61
+ process.exit(1);
62
+ }
63
+ try {
64
+ const artifacts = (0, score_js_1.loadRunArtifacts)(root, runId);
65
+ const evidence = (0, sol_evidence_loader_js_1.aggregateSolEvidence)(artifacts);
66
+ const report = (0, sol_scorer_js_1.computeSolScoreReport)(evidence);
67
+ const output = cmdOptions.json
68
+ ? JSON.stringify(report)
69
+ : JSON.stringify(report, null, 2);
70
+ process.stdout.write(`${output}\n`);
71
+ }
72
+ catch (err) {
73
+ process.stderr.write(`sol score-report error: ${err instanceof Error ? err.message : String(err)}\n`);
74
+ process.exit(1);
75
+ }
76
+ });
77
+ sol
43
78
  .command("propose <diagnosis-file>")
44
79
  .description("File Linear improvement proposals from a diagnosis report (dev-gated — never auto-applied)")
45
80
  .option("-r, --repo-root <path>", "Repository root", repoRoot)
@@ -60,7 +95,7 @@ function createAutoresearchCommand(options) {
60
95
  report = (0, proposal_js_1.loadDiagnosisReport)((0, node_path_1.resolve)(diagnosisFile));
61
96
  }
62
97
  catch (err) {
63
- process.stderr.write(`autoresearch propose: invalid diagnosis file: ${err instanceof Error ? err.message : String(err)}\n`);
98
+ process.stderr.write(`sol propose: invalid diagnosis file: ${err instanceof Error ? err.message : String(err)}\n`);
64
99
  process.exit(1);
65
100
  }
66
101
  const proposals = (0, proposal_js_1.buildProposals)(report);
@@ -70,7 +105,7 @@ function createAutoresearchCommand(options) {
70
105
  }
71
106
  const apiKey = process.env["LINEAR_API_KEY"];
72
107
  if (!apiKey && !cmdOptions.dryRun) {
73
- process.stderr.write("autoresearch propose: LINEAR_API_KEY environment variable is required.\n");
108
+ process.stderr.write("sol propose: LINEAR_API_KEY environment variable is required.\n");
74
109
  process.exit(1);
75
110
  }
76
111
  try {
@@ -88,9 +123,148 @@ function createAutoresearchCommand(options) {
88
123
  }
89
124
  }
90
125
  catch (err) {
91
- process.stderr.write(`autoresearch propose error: ${err instanceof Error ? err.message : String(err)}\n`);
126
+ process.stderr.write(`sol propose error: ${err instanceof Error ? err.message : String(err)}\n`);
127
+ process.exit(1);
128
+ }
129
+ });
130
+ sol
131
+ .command("recommend")
132
+ .description("Generate SOL routing recommendations from historical snapshots (advisory by default; filing is Polaris-dev only)")
133
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
134
+ .option("--history-path <path>", "Custom history directory (relative to repo root)")
135
+ .option("--group-by <dims>", "Comma-separated grouping dimensions (provider,model,role,route,task_type,repo,risk,worker_id,run_id,time_window)", "provider,model,role,route,task_type")
136
+ .option("--threshold <n>", "Mean composite threshold below which a recommendation is triggered", "0.7")
137
+ .option("--min-samples <n>", "Minimum snapshots per group before recommending", "2")
138
+ .option("--file", "File review-gated tracker proposals (requires Polaris dev context)")
139
+ .option("--team <team>", "Tracker team name or ID", "Polaris")
140
+ .option("--dry-run", "Show tracker mutations without writing to the tracker")
141
+ .option("--json", "Output raw JSON (default: human-readable)")
142
+ .action(async (cmdOptions) => {
143
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
144
+ let snapshots;
145
+ try {
146
+ snapshots = (0, sol_history_js_1.loadSnapshots)(root, cmdOptions.historyPath);
147
+ }
148
+ catch (err) {
149
+ process.stderr.write(`sol recommend error: ${err instanceof Error ? err.message : String(err)}\n`);
150
+ process.exit(1);
151
+ }
152
+ const groupBy = cmdOptions.groupBy.split(",").filter(Boolean);
153
+ const threshold = parseFloat(cmdOptions.threshold) || 0.7;
154
+ const minSamples = parseInt(cmdOptions.minSamples, 10) || 2;
155
+ const report = (0, sol_recommendations_js_1.generateRecommendations)(snapshots, { groupBy, threshold, minSamples });
156
+ // Advisory mode: never touches the tracker or filesystem.
157
+ if (!cmdOptions.file) {
158
+ if (cmdOptions.json) {
159
+ process.stdout.write(JSON.stringify(report) + "\n");
160
+ }
161
+ else {
162
+ process.stdout.write((0, sol_recommendations_js_1.formatRecommendationsCli)(report));
163
+ }
164
+ process.exit(0);
165
+ }
166
+ // Filing mode: Polaris dev context only.
167
+ try {
168
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
169
+ }
170
+ catch (err) {
171
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
172
+ process.exit(1);
173
+ }
174
+ if (report.recommendations.length === 0) {
175
+ process.stdout.write("No underperforming groups — nothing to file.\n");
176
+ process.exit(0);
177
+ }
178
+ const apiKey = process.env["LINEAR_API_KEY"];
179
+ if (!apiKey && !cmdOptions.dryRun) {
180
+ process.stderr.write("sol recommend: LINEAR_API_KEY environment variable is required.\n");
181
+ process.exit(1);
182
+ }
183
+ const proposals = (0, sol_recommendations_js_1.recommendationsToProposals)(report.recommendations);
184
+ try {
185
+ const result = await (0, routing_js_1.routeProposals)(proposals, {
186
+ apiKey: apiKey ?? "",
187
+ teamKey: cmdOptions.team,
188
+ dryRun: cmdOptions.dryRun,
189
+ });
190
+ const output = cmdOptions.json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
191
+ process.stdout.write(`${output}\n`);
192
+ if (result.total_errors > 0) {
193
+ process.exit(1);
194
+ }
195
+ }
196
+ catch (err) {
197
+ process.stderr.write(`sol recommend error: ${err instanceof Error ? err.message : String(err)}\n`);
198
+ process.exit(1);
199
+ }
200
+ });
201
+ // ── history subcommand group ──
202
+ const history = new commander_1.Command("history")
203
+ .description("SOL historical performance storage and reports");
204
+ history
205
+ .command("save <run-id>")
206
+ .description("Score a run and persist the SOL score snapshot to local history")
207
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
208
+ .option("--history-path <path>", "Custom history directory (relative to repo root)")
209
+ .action((runId, cmdOptions) => {
210
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
211
+ try {
212
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
213
+ }
214
+ catch (err) {
215
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
216
+ process.exit(1);
217
+ }
218
+ try {
219
+ const artifacts = (0, score_js_1.loadRunArtifacts)(root, runId);
220
+ const evidence = (0, sol_evidence_loader_js_1.aggregateSolEvidence)(artifacts);
221
+ const report = (0, sol_scorer_js_1.computeSolScoreReport)(evidence);
222
+ const workerIds = evidence.children.map((c) => c.worker_id);
223
+ const snapshot = (0, sol_history_js_1.buildSnapshot)(report, evidence.grouping_keys, workerIds);
224
+ const path = (0, sol_history_js_1.appendSnapshot)(root, snapshot, cmdOptions.historyPath);
225
+ process.stdout.write(`Snapshot saved to ${path}\n`);
226
+ }
227
+ catch (err) {
228
+ process.stderr.write(`sol history save error: ${err instanceof Error ? err.message : String(err)}\n`);
229
+ process.exit(1);
230
+ }
231
+ });
232
+ history
233
+ .command("report")
234
+ .description("Generate a report from SOL historical snapshots")
235
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
236
+ .option("--history-path <path>", "Custom history directory (relative to repo root)")
237
+ .option("--group-by <dims>", "Comma-separated grouping dimensions (repo,route,task_type,role,risk,provider,model,worker_id,run_id,time_window)", "run_id")
238
+ .option("--window-days <days>", "Time window size in days for time_window grouping", "7")
239
+ .option("--json", "Output raw JSON (default: human-readable table)")
240
+ .action((cmdOptions) => {
241
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
242
+ try {
243
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
244
+ }
245
+ catch (err) {
246
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
247
+ process.exit(1);
248
+ }
249
+ try {
250
+ const snapshots = (0, sol_history_js_1.loadSnapshots)(root, cmdOptions.historyPath);
251
+ const groupBy = cmdOptions.groupBy.split(",").filter(Boolean);
252
+ const windowDays = parseInt(cmdOptions.windowDays, 10) || 7;
253
+ const report = (0, sol_report_js_1.generateReport)(snapshots, { groupBy, windowDays });
254
+ if (cmdOptions.json) {
255
+ process.stdout.write(JSON.stringify(report) + "\n");
256
+ }
257
+ else {
258
+ process.stdout.write((0, sol_report_js_1.formatReportCli)(report));
259
+ }
260
+ }
261
+ catch (err) {
262
+ process.stderr.write(`sol history report error: ${err instanceof Error ? err.message : String(err)}\n`);
92
263
  process.exit(1);
93
264
  }
94
265
  });
95
- return autoresearch;
266
+ sol.addCommand(history);
267
+ return sol;
96
268
  }
269
+ /** @deprecated Use {@link createSolCommand}. */
270
+ exports.createAutoresearchCommand = createSolCommand;
package/dist/cli/index.js CHANGED
@@ -129,7 +129,7 @@ function createPolarisCommand(options = {}) {
129
129
  }));
130
130
  program.addCommand((0, adopt_command_js_1.createAdoptCommand)({ repoRoot }));
131
131
  program.addCommand((0, upgrade_command_js_1.createUpgradeCommand)({ repoRoot }));
132
- program.addCommand((0, autoresearch_js_1.createAutoresearchCommand)({ repoRoot }));
132
+ program.addCommand((0, autoresearch_js_1.createSolCommand)({ repoRoot }));
133
133
  program
134
134
  .command("simplicity")
135
135
  .description("View or override the simplicity discipline mode for the active run")
@@ -119,5 +119,6 @@ exports.DEFAULT_CONFIG = {
119
119
  maxRuns: 10,
120
120
  },
121
121
  routes: {},
122
+ maxRepairRounds: 2,
122
123
  },
123
124
  };
@@ -10,6 +10,9 @@ function isNumber(value) {
10
10
  function isPositiveInteger(value) {
11
11
  return Number.isInteger(value) && Number(value) > 0;
12
12
  }
13
+ function isNonNegativeInteger(value) {
14
+ return Number.isInteger(value) && Number(value) >= 0;
15
+ }
13
16
  function isString(value) {
14
17
  return typeof value === "string";
15
18
  }
@@ -91,6 +94,8 @@ const SUPPORTED_QC_PROVIDER_CAPABILITIES = [
91
94
  "auto-fix",
92
95
  "metrics-import",
93
96
  ];
97
+ const SUPPORTED_QC_OUTPUT_FORMATS = ["json", "jsonl", "sarif", "generic"];
98
+ const SUPPORTED_QC_FAILURE_ACTIONS = ["fail", "fallback", "ignore", "block"];
94
99
  const SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
95
100
  function severityIndex(severity) {
96
101
  return SEVERITY_ORDER.indexOf(severity);
@@ -908,6 +913,158 @@ function validateConfig(config) {
908
913
  }
909
914
  }
910
915
  }
916
+ if ("enabled" in providerConfig && providerConfig.enabled !== undefined && !isBoolean(providerConfig.enabled)) {
917
+ result.valid = false;
918
+ result.errors.push(`qc.providers.${providerName}.enabled must be a boolean`);
919
+ }
920
+ if ("execution" in providerConfig && providerConfig.execution !== undefined) {
921
+ if (!isPlainObject(providerConfig.execution)) {
922
+ result.valid = false;
923
+ result.errors.push(`qc.providers.${providerName}.execution must be a plain object`);
924
+ }
925
+ else {
926
+ const execution = providerConfig.execution;
927
+ if ("command" in execution && execution.command !== undefined && !isString(execution.command)) {
928
+ result.valid = false;
929
+ result.errors.push(`qc.providers.${providerName}.execution.command must be a string`);
930
+ }
931
+ if ("args" in execution && execution.args !== undefined && !isStringArray(execution.args)) {
932
+ result.valid = false;
933
+ result.errors.push(`qc.providers.${providerName}.execution.args must be an array of strings`);
934
+ }
935
+ if ("output" in execution && execution.output !== undefined) {
936
+ if (!isPlainObject(execution.output)) {
937
+ result.valid = false;
938
+ result.errors.push(`qc.providers.${providerName}.execution.output must be a plain object`);
939
+ }
940
+ else {
941
+ if ("format" in execution.output &&
942
+ execution.output.format !== undefined &&
943
+ (!isString(execution.output.format) ||
944
+ !SUPPORTED_QC_OUTPUT_FORMATS.includes(execution.output.format))) {
945
+ result.valid = false;
946
+ result.errors.push(`qc.providers.${providerName}.execution.output.format must be one of json, jsonl, sarif, generic`);
947
+ }
948
+ if ("parser" in execution.output &&
949
+ execution.output.parser !== undefined &&
950
+ !isString(execution.output.parser)) {
951
+ result.valid = false;
952
+ result.errors.push(`qc.providers.${providerName}.execution.output.parser must be a string`);
953
+ }
954
+ }
955
+ }
956
+ if ("configPath" in execution &&
957
+ execution.configPath !== undefined &&
958
+ !isString(execution.configPath)) {
959
+ result.valid = false;
960
+ result.errors.push(`qc.providers.${providerName}.execution.configPath must be a string`);
961
+ }
962
+ }
963
+ }
964
+ if ("timeoutMs" in providerConfig && providerConfig.timeoutMs !== undefined && !isPositiveInteger(providerConfig.timeoutMs)) {
965
+ result.valid = false;
966
+ result.errors.push(`qc.providers.${providerName}.timeoutMs must be a positive integer`);
967
+ }
968
+ if ("primary" in providerConfig && providerConfig.primary !== undefined && !isBoolean(providerConfig.primary)) {
969
+ result.valid = false;
970
+ result.errors.push(`qc.providers.${providerName}.primary must be a boolean`);
971
+ }
972
+ if ("fallback" in providerConfig && providerConfig.fallback !== undefined && !isStringArray(providerConfig.fallback)) {
973
+ result.valid = false;
974
+ result.errors.push(`qc.providers.${providerName}.fallback must be an array of strings`);
975
+ }
976
+ if ("failurePolicy" in providerConfig && providerConfig.failurePolicy !== undefined) {
977
+ if (!isPlainObject(providerConfig.failurePolicy)) {
978
+ result.valid = false;
979
+ result.errors.push(`qc.providers.${providerName}.failurePolicy must be a plain object`);
980
+ }
981
+ else {
982
+ for (const key of ["timeout", "parseFailure", "allProvidersFailed"]) {
983
+ const value = providerConfig.failurePolicy[key];
984
+ if (value !== undefined && (!isString(value) || !SUPPORTED_QC_FAILURE_ACTIONS.includes(value))) {
985
+ result.valid = false;
986
+ result.errors.push(`qc.providers.${providerName}.failurePolicy.${key} must be one of fail, fallback, ignore, block`);
987
+ }
988
+ }
989
+ }
990
+ }
991
+ if ("rateLimit" in providerConfig && providerConfig.rateLimit !== undefined) {
992
+ if (!isPlainObject(providerConfig.rateLimit)) {
993
+ result.valid = false;
994
+ result.errors.push(`qc.providers.${providerName}.rateLimit must be a plain object`);
995
+ }
996
+ else {
997
+ if ("requestsPerMinute" in providerConfig.rateLimit &&
998
+ providerConfig.rateLimit.requestsPerMinute !== undefined &&
999
+ !isPositiveInteger(providerConfig.rateLimit.requestsPerMinute)) {
1000
+ result.valid = false;
1001
+ result.errors.push(`qc.providers.${providerName}.rateLimit.requestsPerMinute must be a positive integer`);
1002
+ }
1003
+ if ("maxConcurrent" in providerConfig.rateLimit &&
1004
+ providerConfig.rateLimit.maxConcurrent !== undefined &&
1005
+ !isPositiveInteger(providerConfig.rateLimit.maxConcurrent)) {
1006
+ result.valid = false;
1007
+ result.errors.push(`qc.providers.${providerName}.rateLimit.maxConcurrent must be a positive integer`);
1008
+ }
1009
+ }
1010
+ }
1011
+ if ("retry" in providerConfig && providerConfig.retry !== undefined) {
1012
+ if (!isPlainObject(providerConfig.retry)) {
1013
+ result.valid = false;
1014
+ result.errors.push(`qc.providers.${providerName}.retry must be a plain object`);
1015
+ }
1016
+ else {
1017
+ if ("maxRetries" in providerConfig.retry &&
1018
+ providerConfig.retry.maxRetries !== undefined &&
1019
+ !isNonNegativeInteger(providerConfig.retry.maxRetries)) {
1020
+ result.valid = false;
1021
+ result.errors.push(`qc.providers.${providerName}.retry.maxRetries must be a non-negative integer`);
1022
+ }
1023
+ if ("backoffMs" in providerConfig.retry &&
1024
+ providerConfig.retry.backoffMs !== undefined &&
1025
+ !isNonNegativeInteger(providerConfig.retry.backoffMs)) {
1026
+ result.valid = false;
1027
+ result.errors.push(`qc.providers.${providerName}.retry.backoffMs must be a non-negative integer`);
1028
+ }
1029
+ }
1030
+ }
1031
+ if ("artifactPolicy" in providerConfig && providerConfig.artifactPolicy !== undefined) {
1032
+ if (!isPlainObject(providerConfig.artifactPolicy)) {
1033
+ result.valid = false;
1034
+ result.errors.push(`qc.providers.${providerName}.artifactPolicy must be a plain object`);
1035
+ }
1036
+ else {
1037
+ if ("retainRawOutput" in providerConfig.artifactPolicy &&
1038
+ providerConfig.artifactPolicy.retainRawOutput !== undefined &&
1039
+ !isBoolean(providerConfig.artifactPolicy.retainRawOutput)) {
1040
+ result.valid = false;
1041
+ result.errors.push(`qc.providers.${providerName}.artifactPolicy.retainRawOutput must be a boolean`);
1042
+ }
1043
+ if ("outputDir" in providerConfig.artifactPolicy &&
1044
+ providerConfig.artifactPolicy.outputDir !== undefined &&
1045
+ !isString(providerConfig.artifactPolicy.outputDir)) {
1046
+ result.valid = false;
1047
+ result.errors.push(`qc.providers.${providerName}.artifactPolicy.outputDir must be a string`);
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+ // Validate fallback references after all provider keys are known.
1053
+ if (providers) {
1054
+ const providerKeys = new Set(Object.keys(config.qc.providers));
1055
+ for (const [providerName, providerConfig] of Object.entries(config.qc.providers)) {
1056
+ if (!isPlainObject(providerConfig))
1057
+ continue;
1058
+ const fallback = providerConfig.fallback;
1059
+ if (Array.isArray(fallback)) {
1060
+ for (const fallbackName of fallback) {
1061
+ if (isString(fallbackName) && !providerKeys.has(fallbackName)) {
1062
+ result.valid = false;
1063
+ result.errors.push(`qc.providers.${providerName}.fallback contains unknown provider: ${fallbackName}`);
1064
+ }
1065
+ }
1066
+ }
1067
+ }
911
1068
  }
912
1069
  }
913
1070
  }
@@ -185,7 +185,7 @@ function resolveQcTelemetryFile(state, repoRoot) {
185
185
  }
186
186
  async function runQcGate(options) {
187
187
  const { config, state, repoRoot, branch, trigger, prUrl, stepLabel } = options;
188
- const registry = (0, index_js_2.createDefaultQcRegistry)();
188
+ const registry = (0, index_js_2.createQcRegistry)(config);
189
189
  const result = await (0, index_js_2.runQcAtTrigger)({
190
190
  config,
191
191
  registry,
@@ -37,12 +37,15 @@ const node_child_process_1 = require("node:child_process");
37
37
  const artifact_policy_js_1 = require("../finalize/artifact-policy.js");
38
38
  const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
39
39
  const triggers_js_1 = require("../qc/triggers.js");
40
+ const index_js_1 = require("../qc/index.js");
41
+ const repair_loop_js_1 = require("../qc/repair-loop.js");
42
+ const worker_packet_js_2 = require("./worker-packet.js");
40
43
  const run_bootstrap_js_1 = require("./run-bootstrap.js");
41
44
  const ledger_js_1 = require("./ledger.js");
42
45
  const dispatch_js_1 = require("./dispatch.js");
43
- const index_js_1 = require("./router/index.js");
46
+ const index_js_2 = require("./router/index.js");
44
47
  const child_selector_js_1 = require("../runtime/scheduling/child-selector.js");
45
- const index_js_2 = require("../tracker/index.js");
48
+ const index_js_3 = require("../tracker/index.js");
46
49
  const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
47
50
  const local_graph_js_1 = require("../tracker/local-graph.js");
48
51
  const CLAIM_TTL_MS = 30 * 60 * 1000;
@@ -813,7 +816,7 @@ async function runParentLoop(options) {
813
816
  if (openChildrenNeedingScope.length > 0) {
814
817
  process.stderr.write(`[polaris] ${openChildrenNeedingScope.length} children missing scope — attempting tracker sync-in...\n`);
815
818
  try {
816
- await (0, index_js_2.loadTrackerGraph)(config, state.cluster_id);
819
+ await (0, index_js_3.loadTrackerGraph)(config, state.cluster_id);
817
820
  process.stderr.write(`[polaris] sync-in complete.\n`);
818
821
  }
819
822
  catch (syncErr) {
@@ -844,7 +847,7 @@ async function runParentLoop(options) {
844
847
  max_concurrent: maxConcurrentWorkers,
845
848
  claim_ttl_ms: CLAIM_TTL_MS,
846
849
  get_dependencies: (childId) => localGraph?.getDependencies(childId) ?? [],
847
- decide_route: ({ activeSlotsByProvider }) => (0, index_js_1.decideWorkerRoute)({
850
+ decide_route: ({ activeSlotsByProvider }) => (0, index_js_2.decideWorkerRoute)({
848
851
  role: "worker",
849
852
  taskType: "impl",
850
853
  adapter: adapterName,
@@ -884,6 +887,154 @@ async function runParentLoop(options) {
884
887
  };
885
888
  }
886
889
  const autoFinalizeRequested = orchestrationMode === "auto" && config.orchestration?.auto_finalize === true;
890
+ // ── QC repair loop (post-completion gate) ─────────────────────────────
891
+ // When QC is enabled, run the completed-cluster QC trigger and, if
892
+ // findings are produced, run the bounded repair loop before halting.
893
+ // The repair loop is Foreman-owned: it dispatches repair workers via the
894
+ // same adapter and NEVER implements repairs inline.
895
+ if (!dryRun && config.qc?.enabled) {
896
+ const qcRegistry = (0, index_js_1.createQcRegistry)(config.qc);
897
+ let initialQcResult;
898
+ try {
899
+ initialQcResult = await (0, index_js_1.runQcAtTrigger)({
900
+ config: config.qc,
901
+ registry: qcRegistry,
902
+ trigger: "completed-cluster",
903
+ repoRoot,
904
+ runId: state.run_id,
905
+ clusterId: state.cluster_id,
906
+ branch: state.branch ?? getCurrentBranch(repoRoot),
907
+ telemetryFile,
908
+ state,
909
+ });
910
+ }
911
+ catch (err) {
912
+ const msg = err instanceof Error ? err.message : String(err);
913
+ appendTelemetry(telemetryFile, {
914
+ event: "qc-repair-loop-qc-run-error",
915
+ run_id: state.run_id,
916
+ error: msg,
917
+ timestamp: new Date().toISOString(),
918
+ });
919
+ // Non-fatal: proceed to cluster-complete without repair loop.
920
+ initialQcResult = null;
921
+ }
922
+ const hasFindings = initialQcResult !== null &&
923
+ initialQcResult.results.some((r) => r.findings.length > 0 && r.status !== "passed" && r.status !== "skipped");
924
+ if (initialQcResult !== null && hasFindings) {
925
+ const maxRepairRounds = config.qc.maxRepairRounds ?? repair_loop_js_1.DEFAULT_MAX_REPAIR_ROUNDS;
926
+ const priorLoopState = state.qc_repair_loop ?? null;
927
+ // Build the repair worker dispatcher using the existing adapter + provider.
928
+ const repairDispatcher = async (packet, round, manifest) => {
929
+ const repairPacketId = packet.packetId;
930
+ const dispatchId = (0, node_crypto_1.randomUUID)();
931
+ const repairWorkerId = `${state.run_id}:repair-${repairPacketId}:${Date.now()}`;
932
+ const repairResultPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "results", `repair-${repairPacketId}-${dispatchId}.json`);
933
+ const workerPacket = (0, worker_packet_js_2.compileRepairWorkerPacket)({
934
+ runId: state.run_id,
935
+ clusterId: state.cluster_id,
936
+ packetId: repairPacketId,
937
+ branch: state.branch ?? getCurrentBranch(repoRoot),
938
+ stateFile,
939
+ telemetryFile,
940
+ round,
941
+ allowedScope: packet.allowedScope,
942
+ prohibitedScope: packet.prohibitedScope,
943
+ validationCommands: packet.validationCommands,
944
+ rootCauseHint: packet.rootCauseHint,
945
+ resultFile: repairResultPath,
946
+ maxConcurrentWorkers,
947
+ });
948
+ appendTelemetry(telemetryFile, {
949
+ event: "repair-worker-dispatched",
950
+ run_id: state.run_id,
951
+ cluster_id: state.cluster_id,
952
+ packet_id: repairPacketId,
953
+ worker_id: repairWorkerId,
954
+ round,
955
+ timestamp: new Date().toISOString(),
956
+ });
957
+ try {
958
+ const dispatchResult = await adapter.dispatch(workerPacket, { provider: providerName, dryRun });
959
+ const workerSummary = parseWorkerSummary(dispatchResult.summary);
960
+ const success = workerSummary?.status === "done";
961
+ appendTelemetry(telemetryFile, {
962
+ event: "repair-worker-completed",
963
+ run_id: state.run_id,
964
+ packet_id: repairPacketId,
965
+ worker_id: repairWorkerId,
966
+ round,
967
+ status: success ? "success" : "failure",
968
+ timestamp: new Date().toISOString(),
969
+ });
970
+ return {
971
+ packetId: repairPacketId,
972
+ status: success ? "success" : "failure",
973
+ commitSha: workerSummary?.commit,
974
+ errorMessage: success ? undefined : String(workerSummary?.error_message ?? "repair worker failed"),
975
+ };
976
+ }
977
+ catch (err) {
978
+ const msg = err instanceof Error ? err.message : String(err);
979
+ appendTelemetry(telemetryFile, {
980
+ event: "repair-worker-error",
981
+ run_id: state.run_id,
982
+ packet_id: repairPacketId,
983
+ worker_id: repairWorkerId,
984
+ round,
985
+ error: msg,
986
+ timestamp: new Date().toISOString(),
987
+ });
988
+ return {
989
+ packetId: repairPacketId,
990
+ status: "failure",
991
+ errorMessage: msg,
992
+ };
993
+ }
994
+ };
995
+ try {
996
+ const repairLoopResult = await (0, repair_loop_js_1.runQcRepairLoop)({
997
+ clusterId: state.cluster_id,
998
+ runId: state.run_id,
999
+ branch: state.branch ?? getCurrentBranch(repoRoot),
1000
+ repoRoot,
1001
+ telemetryFile,
1002
+ config: config.qc,
1003
+ registry: qcRegistry,
1004
+ initialQcResults: initialQcResult.results,
1005
+ dispatchRepairWorker: repairDispatcher,
1006
+ maxRounds: maxRepairRounds,
1007
+ priorLoopState,
1008
+ onStateUpdate: (loopState) => {
1009
+ // Persist loop state to the state file on each mutation.
1010
+ const stateWithLoop = { ...state, qc_repair_loop: loopState };
1011
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, stateWithLoop);
1012
+ },
1013
+ });
1014
+ state = { ...state, qc_repair_loop: repairLoopResult.loop_state };
1015
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, state);
1016
+ appendTelemetry(telemetryFile, {
1017
+ event: "qc-repair-loop-finished",
1018
+ run_id: state.run_id,
1019
+ outcome: repairLoopResult.outcome,
1020
+ rounds_completed: repairLoopResult.rounds_completed,
1021
+ summary: repairLoopResult.summary,
1022
+ timestamp: new Date().toISOString(),
1023
+ });
1024
+ }
1025
+ catch (err) {
1026
+ const msg = err instanceof Error ? err.message : String(err);
1027
+ appendTelemetry(telemetryFile, {
1028
+ event: "qc-repair-loop-error",
1029
+ run_id: state.run_id,
1030
+ error: msg,
1031
+ timestamp: new Date().toISOString(),
1032
+ });
1033
+ // Non-fatal: proceed to cluster-complete.
1034
+ }
1035
+ }
1036
+ }
1037
+ // ── End QC repair loop ─────────────────────────────────────────────────
887
1038
  // All children completed — write final state and halt
888
1039
  if (!dryRun) {
889
1040
  logStatus(notificationFormat, "COMPLETE");
@@ -1427,7 +1578,7 @@ async function runParentLoop(options) {
1427
1578
  // Errors are logged to telemetry but do not fail the halt.
1428
1579
  let adapter;
1429
1580
  try {
1430
- adapter = (0, index_js_2.loadTrackerAdapter)(config);
1581
+ adapter = (0, index_js_3.loadTrackerAdapter)(config);
1431
1582
  }
1432
1583
  catch (err) {
1433
1584
  const errorMsg = err instanceof Error ? err.message : String(err);