@lsctech/polaris 0.5.8 → 0.5.10

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.
@@ -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: {
@@ -120,5 +123,6 @@ exports.DEFAULT_CONFIG = {
120
123
  },
121
124
  routes: {},
122
125
  maxRepairRounds: 2,
126
+ repairDispatchTimeoutMs: 1_800_000,
123
127
  },
124
128
  };
@@ -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)) {
@@ -7,6 +7,7 @@ exports.validateAuthoritativeChildState = validateAuthoritativeChildState;
7
7
  exports.runFinalize = runFinalize;
8
8
  exports.createFinalizeCommand = createFinalizeCommand;
9
9
  const commander_1 = require("commander");
10
+ const node_crypto_1 = require("node:crypto");
10
11
  const node_path_1 = require("node:path");
11
12
  const node_fs_1 = require("node:fs");
12
13
  const node_child_process_1 = require("node:child_process");
@@ -34,7 +35,12 @@ const local_graph_js_1 = require("../tracker/local-graph.js");
34
35
  const index_js_1 = require("../tracker/sync/index.js");
35
36
  const finalize_evidence_js_1 = require("../loop/finalize-evidence.js");
36
37
  const delivery_integrity_js_1 = require("./delivery-integrity.js");
38
+ const medic_gate_js_1 = require("./medic-gate.js");
39
+ const qc_escalation_js_1 = require("../run-health/qc-escalation.js");
37
40
  const index_js_2 = require("../qc/index.js");
41
+ const worker_packet_js_1 = require("../loop/worker-packet.js");
42
+ const registry_js_1 = require("../loop/adapters/registry.js");
43
+ const dispatch_js_1 = require("../loop/dispatch.js");
38
44
  function getBranch(repoRoot) {
39
45
  try {
40
46
  return (0, node_child_process_1.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
@@ -367,8 +373,190 @@ async function runQcGate(options) {
367
373
  process.stderr.write(`${stepLabel} QC ${trigger} blocked finalize: ${result.summary}\n`);
368
374
  process.exit(1);
369
375
  }
376
+ function parseWorkerSummary(summary) {
377
+ if (!summary)
378
+ return null;
379
+ try {
380
+ const parsed = JSON.parse(summary);
381
+ if (typeof parsed === "object" && parsed !== null) {
382
+ return parsed;
383
+ }
384
+ return null;
385
+ }
386
+ catch {
387
+ return null;
388
+ }
389
+ }
390
+ async function runCompletedClusterQcWithRepair(options) {
391
+ const { config, state, stateFile, repoRoot, branch, baseRef, stepLabel } = options;
392
+ const registry = (0, index_js_2.createQcRegistry)(config.qc);
393
+ const qcResult = await (0, index_js_2.runQcAtTrigger)({
394
+ config: config.qc,
395
+ registry,
396
+ trigger: "completed-cluster",
397
+ repoRoot,
398
+ runId: state.run_id,
399
+ clusterId: state.cluster_id,
400
+ branch,
401
+ baseRef,
402
+ telemetryFile: resolveQcTelemetryFile(state, repoRoot),
403
+ state,
404
+ });
405
+ if (qcResult.action === "pass") {
406
+ const maxRounds = config.qc.maxRepairRounds ?? index_js_2.DEFAULT_MAX_REPAIR_ROUNDS;
407
+ const sourceQcRunIds = qcResult.results.map((r) => r.qcRunId).filter(Boolean);
408
+ const existingLoop = state.qc_repair_loop;
409
+ const now = new Date().toISOString();
410
+ const passLoopState = existingLoop
411
+ ? {
412
+ ...existingLoop,
413
+ terminal_outcome: existingLoop.terminal_outcome ?? "pass",
414
+ updated_at: existingLoop.terminal_outcome == null ? now : (existingLoop.updated_at ?? now),
415
+ }
416
+ : {
417
+ ...(0, index_js_2.initRepairLoopState)({ maxRounds, sourceQcRunIds }),
418
+ terminal_outcome: "pass",
419
+ updated_at: now,
420
+ };
421
+ const passedState = { ...state, qc_repair_loop: passLoopState };
422
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, passedState);
423
+ console.log(`${stepLabel} QC completed-cluster passed: ${qcResult.summary}`);
424
+ return passedState;
425
+ }
426
+ // Append QC escalation symptoms for the initial non-passing completed-cluster QC result.
427
+ if (qcResult.results.length > 0) {
428
+ (0, qc_escalation_js_1.appendQcEscalationSymptoms)({
429
+ runId: state.run_id,
430
+ clusterId: state.cluster_id,
431
+ qcResults: qcResult.results,
432
+ afterRepair: false,
433
+ repoRoot,
434
+ });
435
+ }
436
+ const repairRouting = config.qc.repairRouting ?? "route";
437
+ if (repairRouting !== "route" && repairRouting !== "follow-up") {
438
+ process.stderr.write(`${stepLabel} QC completed-cluster blocked finalize: ${qcResult.summary}\n`);
439
+ process.exit(1);
440
+ }
441
+ const adapterName = config.execution?.adapter ?? "terminal-cli";
442
+ let providerName;
443
+ if (adapterName === "agent-subtask") {
444
+ providerName = "agent-subtask";
445
+ }
446
+ else {
447
+ try {
448
+ const providerEvidence = (0, dispatch_js_1.resolveProviderAndMode)({ stateFile, repoRoot }, "worker", config);
449
+ providerName = providerEvidence.provider ?? "default";
450
+ }
451
+ catch (err) {
452
+ const msg = err instanceof Error ? err.message : String(err);
453
+ process.stderr.write(`${stepLabel} QC repair loop provider resolution failed: ${msg}\n`);
454
+ process.exit(1);
455
+ }
456
+ }
457
+ const executionConfig = adapterName === "agent-subtask"
458
+ ? { ...(config.execution ?? { providers: {} }), adapter: "agent-subtask" }
459
+ : config.execution ?? { adapter: adapterName, providers: {} };
460
+ const adapter = (0, registry_js_1.createAdapter)(adapterName, executionConfig);
461
+ const maxConcurrentWorkers = config.execution?.routerPolicy?.defaultWorkerPool?.maxActiveWorkers ?? 1;
462
+ const telemetryFile = resolveQcTelemetryFile(state, repoRoot);
463
+ let nextState = state;
464
+ const repairDispatcher = async (packet, round, manifest, signal) => {
465
+ const dispatchId = (0, node_crypto_1.randomUUID)();
466
+ const repairResultPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", nextState.cluster_id, "results", `repair-${packet.packetId}-${dispatchId}.json`);
467
+ const workerPacket = (0, worker_packet_js_1.compileRepairWorkerPacket)({
468
+ runId: nextState.run_id,
469
+ clusterId: nextState.cluster_id,
470
+ packetId: packet.packetId,
471
+ branch,
472
+ stateFile,
473
+ telemetryFile,
474
+ round,
475
+ allowedScope: packet.allowedScope,
476
+ prohibitedScope: packet.prohibitedScope,
477
+ validationCommands: packet.validationCommands,
478
+ rootCauseHint: packet.rootCauseHint,
479
+ resultFile: repairResultPath,
480
+ maxConcurrentWorkers,
481
+ });
482
+ try {
483
+ // TODO: Propagate AbortSignal to adapter.dispatch() once ExecutionAdapter
484
+ // interface supports cancellation. Current limitation: timeout abandons the
485
+ // Promise but doesn't terminate the spawned worker process.
486
+ if (signal?.aborted) {
487
+ throw signal.reason || new Error("Repair dispatch aborted");
488
+ }
489
+ const dispatchResult = await adapter.dispatch(workerPacket, { provider: providerName });
490
+ const workerSummary = parseWorkerSummary(dispatchResult.summary);
491
+ const success = workerSummary?.status === "done";
492
+ const commitSha = workerSummary && typeof workerSummary["commit"] === "string"
493
+ ? workerSummary["commit"]
494
+ : undefined;
495
+ const errorMessage = workerSummary && typeof workerSummary["error_message"] === "string"
496
+ ? workerSummary["error_message"]
497
+ : "repair worker failed";
498
+ return {
499
+ packetId: packet.packetId,
500
+ status: success ? "success" : "failure",
501
+ commitSha,
502
+ errorMessage: success ? undefined : errorMessage,
503
+ };
504
+ }
505
+ catch (err) {
506
+ const msg = err instanceof Error ? err.message : String(err);
507
+ return {
508
+ packetId: packet.packetId,
509
+ status: "failure",
510
+ errorMessage: msg,
511
+ };
512
+ }
513
+ };
514
+ const repairLoopResult = await (0, index_js_2.runQcRepairLoop)({
515
+ clusterId: nextState.cluster_id,
516
+ runId: nextState.run_id,
517
+ branch,
518
+ repoRoot,
519
+ telemetryFile,
520
+ config: config.qc,
521
+ registry,
522
+ initialQcResults: qcResult.results,
523
+ dispatchRepairWorker: repairDispatcher,
524
+ maxRounds: config.qc.maxRepairRounds ?? index_js_2.DEFAULT_MAX_REPAIR_ROUNDS,
525
+ priorLoopState: nextState.qc_repair_loop ?? null,
526
+ onStateUpdate: (loopState) => {
527
+ nextState = { ...nextState, qc_repair_loop: loopState };
528
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, nextState);
529
+ },
530
+ });
531
+ nextState = { ...nextState, qc_repair_loop: repairLoopResult.loop_state };
532
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, nextState);
533
+ // Append QC escalation symptoms for non-passing repair-loop outcomes and post-repair findings.
534
+ (0, qc_escalation_js_1.appendRepairLoopOutcomeSymptom)({
535
+ runId: nextState.run_id,
536
+ clusterId: nextState.cluster_id,
537
+ repairResult: repairLoopResult,
538
+ repoRoot,
539
+ });
540
+ if (repairLoopResult.final_qc_results.length > 0 && repairLoopResult.rounds_completed > 0) {
541
+ (0, qc_escalation_js_1.appendQcEscalationSymptoms)({
542
+ runId: nextState.run_id,
543
+ clusterId: nextState.cluster_id,
544
+ qcResults: repairLoopResult.final_qc_results,
545
+ afterRepair: true,
546
+ repoRoot,
547
+ });
548
+ }
549
+ const repairLoopBlocker = validateQcRepairLoopGate(nextState, config.qc);
550
+ if (repairLoopBlocker) {
551
+ process.stderr.write(`${stepLabel} QC completed-cluster blocked finalize: ${qcResult.summary}\n` +
552
+ `${repairLoopBlocker}\n`);
553
+ process.exit(1);
554
+ }
555
+ console.log(`${stepLabel} QC completed-cluster repaired: ${repairLoopResult.summary}`);
556
+ return nextState;
557
+ }
370
558
  async function runFinalize(options) {
371
- const { repoRoot, stateFile, dryRun, skipDelivery, skipLibrarian } = options;
559
+ const { repoRoot, stateFile, dryRun, skipDelivery, skipLibrarian, bypassMedicReason } = options;
372
560
  const config = (0, loader_js_1.loadConfig)(repoRoot);
373
561
  // Step 1: polaris map update --changed
374
562
  console.log("[1/14] Updating map..."); // Step count updated
@@ -553,13 +741,13 @@ async function runFinalize(options) {
553
741
  const qcBaseRef = clusterStateForQc?.base_branch ??
554
742
  config.finalize?.targetBranch ??
555
743
  "main";
556
- await runQcGate({
557
- config: config.qc,
744
+ state = await runCompletedClusterQcWithRepair({
745
+ config,
558
746
  state,
747
+ stateFile: (0, node_path_1.resolve)(stateFile),
559
748
  repoRoot,
560
749
  branch,
561
750
  baseRef: qcBaseRef,
562
- trigger: "completed-cluster",
563
751
  stepLabel: "[5.8/14]",
564
752
  });
565
753
  // Step 5.9: QC repair-loop terminal state gate
@@ -580,6 +768,29 @@ async function runFinalize(options) {
580
768
  process.stderr.write(`finalize aborted: authoritative child state mismatch.\n${authChildResult.reason}\n`);
581
769
  process.exit(1);
582
770
  }
771
+ // Step 5.11: Run-health Medic gate
772
+ // After all worker/QC evidence is available and before any final commit, push,
773
+ // PR creation, or tracker update, require a Medic consultation decision for any
774
+ // run that recorded health symptoms.
775
+ {
776
+ console.log("[5.11/14] Checking run-health Medic gate...");
777
+ const bypassPolicy = config.finalize?.medic?.bypassPolicy ?? "none";
778
+ if (dryRun && bypassMedicReason && bypassPolicy === "cli") {
779
+ console.warn(`Dry run: would write Medic bypass to run-health report for run "${state.run_id}" (reason: ${bypassMedicReason}).`);
780
+ }
781
+ const medicBlocker = (0, medic_gate_js_1.validateMedicGate)({
782
+ runId: state.run_id,
783
+ repoRoot,
784
+ bypassReason: bypassMedicReason,
785
+ bypassPolicy,
786
+ dryRun,
787
+ });
788
+ if (medicBlocker) {
789
+ process.stderr.write(`finalize aborted: run-health Medic gate failed.\n${medicBlocker}\n`);
790
+ process.exit(1);
791
+ }
792
+ console.log("[5.11/14] Run-health Medic gate passed.");
793
+ }
583
794
  // Step 6: Tracker Reconciliation
584
795
  // LinearAdapter is sync-in only; only McpBridgeAdapter supports full reconciliation.
585
796
  const trackerType = config.tracker?.adapter;
@@ -715,11 +926,19 @@ function createFinalizeCommand(handlers = {}) {
715
926
  .option("--dry-run", "non-mutating preview: validate and generate report without committing or pushing")
716
927
  .option("--skip-delivery", "perform local finalize steps only; skip push/PR/Linear/archive")
717
928
  .option("--skip-librarian", "skip the Closeout Librarian gate (backward compatibility only)")
929
+ .option("--bypass-medic <reason>", "bypass the run-health Medic gate (requires finalize.medic.bypassPolicy=cli)")
718
930
  .action((options) => {
719
931
  const repoRoot = options.repoRoot;
720
932
  const stateFile = options.stateFile ??
721
933
  (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json");
722
- finalizeHandler({ repoRoot, stateFile, dryRun: options.dryRun, skipDelivery: options.skipDelivery, skipLibrarian: options.skipLibrarian })
934
+ finalizeHandler({
935
+ repoRoot,
936
+ stateFile,
937
+ dryRun: options.dryRun,
938
+ skipDelivery: options.skipDelivery,
939
+ skipLibrarian: options.skipLibrarian,
940
+ bypassMedicReason: options.bypassMedic,
941
+ })
723
942
  .catch((err) => {
724
943
  process.stderr.write(`finalize error: ${err instanceof Error ? err.message : String(err)}\n`);
725
944
  process.exit(1);
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateMedicGate = validateMedicGate;
4
+ const index_js_1 = require("../run-health/index.js");
5
+ /**
6
+ * Validates that a run with a run-health report either has a Medic decision or
7
+ * an explicit, allowed policy bypass. Returns `null` when finalize may proceed;
8
+ * returns a human-readable blocker string otherwise.
9
+ *
10
+ * Fails closed: a report that exists but lacks a Medic decision/bypass blocks
11
+ * final commit, push, PR creation, and tracker update.
12
+ */
13
+ function validateMedicGate(options) {
14
+ const { runId, repoRoot, bypassReason, bypassPolicy = "none", dryRun = false } = options;
15
+ const reportPath = (0, index_js_1.getRunHealthReportPath)(runId, repoRoot);
16
+ const report = (0, index_js_1.readRunHealthReport)(runId, repoRoot);
17
+ // No report means no symptoms were recorded — gate does not apply.
18
+ if (!report)
19
+ return null;
20
+ if ((0, index_js_1.isMedicGateSatisfied)(report))
21
+ return null;
22
+ if (bypassReason) {
23
+ if (bypassPolicy !== "cli") {
24
+ return (`Run-health report at ${reportPath} requires Medic consultation, but a CLI bypass is not allowed ` +
25
+ `by finalize.medic.bypassPolicy (current: "${bypassPolicy}").`);
26
+ }
27
+ if (!dryRun) {
28
+ (0, index_js_1.markBypassed)(runId, {
29
+ reason: bypassReason,
30
+ bypassed_by: process.env.USER ?? process.env.LOGNAME ?? "operator",
31
+ bypassed_at: new Date().toISOString(),
32
+ }, repoRoot);
33
+ }
34
+ return null;
35
+ }
36
+ const bypassHint = bypassPolicy === "cli"
37
+ ? `To bypass this gate, use --bypass-medic "<reason>".`
38
+ : `Bypass is not enabled by finalize.medic.bypassPolicy (current: "${bypassPolicy}").`;
39
+ return (`Run-health report at ${reportPath} has recorded symptoms and requires a Medic consultation decision ` +
40
+ `(medic_consult.status "resolved" or "bypassed", or an explicit policy_bypass) before finalize can commit, push, ` +
41
+ `create a PR, or update the tracker.\n${bypassHint}`);
42
+ }
@@ -169,10 +169,11 @@ class TerminalCliAdapter {
169
169
  const primaryProvider = options.provider || "terminal-cli";
170
170
  const routerEvidence = options.routerDecision;
171
171
  const providerAttempts = [];
172
- if ((0, worker_packet_js_1.isWorkerPacket)(packet) && packet.worker_role === "impl") {
172
+ if ((0, worker_packet_js_1.isWorkerPacket)(packet) &&
173
+ (packet.worker_role === "impl" || packet.worker_role === "repair")) {
173
174
  const allowed = Array.isArray(packet.instructions?.allowed_scope) ? packet.instructions.allowed_scope : [];
174
175
  if (allowed.length === 0) {
175
- const blockedMsg = `Worker blocked: impl packet for ${packet.active_child} has empty allowed_scope. Foreman must provide scope or approve override.`;
176
+ const blockedMsg = `Worker blocked: ${packet.worker_role} packet for ${packet.active_child} has empty allowed_scope. Foreman must provide scope or approve override.`;
176
177
  return {
177
178
  exit_code: 1,
178
179
  provider_used: primaryProvider,