@lsctech/polaris 0.5.7 → 0.5.9

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 (36) hide show
  1. package/dist/autoresearch/index.js +30 -1
  2. package/dist/autoresearch/sol-evaluation-writer.js +135 -0
  3. package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
  4. package/dist/autoresearch/sol-recommendations.js +130 -0
  5. package/dist/autoresearch/sol-report-renderer.js +282 -0
  6. package/dist/autoresearch/sol-run-health-bridge.js +246 -0
  7. package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
  8. package/dist/cli/autoresearch.js +77 -0
  9. package/dist/cli/medic.js +65 -0
  10. package/dist/cluster-state/store.js +56 -0
  11. package/dist/config/defaults.js +3 -0
  12. package/dist/config/validator.js +141 -0
  13. package/dist/finalize/artifact-policy.js +2 -0
  14. package/dist/finalize/github.js +13 -9
  15. package/dist/finalize/index.js +231 -5
  16. package/dist/finalize/linear.js +8 -4
  17. package/dist/finalize/medic-gate.js +42 -0
  18. package/dist/finalize/steps/08-create-pr.js +2 -2
  19. package/dist/finalize/steps/11-update-linear.js +2 -2
  20. package/dist/loop/parent.js +234 -28
  21. package/dist/loop/worker-packet.js +19 -1
  22. package/dist/medic/run-health-consult.js +381 -0
  23. package/dist/medic/treatment-packets.js +169 -0
  24. package/dist/qc/artifacts.js +27 -0
  25. package/dist/qc/orchestration.js +3 -2
  26. package/dist/qc/providers/coderabbit.js +114 -11
  27. package/dist/qc/repair-loop.js +49 -14
  28. package/dist/qc/runner.js +10 -2
  29. package/dist/qc/schemas.js +1 -0
  30. package/dist/run-health/foreman-symptoms.js +100 -0
  31. package/dist/run-health/index.js +301 -0
  32. package/dist/run-health/qc-escalation.js +155 -0
  33. package/dist/run-health/schema.js +123 -0
  34. package/dist/types/sol-metrics.js +108 -0
  35. package/dist/types/sol-scorecard.js +148 -0
  36. package/package.json +1 -1
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createAutoresearchCommand = void 0;
4
4
  exports.createSolCommand = createSolCommand;
5
5
  const node_path_1 = require("node:path");
6
+ const node_fs_1 = require("node:fs");
6
7
  const commander_1 = require("commander");
7
8
  const dev_gate_js_1 = require("../autoresearch/dev-gate.js");
8
9
  const score_js_1 = require("../autoresearch/score.js");
@@ -12,6 +13,9 @@ const sol_evidence_loader_js_1 = require("../autoresearch/sol-evidence-loader.js
12
13
  const sol_scorer_js_1 = require("../autoresearch/sol-scorer.js");
13
14
  const sol_history_js_1 = require("../autoresearch/sol-history.js");
14
15
  const sol_report_js_1 = require("../autoresearch/sol-report.js");
16
+ const sol_scorecard_calculator_js_1 = require("../autoresearch/sol-scorecard-calculator.js");
17
+ const sol_evaluation_writer_js_1 = require("../autoresearch/sol-evaluation-writer.js");
18
+ const sol_report_renderer_js_1 = require("../autoresearch/sol-report-renderer.js");
15
19
  const sol_recommendations_js_1 = require("../autoresearch/sol-recommendations.js");
16
20
  function createSolCommand(options) {
17
21
  const repoRoot = options.repoRoot;
@@ -74,6 +78,79 @@ function createSolCommand(options) {
74
78
  process.exit(1);
75
79
  }
76
80
  });
