@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
@@ -10,6 +10,39 @@
10
10
  * WorkerPacket extends BootstrapPacket so all existing adapters that
11
11
  * accept BootstrapPacket transparently accept WorkerPacket.
12
12
  */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
13
46
  Object.defineProperty(exports, "__esModule", { value: true });
14
47
  exports.REPAIR_RETURN_CONTRACT = exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_SYMPTOM_CATEGORIES = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
15
48
  exports.roleContextForWorkerRole = roleContextForWorkerRole;
@@ -20,6 +53,7 @@ exports.compileImplPacket = compileImplPacket;
20
53
  exports.compileFinalizePacket = compileFinalizePacket;
21
54
  exports.compilePreflightPacket = compilePreflightPacket;
22
55
  exports.compileRepairWorkerPacket = compileRepairWorkerPacket;
56
+ const path = __importStar(require("node:path"));
23
57
  const worker_prompt_js_1 = require("./worker-prompt.js");
24
58
  const body_parser_js_1 = require("./body-parser.js");
25
59
  const WORKER_ROLE_CONTEXT = {
@@ -172,6 +206,49 @@ function defaultLifecycle(maxConcurrent, cleanupOnExit) {
172
206
  cleanup_on_exit: cleanupOnExit,
173
207
  };
174
208
  }