81
+ sol
82
+ .command("report <run-id>")
83
+ .description("Generate SOL evaluation artifacts and a human-readable SmartDocs report for a run")
84
+ .option("-r, --repo-root <path>", "Repository root", repoRoot)
85
+ .option("--format <mode>", "Output format: markdown or json", "markdown")
86
+ .option("--json", "Output raw JSON (same as --format json)")
87
+ .option("--no-write", "Skip writing artifact files")
88
+ .action((runId, cmdOptions) => {
89
+ const root = (0, node_path_1.resolve)(cmdOptions.repoRoot ?? repoRoot);
90
+ if (cmdOptions.format !== "markdown" && cmdOptions.format !== "json") {
91
+ process.stderr.write(`sol report error: invalid --format "${cmdOptions.format}" (expected "markdown" or "json")\n`);
92
+ process.exit(1);
93
+ }
94
+ try {
95
+ (0, dev_gate_js_1.assertPolarisDevContext)(root);
96
+ }
97
+ catch (err) {
98
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
99
+ process.exit(1);
100
+ }
101
+ let artifacts;
102
+ let evidence;
103
+ try {
104
+ artifacts = (0, score_js_1.loadRunArtifacts)(root, runId);
105
+ evidence = (0, sol_evidence_loader_js_1.aggregateSolEvidence)(artifacts);
106
+ }
107
+ catch (err) {
108
+ process.stderr.write(`sol report error: cannot load run artifacts: ${err instanceof Error ? err.message : String(err)}\n`);
109
+ process.exit(1);
110
+ }
111
+ try {
112
+ const report = (0, sol_scorer_js_1.computeSolScoreReport)(evidence);
113
+ const scorecards = (0, sol_scorecard_calculator_js_1.computeAllScorecards)(evidence);
114
+ const qcRecommendations = (0, sol_recommendations_js_1.generateQcRecommendations)(evidence);
115
+ let evaluationPath;
116
+ let scorecardPaths;
117
+ let markdownPath;
118
+ const evaluationRecord = (0, sol_evaluation_writer_js_1.buildEvaluationRecord)(report);
119
+ const rendered = (0, sol_report_renderer_js_1.renderSolMarkdown)(evaluationRecord, scorecards, qcRecommendations);
120
+ if (cmdOptions.write) {
121
+ // Persist the same evaluationRecord object that was rendered, rather than rebuilding internally
122
+ const evaluationDir = (0, sol_evaluation_writer_js_1.getSolEvaluationsDir)(root);
123
+ if (!(0, node_fs_1.existsSync)(evaluationDir))
124
+ (0, node_fs_1.mkdirSync)(evaluationDir, { recursive: true });
125
+ evaluationPath = (0, sol_evaluation_writer_js_1.getEvaluationRecordPath)(root, report.run_id);
126
+ (0, node_fs_1.writeFileSync)(evaluationPath, JSON.stringify(evaluationRecord, null, 2) + "\n", "utf-8");
127
+ scorecardPaths = (0, sol_evaluation_writer_js_1.writeScorecardSet)(root, scorecards);
128
+ markdownPath = (0, sol_evaluation_writer_js_1.writeSolMarkdownReport)(root, runId, rendered.markdown);
129
+ }
130
+ const outputFormat = cmdOptions.json || cmdOptions.format === "json" ? "json" : "markdown";
131
+ if (outputFormat === "json") {
132
+ const output = JSON.stringify({
133
+ run_id: runId,
134
+ evaluation: evaluationRecord,
135
+ scorecards,
136
+ qc_recommendations: qcRecommendations,
137
+ artifacts: {
138
+ evaluation: evaluationPath,
139
+ scorecards: scorecardPaths,
140
+ markdown: markdownPath,
141
+ },
142
+ }, null, 2);
143
+ process.stdout.write(`${output}\n`);
144
+ }
145
+ else {
146
+ process.stdout.write(rendered.markdown);
147
+ }
148
+ }
149
+ catch (err) {
150
+ process.stderr.write(`sol report error: ${err instanceof Error ? err.message : String(err)}\n`);
151
+ process.exit(1);
152
+ }
153
+ });
77
154
  sol
78
155
  .command("propose <diagnosis-file>")
79
156
  .description("File Linear improvement proposals from a diagnosis report (dev-gated — never auto-applied)")
package/dist/cli/medic.js CHANGED
@@ -5,6 +5,10 @@ const commander_1 = require("commander");
5
5
  const node_path_1 = require("node:path");
6
6
  const node_fs_1 = require("node:fs");
7
7
  const chart_id_js_1 = require("../medic/chart-id.js");
8
+ const loader_js_1 = require("../config/loader.js");
9
+ const terminal_cli_js_1 = require("../loop/adapters/terminal-cli.js");
10
+ const run_health_consult_js_1 = require("../medic/run-health-consult.js");
11
+ const treatment_packets_js_1 = require("../medic/treatment-packets.js");
8
12
  function createMedicCommand(options = {}) {
9
13
  const repoRootDefault = options.repoRoot ?? (0, node_path_1.resolve)(process.cwd());
10
14
  const medic = new commander_1.Command("medic")
@@ -34,6 +38,67 @@ function createMedicCommand(options = {}) {
34
38
  process.exit(1);
35
39
  }
36
40
  }));
41
+ medic
42
+ .command("run-health-consult")
43
+ .description("Run a Medic run-health consult from a packet file")
44
+ .requiredOption("--packet-file <path>", "Path to MedicRunHealthPacket JSON")
45
+ .option("-r, --repo-root <path>", "Repository root", repoRootDefault)
46
+ .action(async (cmdOptions) => {
47
+ const repoRoot = cmdOptions.repoRoot;
48
+ let packet;
49
+ try {
50
+ packet = JSON.parse((0, node_fs_1.readFileSync)(cmdOptions.packetFile, "utf-8"));
51
+ }
52
+ catch (err) {
53
+ process.stderr.write(`medic run-health-consult error: cannot read packet: ${err instanceof Error ? err.message : String(err)}\n`);
54
+ process.exit(1);
55
+ return;
56
+ }
57
+ if (packet.role !== "medic-run-health") {
58
+ process.stderr.write(`medic run-health-consult error: packet role must be \"medic-run-health\", got \"${packet.role}\"\n`);
59
+ process.exit(1);
60
+ return;
61
+ }
62
+ const config = (0, loader_js_1.loadConfig)(repoRoot);
63
+ const adapter = new terminal_cli_js_1.TerminalCliAdapter(config.execution);
64
+ // Use shared provider resolution logic: prefer rotation, fall back to first provider
65
+ const providerName = (config.execution.rotation && config.execution.rotation.length > 0)
66
+ ? config.execution.rotation[0]
67
+ : Object.keys(config.execution.providers ?? {})[0] ?? "terminal-cli";
68
+ // Get current branch from git
69
+ let branch = "main";
70
+ try {
71
+ const { execFileSync } = require("node:child_process");
72
+ branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
73
+ cwd: repoRoot,
74
+ encoding: "utf-8",
75
+ }).trim();
76
+ }
77
+ catch {
78
+ // Fall back to main if git fails
79
+ }
80
+ try {
81
+ const result = await (0, run_health_consult_js_1.runMedicRunHealthConsult)({
82
+ packet,
83
+ repoRoot,
84
+ stateFile: packet.cluster_state_path,
85
+ telemetryFile: packet.telemetry_path,
86
+ branch,
87
+ dryRun: false,
88
+ dispatchTreatmentWorkerFn: (input) => (0, treatment_packets_js_1.dispatchTreatmentWorker)({
89
+ ...input,
90
+ repoRoot,
91
+ dispatch: (workerPacket) => adapter.dispatch(workerPacket, { provider: providerName }),
92
+ }),
93
+ });
94
+ (0, node_fs_1.writeFileSync)(packet.result_path, JSON.stringify(result, null, 2), "utf-8");
95
+ process.stdout.write(`Medic run-health consult result written to ${packet.result_path}\n`);
96
+ }
97
+ catch (err) {
98
+ process.stderr.write(`medic run-health-consult error: ${err instanceof Error ? err.message : String(err)}\n`);
99
+ process.exit(1);
100
+ }
101
+ });
37
102
  return medic;