209
+ /** Derives the adjacent test file path(s) for a single source TypeScript file. */
210
+ function deriveTestPaths(sourcePath) {
211
+ if (!sourcePath.endsWith('.ts') || sourcePath.endsWith('.test.ts') || sourcePath.endsWith('.d.ts')) {
212
+ return [];
213
+ }
214
+ if (sourcePath.includes('*') || sourcePath.includes('?')) {
215
+ return [];
216
+ }
217
+ const candidates = [sourcePath.replace(/\.ts$/, '.test.ts')];
218
+ if (sourcePath.endsWith('/index.ts')) {
219
+ const dir = sourcePath.slice(0, -'/index.ts'.length);
220
+ const dirName = path.basename(dir);
221
+ if (dirName) {
222
+ candidates.push(`${dir}/${dirName}.test.ts`);
223
+ }
224
+ }
225
+ return candidates;
226
+ }
227
+ /**
228
+ * Expands an allowed scope list with adjacent test paths when the validation
229
+ * commands include a vitest run. This lets workers touch test files for source
230
+ * files they are modifying, matching the `*.test.ts` validation commands.
231
+ */
232
+ function expandScopeWithTestPaths(scope, validationCommands) {
233
+ const hasVitest = validationCommands.some((cmd) => cmd.includes('vitest'));
234
+ if (!hasVitest || scope.length === 0)
235
+ return scope;
236
+ const testPaths = new Set();
237
+ for (const item of scope) {
238
+ for (const testPath of deriveTestPaths(item)) {
239
+ testPaths.add(testPath);
240
+ }
241
+ }
242
+ if (testPaths.size === 0)
243
+ return scope;
244
+ const expanded = [...scope];
245
+ for (const testPath of testPaths) {
246
+ if (!expanded.includes(testPath)) {
247
+ expanded.push(testPath);
248
+ }
249
+ }
250
+ return expanded;
251
+ }
175
252
  /**
176
253
  * Build a compiled startup worker packet.
177
254
  * Startup workers prepare tracker/graph/config/run-state evidence; the parent
@@ -257,6 +334,8 @@ function compileImplPacket(input) {
257
334
  const resolvedRequirements = input.issueContext?.key_requirements.length
258
335
  ? input.issueContext.key_requirements
259
336
  : bodyParsed?.requirements ?? [];
337
+ // Include adjacent test files when the validation commands indicate a vitest run.
338
+ const expandedScope = expandScopeWithTestPaths(resolvedScope, resolvedValidation);
260
339
  const requirementLines = resolvedRequirements.map((r, i) => ` ${i + 1}. ${r}`);
261
340
  const bodyLines = input.issueContext?.body
262
341
  ? [`Issue description:\n${input.issueContext.body}`]
@@ -285,7 +364,7 @@ function compileImplPacket(input) {
285
364
  stateFile: input.stateFile,
286
365
  telemetryFile: input.telemetryFile,
287
366
  issueContext: input.issueContext,
288
- allowedScope: resolvedScope,
367
+ allowedScope: expandedScope,
289
368
  validationCommands: resolvedValidation,
290
369
  mode: promptMode,
291
370
  simplicityMode: input.simplicityMode,
@@ -301,7 +380,7 @@ function compileImplPacket(input) {
301
380
  instructions: {
302
381
  primary_goal: promptResult.prompt,
303
382
  steps,
304
- allowed_scope: resolvedScope,
383
+ allowed_scope: expandedScope,
305
384
  validation_commands: resolvedValidation,
306
385
  issue_context: input.issueContext,
307
386
  },
@@ -17,7 +17,10 @@ function inferRoute(filePath, repoRoot, config, knownRoutes, branchName) {
17
17
  const subdir = rest.split("/")[0];
18
18
  if (subdir && rest.includes("/")) {
19
19
  domain = subdir;
20
- route = `${sourceRoot}/${subdir}`;
20
+ // Route identity follows the file's own containing directory, not just the
21
+ // top-level domain subdir — otherwise nested folders (e.g. src/runtime/continuation)
22
+ // incorrectly report the parent domain's route (e.g. src/runtime).
23
+ route = (0, node_path_1.dirname)(filePath).replace(/\\/g, "/");
21
24
  taskchain = `polaris-${subdir}`;
22
25
  confidence += 0.85;
23
26
  tags.push(subdir);
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Polaris routing anomaly → Medic/state-repair signal classification.
4
+ *
5
+ * These signals are emitted by the loop/dispatch/runtime boundary and are
6
+ * reviewed by Medic, not used to mutate routing.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.classifyRoutingTelemetryEvent = classifyRoutingTelemetryEvent;
10
+ /**
11
+ * Map a raw telemetry event to a state-repair review signal.
12
+ *
13
+ * Only loop/dispatch boundary events that indicate runtime state-repair work
14
+ * are classified. The returned signal has occurrences=1 and a single child_id;
15
+ * consumers (such as SOL scoring) should aggregate by signal name.
16
+ */
17
+ function classifyRoutingTelemetryEvent(event) {
18
+ const eventName = event["event"];
19
+ if (typeof eventName !== "string")
20
+ return undefined;
21
+ const childId = typeof event["child_id"] === "string" ? event["child_id"] : undefined;
22
+ const childIds = childId ? [childId] : [];
23
+ switch (eventName) {
24
+ case "sealed-result-read-error":
25
+ return {
26
+ signal: "missing-sealed-result",
27
+ reason: "missing-sealed-result",
28
+ occurrences: 1,
29
+ child_ids: childIds,
30
+ };
31
+ case "stale-dispatch-aborted":
32
+ return {
33
+ signal: "stale-dispatch-abort",
34
+ reason: "stale-dispatch-abort",
35
+ occurrences: 1,
36
+ child_ids: childIds,
37
+ };
38
+ case "invalid-inline-attempt":
39
+ return {
40
+ signal: "invalid-inline-attempt",
41
+ reason: "invalid-inline-attempt",
42
+ occurrences: 1,
43
+ child_ids: childIds,
44
+ };
45
+ case "child-recovery-initiated": {
46
+ const recoveryReason = event["recovery_reason"];
47
+ if (recoveryReason === "stale-dispatch") {
48
+ return {
49
+ signal: "stale-dispatch-abort",
50
+ reason: "stale-dispatch",
51
+ occurrences: 1,
52
+ child_ids: childIds,
53
+ };
54
+ }
55
+ return undefined;
56
+ }
57
+ default:
58
+ return undefined;
59
+ }
60
+ }
@@ -114,11 +114,12 @@ function markInProgress(report, chartRef, repoRoot) {
114
114
  (0, index_js_1.markMedicDecision)(report.run_id, { status: "in-progress", chartRefs: [chartRef] }, repoRoot);
115
115
  }
116
116
  function markResolved(report, chartRef, treatmentRefs, outcome, repoRoot) {
117
+ const success = outcome === "treatment-success" || outcome === "no-treatment-needed";
117
118
  (0, index_js_1.markMedicDecision)(report.run_id, {
118
- status: "resolved",
119
+ status: success ? "resolved" : "in-progress",
119
120
  chartRefs: [chartRef],
120
121
  treatmentPacketRefs: treatmentRefs,
121
- resolvedAt: new Date().toISOString(),
122
+ resolvedAt: success ? new Date().toISOString() : undefined,
122
123
  resolutionNotes: `Terminal outcome: ${outcome}`,
123
124
  }, repoRoot);
124
125
  }
@@ -355,7 +356,7 @@ async function runMedicRunHealthConsult(input) {
355
356
  run_id: packet.run_id,
356
357
  cluster_id: packet.cluster_id,
357
358
  dispatch_id: packet.dispatch_id,
358
- status: "resolved",
359
+ status: "blocked",
359
360
  chart_id: chart.chart_id,
360
361
  decision: "terminal",
361
362
  treatment_packet_refs: allTreatmentRefs,
@@ -371,7 +372,7 @@ async function runMedicRunHealthConsult(input) {
371
372
  run_id: packet.run_id,
372
373
  cluster_id: packet.cluster_id,
373
374
  dispatch_id: packet.dispatch_id,
374
- status: "resolved",
375
+ status: remainingSymptoms.length === 0 ? "resolved" : "blocked",
375
376
  chart_id: chart.chart_id,
376
377
  decision: "terminal",
377
378
  treatment_packet_refs: allTreatmentRefs,
@@ -59,7 +59,7 @@ function buildTreatmentPackets(input) {
59
59
  return treated.map((symptom) => {
60
60
  const packetId = buildTreatmentPacketId(report.run_id, round, symptom.id);
61
61
  const dispatchId = (0, node_crypto_1.randomUUID)();
62
- const resultFile = (0, node_path_1.join)(getMedicDir(report.cluster_id, repoRoot), `${packetId}-result.json`);
62
+ const resultFile = (0, node_path_1.relative)(repoRoot, (0, node_path_1.join)(getMedicDir(report.cluster_id, repoRoot), `${packetId}-result.json`));
63
63
  return {
64
64
  packet_id: packetId,
65
65
  run_id: report.run_id,
@@ -154,12 +154,16 @@ async function dispatchTreatmentWorker(input) {
154
154
  const result = await dispatch(workerPacket);
155
155
  const summary = parseTreatmentWorkerSummary(result.summary);
156
156
  if (result.exit_code === 0 && summary?.status === "done") {
157
+ const completedTreatment = { ...treatment, status: "completed" };
158
+ writeTreatmentPacket({ treatment: completedTreatment, repoRoot });
157
159
  return {
158
160
  packet_id: treatment.packet_id,
159
161
  status: "success",
160
162
  commit_sha: summary.commit,
161
163
  };
162
164
  }
165
+ const failedTreatment = { ...treatment, status: "failed" };
166
+ writeTreatmentPacket({ treatment: failedTreatment, repoRoot });
163
167
  return {
164
168
  packet_id: treatment.packet_id,
165
169
  status: "failure",
package/dist/qc/policy.js CHANGED
@@ -59,6 +59,8 @@ function decideProviderFailureAction(reason, policy) {
59
59
  return policy?.timeout ?? "fail";
60
60
  case "parse-failed":
61
61
  return policy?.parseFailure ?? "fail";
62
+ case "rate-limited":
63
+ return policy?.rateLimited ?? "fail";
62
64
  default:
63
65
  return "fail";
64
66
  }
@@ -37,6 +37,8 @@ const FINDING_BOOKKEEPING_KEYS = [
37
37
  "findingId",
38
38
  ];
39
39
  const PROGRESS_SHAPE_KEYS = new Set(["event", "progress", "heartbeat", "complete", "review_context"]);
40
+ const ERROR_TYPE_RATE_LIMIT = new Set(["rate_limit", "rate-limit", "ratelimit", "rate limit"]);
41
+ const ERROR_TYPE_AUTH = new Set(["auth", "authentication", "unauthorized", "unauthorised", "forbidden"]);
40
42
  const PROGRESS_TYPE_VALUES = new Set([
41
43
  "progress",
42
44
  "status",
@@ -56,6 +58,14 @@ const PROGRESS_STATUS_VALUES = new Set([
56
58
  "ok",
57
59
  "success",
58
60
  ]);
61
+ const TERMINAL_COMPLETE_STATUSES = new Set([
62
+ "review_completed",
63
+ "review_skipped",
64
+ "completed",
65
+ "complete",
66
+ "done",
67
+ "success",
68
+ ]);
59
69
  function hasFindingLocation(record) {
60
70
  return FINDING_LOCATION_KEYS.some((key) => record[key] !== undefined);
61
71
  }
@@ -112,6 +122,8 @@ function isProgressRecord(record) {
112
122
  return false;
113
123
  }
114
124
  function isActionableFinding(record) {
125
+ if (isErrorRecord(record))
126
+ return false;
115
127
  if (isProgressRecord(record))
116
128
  return false;
117
129
  return hasFindingLocation(record) || hasFindingContent(record);
@@ -124,6 +136,87 @@ function makeUnusableOutputError(message) {
124
136
  err.qcFailureReason = "unusable-output";
125
137
  return err;
126
138
  }
139
+ function makeQcFailureError(reason, message) {
140
+ const err = new Error(message);
141
+ err.qcFailureReason = reason;
142
+ return err;
143
+ }
144
+ function isErrorRecord(record) {
145
+ if (record.type === "error")
146
+ return true;
147
+ if (typeof record.errorType === "string")
148
+ return true;
149
+ if (record.error === true || record.error === "true")
150
+ return true;
151
+ if (typeof record.status === "string" && record.status.toLowerCase() === "error")
152
+ return true;
153
+ return false;
154
+ }
155
+ function errorTypeFromRecord(record) {
156
+ const fromType = typeof record.errorType === "string" ? record.errorType : undefined;
157
+ const fromMessage = typeof record.message === "string" ? record.message : undefined;
158
+ return fromType ?? fromMessage;
159
+ }
160
+ function classifyErrorType(record) {
161
+ const raw = errorTypeFromRecord(record);
162
+ const errorType = (raw ?? "").toString().toLowerCase().trim();
163
+ if (errorType === "")
164
+ return "unavailable-provider";
165
+ if (ERROR_TYPE_RATE_LIMIT.has(errorType))
166
+ return "rate-limited";
167
+ if (ERROR_TYPE_AUTH.has(errorType))
168
+ return "auth-failure";
169
+ if (errorType.includes("rate limit"))
170
+ return "rate-limited";
171
+ if (errorType.includes("auth"))
172
+ return "auth-failure";
173
+ if (errorType.includes("unauthorized"))
174
+ return "auth-failure";
175
+ if (errorType.includes("forbidden"))
176
+ return "auth-failure";
177
+ if (["timeout", "timed out"].includes(errorType))
178
+ return "timeout";
179
+ if (errorType.includes("not found") || errorType.includes("not_found"))
180
+ return "command-not-found";
181
+ if (errorType.includes("unavailable") || errorType.includes("api_error") || errorType.includes("apierror") || errorType.includes("server") || errorType.includes("network") || errorType.includes("connection") || errorType.includes("internal")) {
182
+ return "unavailable-provider";
183
+ }
184
+ return "unavailable-provider";
185
+ }
186
+ function getTerminalCompleteFindingsCount(record) {
187
+ if (record.findings !== undefined) {
188
+ if (Array.isArray(record.findings)) {
189
+ return record.findings.length;
190
+ }
191
+ const count = typeof record.findings === "number" ? record.findings : Number(record.findings);
192
+ if (Number.isFinite(count))
193
+ return count;
194
+ }
195
+ const summary = record.summary;
196
+ if (summary && typeof summary === "object") {
197
+ const summaryRecord = summary;
198
+ if (summaryRecord.total !== undefined) {
199
+ const count = Number(summaryRecord.total);
200
+ if (Number.isFinite(count))
201
+ return count;
202
+ }
203
+ if (summaryRecord.issues !== undefined) {
204
+ const count = Number(summaryRecord.issues);
205
+ if (Number.isFinite(count))
206
+ return count;
207
+ }
208
+ }
209
+ return undefined;
210
+ }
211
+ function isTerminalCompleteNoFindings(record) {
212
+ if (record.type !== "complete")
213
+ return false;
214
+ const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
215
+ if (!TERMINAL_COMPLETE_STATUSES.has(status))
216
+ return false;
217
+ const count = getTerminalCompleteFindingsCount(record);
218
+ return count === undefined || count === 0;
219
+ }
127
220
  function buildRange(raw) {
128
221
  const startLine = coerceNumber(raw.startLine ?? raw.line) ?? 1;
129
222
  const endLine = coerceNumber(raw.endLine);
@@ -144,14 +237,27 @@ function parseFindingsFromPayload(payload) {
144
237
  return [];
145
238
  }
146
239
  const record = payload;
147
- if (Array.isArray(record.findings)) {
148
- return record.findings.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
149
- }
150
- if (Array.isArray(record.issues)) {
151
- return record.issues.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
240
+ if (isErrorRecord(record)) {
241
+ throw makeQcFailureError(classifyErrorType(record), `CodeRabbit provider returned an error: ${record.message ?? record.errorType ?? "unknown"}`);
152
242
  }
153
- if (Array.isArray(record.results)) {
154
- return record.results.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
243
+ const arrays = [record.findings, record.issues, record.results];
244
+ for (const arr of arrays) {
245
+ if (Array.isArray(arr)) {
246
+ const findings = [];
247
+ for (const item of arr) {
248
+ if (typeof item !== "object" || item === null) {
249
+ continue;
250
+ }
251
+ const itemRecord = item;
252
+ if (isErrorRecord(itemRecord)) {
253
+ throw makeQcFailureError(classifyErrorType(itemRecord), `CodeRabbit provider returned an error in findings array: ${itemRecord.message ?? itemRecord.errorType ?? "unknown"}`);
254
+ }
255
+ if (isActionableFinding(itemRecord)) {
256
+ findings.push(item);
257
+ }
258
+ }
259
+ return findings;
260
+ }
155
261
  }
156
262
  // Single finding wrapped in an object
157
263
  if (isActionableFinding(record)) {
@@ -172,6 +278,9 @@ function parseReport(output, format, parser) {
172
278
  const findings = parseFindingsFromPayload(data);
173
279
  const record = data;
174
280
  if (findings.length === 0) {
281
+ if (isTerminalCompleteNoFindings(record)) {
282
+ return { findings: [] };
283
+ }
175
284
  if (isProgressRecord(record)) {
176
285
  throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
177
286
  }
@@ -198,7 +307,19 @@ function parseReport(output, format, parser) {
198
307
  try {
199
308
  const parsed = JSON.parse(text);
200
309
  if (Array.isArray(parsed)) {
201
- const findings = parsed.filter((item) => typeof item === "object" && item !== null && isActionableFinding(item));
310
+ const findings = [];
311
+ for (const item of parsed) {
312
+ if (typeof item !== "object" || item === null) {
313
+ continue;
314
+ }
315
+ const itemRecord = item;
316
+ if (isErrorRecord(itemRecord)) {
317
+ throw makeQcFailureError(classifyErrorType(itemRecord), `CodeRabbit provider returned an error in JSON array: ${itemRecord.message ?? itemRecord.errorType ?? "unknown"}`);
318
+ }
319
+ if (isActionableFinding(itemRecord)) {
320
+ findings.push(item);
321
+ }
322
+ }
202
323
  if (findings.length === 0 && parsed.length > 0) {
203
324
  const unusableCount = parsed.filter((item) => typeof item === "object" && item !== null && isUnusableFindingRecord(item)).length;
204
325
  if (unusableCount > 0) {
@@ -210,6 +331,9 @@ function parseReport(output, format, parser) {
210
331
  const findings = parseFindingsFromPayload(parsed);
211
332
  if (findings.length === 0) {
212
333
  const parsedRecord = parsed;
334
+ if (isTerminalCompleteNoFindings(parsedRecord)) {
335
+ return { findings: [] };
336
+ }
213
337
  if (isProgressRecord(parsedRecord)) {
214
338
  throw makeUnusableOutputError("CodeRabbit output was a progress/status/heartbeat record");
215
339
  }
@@ -260,9 +384,12 @@ function parseReport(output, format, parser) {
260
384
  continue;
261
385
  }
262
386
  const record = parsed;
263
- if (record.type === "complete" && record.status === "review_skipped") {
387
+ if (isTerminalCompleteNoFindings(record)) {
264
388
  sawExplicitSkip = true;
265
389
  }
390
+ if (isErrorRecord(record)) {
391
+ throw makeQcFailureError(classifyErrorType(record), `CodeRabbit provider returned an error in JSONL: ${record.message ?? record.errorType ?? "unknown"}`);
392
+ }
266
393
  if (isProgressRecord(record)) {
267
394
  progressLineCount++;
268
395
  continue;
@@ -274,7 +401,10 @@ function parseReport(output, format, parser) {
274
401
  unusableLineCount++;
275
402
  }
276
403
  }
277
- catch {
404
+ catch (err) {
405
+ if (typeof err === "object" && err !== null && "qcFailureReason" in err) {
406
+ throw err;
407
+ }
278
408
  // Ignore unparseable lines.
279
409
  }
280
410
  }
@@ -19,10 +19,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.DEFAULT_REPAIR_DISPATCH_TIMEOUT_MS = exports.DEFAULT_MAX_REPAIR_ROUNDS = void 0;
20
20
  exports.partitionRepairPackets = partitionRepairPackets;
21
21
  exports.initRepairLoopState = initRepairLoopState;
22
+ exports.getQcResolutionArtifactPath = getQcResolutionArtifactPath;
23
+ exports.resolveQcResolutionFindings = resolveQcResolutionFindings;
24
+ exports.getResolverIdentity = getResolverIdentity;
25
+ exports.isValidQcResolutionArtifact = isValidQcResolutionArtifact;
26
+ exports.readQcResolutionArtifact = readQcResolutionArtifact;
27
+ exports.writeQcResolutionArtifact = writeQcResolutionArtifact;
22
28
  exports.runQcRepairLoop = runQcRepairLoop;
23
29
  const node_fs_1 = require("node:fs");
30
+ const node_child_process_1 = require("node:child_process");
24
31
  const node_path_1 = require("node:path");
25
32
  const repair_packets_js_1 = require("./repair-packets.js");
33
+ const types_js_1 = require("./types.js");
26
34
  const orchestration_js_1 = require("./orchestration.js");
27
35
  const store_js_1 = require("../cluster-state/store.js");
28
36
  // ── Constants ─────────────────────────────────────────────────────────────────
@@ -189,6 +197,127 @@ function initRepairLoopState(opts) {
189
197
  updated_at: now,
190
198
  };
191
199
  }
200
+ // ── Operator resolution artifact helpers ──────────────────────────────────────
201
+ /** Path for the operator-written resolution artifact for a repair round. */
202
+ function getQcResolutionArtifactPath(clusterId, round, repoRoot) {
203
+ return (0, node_path_1.join)((0, repair_packets_js_1.getRepairRoundDir)(clusterId, round, repoRoot), "resolution.json");
204
+ }
205
+ /** Resolve the finding IDs covered by a resolution artifact. */
206
+ function resolveQcResolutionFindings(manifest, explicitFindings) {
207
+ const manifestFindings = [
208
+ ...new Set(manifest.packets.flatMap((p) => p.findingIds)),
209
+ ].sort();
210
+ if (!explicitFindings || explicitFindings.length === 0) {
211
+ return manifestFindings;
212
+ }
213
+ const allowed = new Set(manifestFindings);
214
+ const unknown = explicitFindings.filter((id) => !allowed.has(id));
215
+ if (unknown.length > 0) {
216
+ throw new Error(`Unknown finding IDs: ${unknown.join(", ")}. Expected one of: ${manifestFindings.join(", ") || "none"}`);
217
+ }
218
+ return [...new Set(explicitFindings)].sort();
219
+ }
220
+ /** Determine the resolver identity from git config or the default operator. */
221
+ function getResolverIdentity(repoRoot) {
222
+ try {
223
+ const name = (0, node_child_process_1.execFileSync)("git", ["config", "user.name"], {
224
+ cwd: repoRoot || process.cwd(),
225
+ encoding: "utf-8",
226
+ }).trim();
227
+ return name || "lsctech";
228
+ }
229
+ catch {
230
+ return "lsctech";
231
+ }
232
+ }
233
+ const QC_RESOLUTION_OUTCOME_SET = new Set(types_js_1.QC_RESOLUTION_OUTCOMES);
234
+ /** Validate a resolution artifact object. */
235
+ function isValidQcResolutionArtifact(value) {
236
+ if (!value || typeof value !== "object")
237
+ return false;
238
+ const v = value;
239
+ if (typeof v.schemaVersion !== "string")
240
+ return false;
241
+ if (typeof v.clusterId !== "string")
242
+ return false;
243
+ if (typeof v.round !== "number")
244
+ return false;
245
+ if (typeof v.resolvedAt !== "string")
246
+ return false;
247
+ if (typeof v.resolver !== "string" || v.resolver === "")
248
+ return false;
249
+ if (typeof v.resolvedOutcome !== "string" ||
250
+ !QC_RESOLUTION_OUTCOME_SET.has(v.resolvedOutcome)) {
251
+ return false;
252
+ }
253
+ if (typeof v.reason !== "string" || v.reason.trim() === "")
254
+ return false;
255
+ if (!Array.isArray(v.findings) ||
256
+ v.findings.length === 0 ||
257
+ !v.findings.every((f) => typeof f === "string")) {
258
+ return false;
259
+ }
260
+ return true;
261
+ }
262
+ /** Read and validate a resolution artifact, if it exists. */
263
+ function readQcResolutionArtifact(clusterId, round, repoRoot) {
264
+ const artifactPath = getQcResolutionArtifactPath(clusterId, round, repoRoot);
265
+ try {
266
+ const data = (0, node_fs_1.readFileSync)(artifactPath, "utf-8");
267
+ const parsed = JSON.parse(data);
268
+ if (isValidQcResolutionArtifact(parsed)) {
269
+ // Verify identity matches expected clusterId and round
270
+ if (parsed.clusterId !== clusterId || parsed.round !== round) {
271
+ return null;
272
+ }
273
+ return parsed;
274
+ }
275
+ return null;
276
+ }
277
+ catch (error) {
278
+ if (error.code === "ENOENT") {
279
+ return null;
280
+ }
281
+ if (error instanceof SyntaxError) {
282
+ return null;
283
+ }
284
+ throw error;
285
+ }
286
+ }
287
+ /** Write a resolution artifact atomically and return its absolute path. */
288
+ function writeQcResolutionArtifact(options) {
289
+ const { clusterId, round, resolver, resolvedOutcome, reason, findings, repoRoot, } = options;
290
+ const artifactPath = getQcResolutionArtifactPath(clusterId, round, repoRoot);
291
+ const dir = (0, node_path_1.dirname)(artifactPath);
292
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
293
+ const artifact = {
294
+ schemaVersion: "1.0",
295
+ clusterId,
296
+ round,
297
+ resolvedAt: new Date().toISOString(),
298
+ resolver,
299
+ resolvedOutcome,
300
+ reason,
301
+ findings,
302
+ };
303
+ const tempPath = `${artifactPath}.tmp.${process.pid}.${Date.now()}.${Math.random()
304
+ .toString(36)
305
+ .slice(2, 8)}`;
306
+ try {
307
+ (0, node_fs_1.writeFileSync)(tempPath, JSON.stringify(artifact, null, 2), "utf-8");
308
+ (0, node_fs_1.renameSync)(tempPath, artifactPath);
309
+ }
310
+ catch (error) {
311
+ try {
312
+ (0, node_fs_1.unlinkSync)(tempPath);
313
+ }
314
+ catch {
315
+ // Ignore cleanup failure.
316
+ }
317
+ throw error;
318
+ }
319
+ return artifactPath;
320
+ }
192
321
  // ── Main repair loop ───────────────────────────────────────────────────────────
193
322
  /**
194
323
  * Run the bounded QC repair loop.
@@ -293,10 +422,21 @@ async function runQcRepairLoop(options) {
293
422
  else {
294
423
  const existingManifestPath = loopState.manifest_path ??
295
424
  (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId, "qc", "repair-rounds", String(round), "repair-packets.json");
425
+ // Synchronize manifest packet statuses with the persisted loop state.
426
+ const completedSet = new Set(loopState.completed_packet_ids);
427
+ for (const packet of manifest.packets) {
428
+ if (completedSet.has(packet.packetId)) {
429
+ packet.status = "completed";
430
+ }
431
+ }
432
+ (0, repair_packets_js_1.writeRepairPacketManifest)(manifest, repoRoot);
296
433
  loopState = {
297
434
  ...loopState,
298
435
  current_round: round,
299
436
  manifest_path: existingManifestPath,
437
+ pending_packet_ids: manifest.packets
438
+ .filter((p) => p.status === "pending" && !completedSet.has(p.packetId))
439
+ .map((p) => p.packetId),
300
440
  updated_at: new Date().toISOString(),
301
441
  };
302
442
  onStateUpdate?.(loopState);
@@ -367,10 +507,16 @@ async function runQcRepairLoop(options) {
367
507
  const result = await dispatchRepairWorkerWithTimeout(dispatchRepairWorker, pkt, round, manifest, repairDispatchTimeoutMs, telemetryFile, runId, clusterId);
368
508
  allWorkerResults.push(result);
369
509
  }
370
- // Record completed packet IDs.
510
+ // Record completed packet IDs and update manifest statuses.
371
511
  const completedIds = allWorkerResults
372
512
  .filter((r) => r.status !== "skipped")
373
513
  .map((r) => r.packetId);
514
+ for (const packet of manifest.packets) {
515
+ if (completedIds.includes(packet.packetId)) {
516
+ packet.status = "completed";
517
+ }
518
+ }
519
+ (0, repair_packets_js_1.writeRepairPacketManifest)(manifest, repoRoot);
374
520
  loopState = {
375
521
  ...loopState,
376
522
  completed_packet_ids: [...loopState.completed_packet_ids, ...completedIds],