38
103
  }
39
104
  function createChart(options) {
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.recordQcRun = exports.initializeClusterState = exports.writeClusterStateSync = exports.writeClusterState = exports.readClusterStateSync = exports.readClusterState = void 0;
37
37
  exports.pruneExpiredClaims = pruneExpiredClaims;
38
+ exports.pruneMissingQcRunPointers = pruneMissingQcRunPointers;
38
39
  const fs_1 = require("fs");
39
40
  const path = __importStar(require("path"));
40
41
  const local_graph_1 = require("../tracker/local-graph");
@@ -350,12 +351,25 @@ const recordQcRun = async (clusterId, result, repoRoot) => {
350
351
  if (!currentState) {
351
352
  throw new Error(`Cluster ${clusterId} state not found; cannot record QC run ${result.qcRunId}.`);
352
353
  }
354
+ const rawArtifactPaths = result.rawArtifactPaths ?? [];
355
+ const providerAttemptPath = result.providerAttempt?.rawOutputArtifactPath;
356
+ const auditArtifactPaths = providerAttemptPath
357
+ ? [...rawArtifactPaths, providerAttemptPath]
358
+ : rawArtifactPaths;
359
+ const failedRun = result.status === "failed" || result.status === "blocked" || result.allProvidersFailed === true;
360
+ let availability = "available";
361
+ if (failedRun && auditArtifactPaths.some((p) => !(0, fs_1.existsSync)(p))) {
362
+ availability = "unavailable";
363
+ }
353
364
  const pointer = {
354
365
  artifact_path: artifactPath,
355
366
  status: result.status,
356
367
  provider: result.provider,
357
368
  started_at: result.startedAt,
358
369
  completed_at: result.completedAt,
370
+ availability,
371
+ raw_artifact_paths: rawArtifactPaths.length > 0 ? rawArtifactPaths : undefined,
372
+ provider_attempt_artifact_path: providerAttemptPath,
359
373
  };
360
374
  const nextState = {
361
375
  ...currentState,
@@ -369,3 +383,45 @@ const recordQcRun = async (clusterId, result, repoRoot) => {
369
383
  return { artifactPath, state: nextState };
370
384
  };
371
385
  exports.recordQcRun = recordQcRun;
386
+ /**
387
+ * Remove QC run pointers whose primary artifacts are missing and mark
388
+ * pointers with missing raw audit artifacts as unavailable. Returns a new
389
+ * state object; callers must persist the result if they want the cleanup to
390
+ * be durable.
391
+ */
392
+ function pruneMissingQcRunPointers(state) {
393
+ const warnings = [];
394
+ const pruned = [];
395
+ if (!state.qc_runs) {
396
+ return { state, pruned, warnings };
397
+ }
398
+ const validation = (0, artifacts_js_1.validateQcArtifactPointers)(state.qc_runs);
399
+ if (validation.ok && validation.unavailable.length === 0) {
400
+ return { state, pruned, warnings };
401
+ }
402
+ const nextQcRuns = {};
403
+ for (const [runId, pointer] of Object.entries(state.qc_runs)) {
404
+ if (validation.missing.includes(pointer.artifact_path)) {
405
+ pruned.push(runId);
406
+ warnings.push(`QC run ${runId} pointer pruned: artifact missing ${pointer.artifact_path}`);
407
+ continue;
408
+ }
409
+ const updated = { ...pointer };
410
+ const hasMissingRaw = (pointer.raw_artifact_paths ?? []).some((p) => validation.unavailable.includes(p)) ||
411
+ (pointer.provider_attempt_artifact_path &&
412
+ validation.unavailable.includes(pointer.provider_attempt_artifact_path));
413
+ if (hasMissingRaw && updated.availability === "available") {
414
+ updated.availability = "unavailable";
415
+ warnings.push(`QC run ${runId} raw audit artifacts missing; marked unavailable`);
416
+ }
417
+ nextQcRuns[runId] = updated;
418
+ }
419
+ return {
420
+ state: {
421
+ ...state,
422
+ qc_runs: nextQcRuns,
423
+ },
424
+ pruned,
425
+ warnings,
426
+ };
427
+ }
@@ -55,6 +55,9 @@ exports.DEFAULT_CONFIG = {
55
55
  requireMapValidation: true,
56
56
  requireSchemaValidation: true,
57
57
  archiveRunSnapshot: true,
58
+ medic: {
59
+ bypassPolicy: "none",
60
+ },
58
61
  },
59
62
  tracker: {
60
63
  lifecyclePolicy: {
@@ -548,6 +548,20 @@ function validateConfig(config) {
548
548
  result.errors.push("finalize.archiveRunSnapshot must be a boolean");
549
549
  }
550
550
  }
551
+ if ("medic" in config.finalize && config.finalize.medic !== undefined) {
552
+ if (!isPlainObject(config.finalize.medic)) {
553
+ result.valid = false;
554
+ result.errors.push("finalize.medic must be an object");
555
+ }
556
+ else {
557
+ if ("bypassPolicy" in config.finalize.medic && config.finalize.medic.bypassPolicy !== undefined) {
558
+ if (!isString(config.finalize.medic.bypassPolicy) || !["none", "cli"].includes(config.finalize.medic.bypassPolicy)) {
559
+ result.valid = false;
560
+ result.errors.push('finalize.medic.bypassPolicy must be one of "none" or "cli"');
561
+ }
562
+ }
563
+ }
564
+ }
551
565
  }
552
566
  }
553
567
  // tracker
@@ -1197,6 +1211,128 @@ function validateConfig(config) {
1197
1211
  }
1198
1212
  }
1199
1213
  }
1214
+ // run_health
1215
+ if ("run_health" in config && config.run_health !== undefined) {
1216
+ if (!isPlainObject(config.run_health)) {
1217
+ result.valid = false;
1218
+ result.errors.push("run_health must be an object");
1219
+ }
1220
+ else {
1221
+ if ("foreman_symptoms" in config.run_health && config.run_health.foreman_symptoms !== undefined) {
1222
+ if (!isPlainObject(config.run_health.foreman_symptoms)) {
1223
+ result.valid = false;
1224
+ result.errors.push("run_health.foreman_symptoms must be an object");
1225
+ }
1226
+ else {
1227
+ if ("enabled" in config.run_health.foreman_symptoms && config.run_health.foreman_symptoms.enabled !== undefined) {
1228
+ if (!isBoolean(config.run_health.foreman_symptoms.enabled)) {
1229
+ result.valid = false;
1230
+ result.errors.push("run_health.foreman_symptoms.enabled must be a boolean");
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+ // sol
1238
+ if ("sol" in config && config.sol !== undefined) {
1239
+ if (!isPlainObject(config.sol)) {
1240
+ result.valid = false;
1241
+ result.errors.push("sol must be an object");
1242
+ }
1243
+ else {
1244
+ if ("history" in config.sol && config.sol.history !== undefined) {
1245
+ if (!isPlainObject(config.sol.history)) {
1246
+ result.valid = false;
1247
+ result.errors.push("sol.history must be an object");
1248
+ }
1249
+ else {
1250
+ if ("enabled" in config.sol.history && config.sol.history.enabled !== undefined) {
1251
+ if (!isBoolean(config.sol.history.enabled)) {
1252
+ result.valid = false;
1253
+ result.errors.push("sol.history.enabled must be a boolean");
1254
+ }
1255
+ }
1256
+ if ("path" in config.sol.history && config.sol.history.path !== undefined) {
1257
+ if (!isString(config.sol.history.path)) {
1258
+ result.valid = false;
1259
+ result.errors.push("sol.history.path must be a string");
1260
+ }
1261
+ }
1262
+ }
1263
+ }
1264
+ if ("thresholds" in config.sol && config.sol.thresholds !== undefined) {
1265
+ if (!isPlainObject(config.sol.thresholds)) {
1266
+ result.valid = false;
1267
+ result.errors.push("sol.thresholds must be an object");
1268
+ }
1269
+ else {
1270
+ if ("enabled" in config.sol.thresholds && config.sol.thresholds.enabled !== undefined) {
1271
+ if (!isBoolean(config.sol.thresholds.enabled)) {
1272
+ result.valid = false;
1273
+ result.errors.push("sol.thresholds.enabled must be a boolean");
1274
+ }
1275
+ }
1276
+ if ("policy" in config.sol.thresholds && config.sol.thresholds.policy !== undefined) {
1277
+ if (!isPlainObject(config.sol.thresholds.policy)) {
1278
+ result.valid = false;
1279
+ result.errors.push("sol.thresholds.policy must be an object");
1280
+ }
1281
+ else {
1282
+ if ("createRunHealthReport" in config.sol.thresholds.policy && config.sol.thresholds.policy.createRunHealthReport !== undefined) {
1283
+ if (!isBoolean(config.sol.thresholds.policy.createRunHealthReport)) {
1284
+ result.valid = false;
1285
+ result.errors.push("sol.thresholds.policy.createRunHealthReport must be a boolean");
1286
+ }
1287
+ }
1288
+ if ("requireMedic" in config.sol.thresholds.policy && config.sol.thresholds.policy.requireMedic !== undefined) {
1289
+ if (!isBoolean(config.sol.thresholds.policy.requireMedic)) {
1290
+ result.valid = false;
1291
+ result.errors.push("sol.thresholds.policy.requireMedic must be a boolean");
1292
+ }
1293
+ }
1294
+ }
1295
+ }
1296
+ if ("low_composite_score" in config.sol.thresholds && config.sol.thresholds.low_composite_score !== undefined) {
1297
+ if (!isNumber(config.sol.thresholds.low_composite_score) || !inRange(config.sol.thresholds.low_composite_score, 0, 1)) {
1298
+ result.valid = false;
1299
+ result.errors.push("sol.thresholds.low_composite_score must be a number between 0 and 1");
1300
+ }
1301
+ }
1302
+ if ("qc_repair_loop_failure_statuses" in config.sol.thresholds && config.sol.thresholds.qc_repair_loop_failure_statuses !== undefined) {
1303
+ if (!isStringArray(config.sol.thresholds.qc_repair_loop_failure_statuses)) {
1304
+ result.valid = false;
1305
+ result.errors.push("sol.thresholds.qc_repair_loop_failure_statuses must be an array of strings");
1306
+ }
1307
+ }
1308
+ if ("repeated_provider_failures" in config.sol.thresholds && config.sol.thresholds.repeated_provider_failures !== undefined) {
1309
+ if (!isPositiveInteger(config.sol.thresholds.repeated_provider_failures)) {
1310
+ result.valid = false;
1311
+ result.errors.push("sol.thresholds.repeated_provider_failures must be a positive integer");
1312
+ }
1313
+ }
1314
+ if ("foreman_intervention_count" in config.sol.thresholds && config.sol.thresholds.foreman_intervention_count !== undefined) {
1315
+ if (!isNonNegativeInteger(config.sol.thresholds.foreman_intervention_count)) {
1316
+ result.valid = false;
1317
+ result.errors.push("sol.thresholds.foreman_intervention_count must be a non-negative integer");
1318
+ }
1319
+ }
1320
+ if ("stale_wrong_run_telemetry" in config.sol.thresholds && config.sol.thresholds.stale_wrong_run_telemetry !== undefined) {
1321
+ if (!isBoolean(config.sol.thresholds.stale_wrong_run_telemetry)) {
1322
+ result.valid = false;
1323
+ result.errors.push("sol.thresholds.stale_wrong_run_telemetry must be a boolean");
1324
+ }
1325
+ }
1326
+ if ("validation_failures" in config.sol.thresholds && config.sol.thresholds.validation_failures !== undefined) {
1327
+ if (!isNonNegativeInteger(config.sol.thresholds.validation_failures)) {
1328
+ result.valid = false;
1329
+ result.errors.push("sol.thresholds.validation_failures must be a non-negative integer");
1330
+ }
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1200
1336
  // unknown top-level fields -> warnings
1201
1337
  const knownKeys = new Set([
1202
1338
  "version",
@@ -1213,6 +1349,11 @@ function validateConfig(config) {
1213
1349
  "budget",
1214
1350
  "compact",
1215
1351
  "qc",
1352
+ "sol",
1353
+ "run_health",
1354
+ "orchestration",
1355
+ "skill_packet",
1356
+ "simplicity",
1216
1357
  ]);
1217
1358
  for (const key of Object.keys(config)) {
1218
1359
  if (!knownKeys.has(key)) {
@@ -37,6 +37,7 @@ function isPromotedClusterArtifact(relativePath, activeClusterId) {
37
37
  const suffix = relativePath.slice(activeClusterPrefix.length);
38
38
  return (suffix === "clusters.json"
39
39
  || suffix === "cluster-state.json"
40
+ || suffix === "state.json"
40
41
  || suffix.startsWith("packets/")
41
42
  || suffix.startsWith("results/")
42
43
  || suffix.startsWith("qc/"));
@@ -131,6 +132,7 @@ function getArtifactPromotionPolicy(activeClusterId) {
131
132
  promoted: [
132
133
  `${activeClusterPrefix}clusters.json`,
133
134
  `${activeClusterPrefix}cluster-state.json`,
135
+ `${activeClusterPrefix}state.json`,
134
136
  `${activeClusterPrefix}packets/**`,
135
137
  `${activeClusterPrefix}results/**`,
136
138
  `${activeClusterPrefix}qc/**`,
@@ -1,9 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPrBody = buildPrBody;
3
4
  exports.createDraftPr = createDraftPr;
4
5
  const node_child_process_1 = require("node:child_process");
6
+ function buildPrBody(state, branch, authoritativeChildCount) {
7
+ return [
8
+ `**Cluster ID:** ${state.cluster_id}`,
9
+ `**Run ID:** ${state.run_id}`,
10
+ `**Branch:** ${branch}`,
11
+ `**Children completed:** ${authoritativeChildCount ?? state.completed_children.length}`,
12
+ ``,
13
+ `_Generated by polaris finalize_`,
14
+ ].join("\n");
15
+ }
5
16
  function createDraftPr(options) {
6
- const { repoRoot, branch, state, draft } = options;
17
+ const { repoRoot, branch, state, draft, authoritativeChildCount } = options;
7
18
  if (!state.cluster_id) {
8
19
  throw new Error(`createDraftPr: state.cluster_id is empty — cannot create PR without a cluster identifier`);
9
20
  }
@@ -16,14 +27,7 @@ function createDraftPr(options) {
16
27
  `branch/cluster mismatch, aborting PR creation`);
17
28
  }
18
29
  const title = `polaris finalize: ${state.cluster_id} (${state.run_id})`;
19
- const body = [
20
- `**Cluster ID:** ${state.cluster_id}`,
21
- `**Run ID:** ${state.run_id}`,
22
- `**Branch:** ${branch}`,
23
- `**Children completed:** ${state.completed_children.length}`,
24
- ``,
25
- `_Generated by polaris finalize_`,
26
- ].join("\n");
30
+ const body = buildPrBody(state, branch, authoritativeChildCount);
27
31
  const args = [
28
32
  "pr", "create",
29
33
  "--title", title,