tickmarkr 2.6.2 → 2.6.4

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 (79) hide show
  1. package/README.md +12 -2
  2. package/dist/adapters/codex.d.ts +2 -1
  3. package/dist/adapters/codex.js +26 -5
  4. package/dist/adapters/model-lints.d.ts +1 -0
  5. package/dist/adapters/model-lints.js +48 -1
  6. package/dist/adapters/model-windows.d.ts +2 -1
  7. package/dist/adapters/model-windows.js +18 -7
  8. package/dist/adapters/types.d.ts +6 -0
  9. package/dist/cli/commands/approve.d.ts +6 -3
  10. package/dist/cli/commands/approve.js +43 -6
  11. package/dist/cli/commands/doctor.d.ts +8 -2
  12. package/dist/cli/commands/doctor.js +6 -1
  13. package/dist/cli/commands/fleet.js +264 -59
  14. package/dist/cli/commands/init.js +5 -3
  15. package/dist/cli/commands/plan.js +13 -8
  16. package/dist/cli/commands/report.d.ts +40 -2
  17. package/dist/cli/commands/report.js +278 -11
  18. package/dist/cli/commands/resume.js +2 -4
  19. package/dist/cli/commands/run.d.ts +11 -0
  20. package/dist/cli/commands/run.js +33 -5
  21. package/dist/cli/commands/status.js +49 -4
  22. package/dist/cli/commands/verify.js +332 -121
  23. package/dist/compile/native.js +137 -0
  24. package/dist/config/config.d.ts +40 -9
  25. package/dist/config/config.js +133 -15
  26. package/dist/config/fleet-overlay.d.ts +13 -3
  27. package/dist/config/fleet-overlay.js +12 -8
  28. package/dist/drivers/herdr.d.ts +12 -0
  29. package/dist/drivers/herdr.js +51 -0
  30. package/dist/drivers/orca.d.ts +9 -1
  31. package/dist/drivers/orca.js +29 -7
  32. package/dist/drivers/types.d.ts +2 -0
  33. package/dist/drivers/types.js +2 -2
  34. package/dist/gates/acceptance.d.ts +7 -0
  35. package/dist/gates/acceptance.js +27 -5
  36. package/dist/gates/baseline.d.ts +20 -1
  37. package/dist/gates/baseline.js +100 -20
  38. package/dist/gates/cache.d.ts +8 -0
  39. package/dist/gates/cache.js +12 -2
  40. package/dist/gates/llm.d.ts +6 -0
  41. package/dist/gates/llm.js +27 -8
  42. package/dist/gates/review.d.ts +6 -1
  43. package/dist/gates/review.js +122 -32
  44. package/dist/gates/run-gates.d.ts +54 -3
  45. package/dist/gates/run-gates.js +331 -45
  46. package/dist/gates/test-manifest.d.ts +42 -0
  47. package/dist/gates/test-manifest.js +69 -10
  48. package/dist/route/router.d.ts +23 -1
  49. package/dist/route/router.js +54 -16
  50. package/dist/run/consult.d.ts +3 -1
  51. package/dist/run/consult.js +4 -2
  52. package/dist/run/daemon.d.ts +2 -1
  53. package/dist/run/daemon.js +349 -79
  54. package/dist/run/interactive-seed.d.ts +4 -0
  55. package/dist/run/interactive-seed.js +35 -9
  56. package/dist/run/journal.d.ts +123 -1
  57. package/dist/run/journal.js +480 -17
  58. package/dist/run/lease.d.ts +13 -0
  59. package/dist/run/lease.js +45 -0
  60. package/dist/run/protocol.d.ts +15 -0
  61. package/dist/run/protocol.js +11 -1
  62. package/dist/run/receipt-resolver.d.ts +22 -0
  63. package/dist/run/receipt-resolver.js +40 -1
  64. package/dist/run/repair-selection.d.ts +11 -1
  65. package/dist/run/repair-selection.js +17 -9
  66. package/dist/run/wall-budget.d.ts +48 -0
  67. package/dist/run/wall-budget.js +280 -0
  68. package/dist/tui/cockpit/live-store.d.ts +6 -0
  69. package/dist/tui/cockpit/live-store.js +36 -11
  70. package/dist/tui/cockpit/run-cockpit.js +2 -2
  71. package/dist/tui/cockpit/run-view.d.ts +2 -1
  72. package/dist/tui/cockpit/run-view.js +13 -9
  73. package/dist/tui/cockpit/setup-cockpit.d.ts +2 -0
  74. package/dist/tui/cockpit/setup-cockpit.js +4 -0
  75. package/package.json +2 -1
  76. package/schema/config.schema.json +8 -1
  77. package/skills/tickmarkr-auto/SKILL.md +2 -2
  78. package/skills/tickmarkr-loop/SKILL.md +17 -5
  79. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +4 -1
@@ -1,5 +1,5 @@
1
1
  import { HOST_PROBE_SAMPLE_MS, HOST_PROBE_SAMPLES, HostDegradedError, hostDegraded, observeHost } from "./host-health.js";
2
- import { VITEST_CACHE_ENV, worktreeVitestCache } from "../gates/test-manifest.js";
2
+ import { runWithInterruptionSink, VITEST_CACHE_ENV, worktreeVitestCache } from "../gates/test-manifest.js";
3
3
  import { COMMAND_LEASE_TOKEN_ENV, commandLeaseEnvironment, CommandLeases, currentCommandLeaseToken, isRunnerCommand, runWithCommandLease, withCommandLease } from "./lease.js";
4
4
  import { execFileSync, spawn } from "node:child_process";
5
5
  import { createHash, randomBytes } from "node:crypto";
@@ -21,7 +21,7 @@ import { driverEvidence } from "../drivers/index.js";
21
21
  import { herdrSealShellPrefix, MAX_BUF, SubprocessDriver } from "../drivers/subprocess.js";
22
22
  import { formatOwnedName } from "../drivers/types.js";
23
23
  import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
24
- import { runGates } from "../gates/run-gates.js";
24
+ import { reobserveTestFiles, runGates } from "../gates/run-gates.js";
25
25
  import { isInfraResult } from "../gates/cache.js";
26
26
  import { filesGlob } from "../graph/files-glob.js";
27
27
  import { addEvidence, attributeBlocked, batteryPriority, blockedTasks, getTask, graphDefinitionHash, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus, taskContentDigest, tickmarkrDir } from "../graph/graph.js";
@@ -29,17 +29,17 @@ import { GATE_NAMES } from "../graph/schema.js";
29
29
  import { distFingerprint } from "../cli/commands/version.js";
30
30
  import { augmentRetryBrief, consult, renderRetryGuidance } from "./consult.js";
31
31
  import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExecutionBudget, ExecutionBudgetExceeded } from "./execution-budget.js";
32
- import { repairSelectionDecision } from "./repair-selection.js";
32
+ import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
33
33
  import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
34
34
  import { runEnvironment } from "./environment.js";
35
35
  import { changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
36
36
  import { runInteractiveSeed } from "./interactive-seed.js";
37
37
  import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
38
- import { applyScopeAmendments, activeRetryBan, interruptedAttempt, RESUME_HARVEST_SOURCE, resumeHarvestAuthor, approvalAction, APPROVAL_REFUSED, bindingToken, effectiveEvents, foldDecisions, physicalLine, staleApprovals, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingApprovalActions, pendingRechecks, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, standingRulings, structuredFindings, upheldFeedbackByTask } from "./journal.js";
39
- import { gateReviewerFloor, isDiffCapPark, pickReviewer } from "../gates/review.js";
38
+ import { applyScopeAmendments, activeRetryBan, interruptedAttempt, RESUME_HARVEST_SOURCE, resumeHarvestAuthor, approvalAction, APPROVAL_REFUSED, bindingToken, effectiveEvents, foldDecisions, physicalLine, staleApprovals, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, readableExcerpt, receiptOrigin, outstandingReviewFindings, pendingApprovalActions, pendingRechecks, pendingRepairFindings, phaseForGate, priorJudgments, readPriorRunEvidence, recordedTaskFailureKind, reraisedReviewChains, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, standingRulings, structuredFindings, upheldFeedbackByTask } from "./journal.js";
39
+ import { gateReviewerFloor, isDiffCapPark, isGarbageReview, pickReviewer } from "../gates/review.js";
40
40
  import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
41
41
  import { ensureIntegration, integrationBranch, integrationHead, mergeTask, reusedTipEvidence, verifyIntegrationTip } from "./merge.js";
42
- import { climbChannel, marginalCostRank, nextChannel, route } from "../route/router.js";
42
+ import { channelExclusion, climbChannel, inTaskPool, marginalCostRank, nextChannel, poolExhaustion, route, taskPool } from "../route/router.js";
43
43
  import { desiredPanes } from "./reconcile.js";
44
44
  import { readTierLiveness, readWatchBoard, supervisionPresencePath } from "./supervision.js";
45
45
  import { harvestCpuFlatWindowMs, NUDGEABLE_ADAPTERS, PANE_READ_ROWS, StallProgressTracker, stallSnapshotBannerRows, WorkerTreeCpuAccountant, reapOwnedProcessGroup, readOwnedProcessGroup, } from "./stall.js";
@@ -370,6 +370,30 @@ function classifyInfraResult(g) {
370
370
  return;
371
371
  g.meta = { ...g.meta, classification: "infra", infra: true };
372
372
  }
373
+ // OBS-1106 residual: a red whose assertion is a timeout or a wall-clock budget, and the runner-level
374
+ // diagnostics (never-started files, a worker RPC timeout) that make such a red infrastructure-SHAPED.
375
+ // Shape alone never parks anything: it only buys one isolated re-observation before a charge. A
376
+ // fresh-fingerprint section masks digits to `#`, so both spellings of a number are read.
377
+ const TIMEOUT_SHAPED_RE = /\b(?:Test|Hook) timed out in (?:\d+|#)\s*ms\b|\bexpected (?:\d+|#)(?:\.(?:\d+|#))? to be (?:less than|below)(?: or equal to)? (?:\d+|#)/i;
378
+ const RUNNER_INFRA_DIAGNOSTIC_RE = /\bnever-started (?:[1-9]\d*|#)(?![\d#])|\[vitest-worker\]: Timeout calling\b/;
379
+ const timeoutShaped = (details) => TIMEOUT_SHAPED_RE.test(readableExcerpt(details));
380
+ const ATTRIBUTED_FILE_RE = /^\s*(?:FAIL|×|✗)\s+(?:\|[^|\n]*\|\s+)?(\S+\.(?:test|spec)\.[cm]?[jt]sx?)\b/gm;
381
+ /** The failing files a red attributes to itself: the runner's own list, else its FAIL headlines. */
382
+ const attributedFailingFiles = (g) => [...new Set(Array.isArray(g.meta?.failingFiles) && g.meta.failingFiles.length
383
+ ? g.meta.failingFiles.filter((f) => typeof f === "string")
384
+ : [...readableExcerpt(g.details).matchAll(ATTRIBUTED_FILE_RE)].map((m) => m[1].replace(/^\.\//, "")))].sort();
385
+ const metaFiles = (v) => Array.isArray(v) ? v.filter((f) => typeof f === "string") : undefined;
386
+ /** D-607: the files a rerun OBSERVABLY ran — the runner's manifest, the only complete selection
387
+ * evidence. Never the requested selection, and never the per-file lines a runner printed: a runner that
388
+ * ignores file arguments and reports only its failures looks isolated while it ran the whole suite.
389
+ * Undefined without a manifest, and the rerun is then ambiguous. */
390
+ const observedSelection = (g) => {
391
+ const ran = metaFiles(g.meta?.manifest);
392
+ return ran?.length ? [...new Set(ran)].sort() : undefined;
393
+ };
394
+ /** D-607: on the manifest path only the reporter's own attribution counts — a `FAIL path` line inside
395
+ * captured stdout is text, not attribution. Headline lexing is for runners without a reporter. */
396
+ const rerunFailingFiles = (g) => metaFiles(g.meta?.manifest) ? [...new Set(metaFiles(g.meta?.failingFiles) ?? [])].sort() : attributedFailingFiles(g);
373
397
  // v1.85 T3: the gates whose failure IS a deterministic measurement — a machine re-ran a command over a
374
398
  // tree and printed the same bytes. Those are the failures the fingerprint cap governs (the ruling names
375
399
  // it a "deterministic-gate" cap): a third identical answer to a question already answered twice is the
@@ -460,18 +484,36 @@ function repairDiffFilesCut(diff, cap, names) {
460
484
  function repairFindingsBrief(results) {
461
485
  return results.filter(gateFailed).map((g) => {
462
486
  const evidence = g.meta?.failureEvidence;
463
- return `${g.gate}: ${g.details}` + (Array.isArray(evidence) && evidence.length
487
+ return `${g.gate}: ${readableExcerpt(g.details)}` + (Array.isArray(evidence) && evidence.length
464
488
  ? `\nAssertion evidence:\n${JSON.stringify(evidence, null, 2)}` : "");
465
489
  }).join("\n\n");
466
490
  }
467
- /** The fix-only contract: the findings verbatim, then the diff content of the work already landed. */
468
- function repairBrief(findings, diff, baseRef) {
491
+ // OBS-1019 add.2: the order a repeated review chain earns in place of the smallest-edit order.
492
+ const REPAIR_CLASS_SENTENCE = "Before editing a chain reraised more than once, enumerate the in-scope consumers,"
493
+ + " bridges, operations and event sequences as a closed case table; repair every implicated member and verify"
494
+ + " the accumulated diff against that table.";
495
+ /**
496
+ * The fix-only contract: the findings verbatim, then the diff content of the work already landed.
497
+ * OBS-1019 add.2: a first finding stays bounded to the smallest edit. A chain a review has re-raised
498
+ * more than once was chased edge by edge while every round re-judged the accumulated diff, so it owes
499
+ * the whole class instead: a closed case table inside the declared bounds, never beyond them.
500
+ */
501
+ function repairBrief(findings, diff, baseRef, repeated = []) {
469
502
  const repair = { findings, diff };
470
503
  return [
471
- "## Repair attempt — fix ONLY what these findings name",
504
+ repeated.length ? "## Repair attempt — repair the whole class these findings name" : "## Repair attempt — fix ONLY what these findings name",
472
505
  "The commits from your prior attempt are already in this worktree and their diff is reproduced"
473
- + " below. Do NOT re-implement that work, do not start over, and do not revert it: make the"
474
- + " smallest change that resolves every finding, then commit.",
506
+ + " below. Do NOT re-implement that work, do not start over, and do not revert it: "
507
+ + (repeated.length ? "resolve every member of the case table below, then commit." : "make the smallest change that resolves every finding, then commit."),
508
+ ...(repeated.length ? [
509
+ "",
510
+ "### Repeated review chains — repair the class, not the next edge",
511
+ // Fingerprint and count only: the finding's prose is already quoted once, below or under its heading.
512
+ ...repeated.map(({ finding, reraises }) => `- \`${finding.fingerprint}\` re-raised ${reraises} times`),
513
+ REPAIR_CLASS_SENTENCE,
514
+ "The table stays inside the task's declared write scope (files[]) and never narrows its goal or an"
515
+ + " acceptance criterion; the reviewer judges the accumulated diff, not the table.",
516
+ ] : []),
475
517
  "",
476
518
  "### Failing gate findings (verbatim)",
477
519
  repair.findings,
@@ -881,7 +923,7 @@ export function formatTipProof(p) {
881
923
  }
882
924
  // Isolate the battery so cancellation can stop its control flow as well as its shell children.
883
925
  // The child uses the same verifier and capacity; only the daemon writes lifecycle verdict rows.
884
- async function cancellableTipBattery(intWt, commands, runDir, baseline, signal) {
926
+ async function cancellableTipBattery(intWt, commands, runDir, baseline, signal, evidence = {}) {
885
927
  signal.throwIfAborted();
886
928
  const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
887
929
  const script = `
@@ -930,9 +972,9 @@ async function cancellableTipBattery(intWt, commands, runDir, baseline, signal)
930
972
  finally { process.send({ type: 'lease-release', id }); }
931
973
  };
932
974
  let input = ''; for await (const chunk of process.stdin) input += chunk;
933
- const { intWt, commands, runDir, baseline, capacity } = JSON.parse(input);
975
+ const { intWt, commands, runDir, baseline, capacity, evidence } = JSON.parse(input);
934
976
  try {
935
- const result = await runWithVerificationBudget(capacity, () => runWithCommandLease(lease, () => verifyIntegrationTip(intWt, commands, runDir, baseline)));
977
+ const result = await runWithVerificationBudget(capacity, () => runWithCommandLease(lease, () => verifyIntegrationTip(intWt, commands, runDir, baseline, evidence)));
936
978
  if (!cancelled) process.stdout.write(JSON.stringify(result.map(row => ({ ...row, capacity: [...commandCapacities].reverse().find(([command]) => command === row.cmd || command.startsWith(row.cmd + " "))?.[1] ?? capacity }))));
937
979
  } catch (error) { if (!cancelled) throw error; }
938
980
  finally { process.disconnect(); }
@@ -983,7 +1025,7 @@ async function cancellableTipBattery(intWt, commands, runDir, baseline, signal)
983
1025
  });
984
1026
  signal.addEventListener("abort", cancel, { once: true });
985
1027
  child.stdin.on("error", () => { });
986
- child.stdin.end(JSON.stringify({ intWt, commands, runDir, baseline, capacity: resolvedCapacity() }));
1028
+ child.stdin.end(JSON.stringify({ intWt, commands, runDir, baseline, capacity: resolvedCapacity(), evidence }));
987
1029
  try {
988
1030
  if (signal.aborted)
989
1031
  cancel();
@@ -1044,9 +1086,9 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
1044
1086
  return false;
1045
1087
  }
1046
1088
  let tipFailed = false;
1047
- for (const r of await (opts.signal
1048
- ? cancellableTipBattery(intWt, commands, journal.dir, opts.baseline, opts.signal)
1049
- : verifyIntegrationTip(intWt, commands, journal.dir, opts.baseline))) {
1089
+ for (const r of await runWithInterruptionSink(journalInterruption(journal, undefined), () => opts.signal
1090
+ ? cancellableTipBattery(intWt, commands, journal.dir, opts.baseline, opts.signal, opts.evidence)
1091
+ : verifyIntegrationTip(intWt, commands, journal.dir, opts.baseline, opts.evidence))) {
1050
1092
  const measuredCapacity = r.capacity ?? capacity;
1051
1093
  const evidence = r.reused ? reusedTipEvidence(r) : {
1052
1094
  ...(r.originRunRoot ? { originRunRoot: r.originRunRoot } : {}),
@@ -1080,6 +1122,12 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
1080
1122
  }
1081
1123
  return tipFailed;
1082
1124
  }
1125
+ /** OBS-953: a detected wall-over-monotonic discontinuity journals `host-suspend` (its offset was
1126
+ * subtracted from active test time); an overdue poll whose clocks advanced together journals
1127
+ * `host-poll-overdue` (recorded, never subtracted). */
1128
+ const journalInterruption = (journal, taskId) => (interruption) => {
1129
+ journal.append(interruption.kind === "host-suspend" ? "host-suspend" : "host-poll-overdue", taskId, { ...interruption });
1130
+ };
1083
1131
  async function commitsAheadOfRef(base, head, cwd) {
1084
1132
  if (head === base)
1085
1133
  return [];
@@ -1904,6 +1952,9 @@ export async function runDaemon(repoRoot, opts = {}) {
1904
1952
  // > repo > global > default. The resolved cfg carries mode-compiled floors; route() never sees the mode.
1905
1953
  const rm = resolveRunMode(repoRoot, { flag: opts.mode, spec: graph.mode, globalDir: opts.globalDir });
1906
1954
  const cfg = rm.cfg;
1955
+ // OBS-1199: the effective repair-selection default is resolved apart from the optional execution
1956
+ // budget, so an absent policy selects on every driver; a resume keeps what the run recorded.
1957
+ let repairSelection = repairSelectionEnabled(cfg);
1907
1958
  if (opts.resume) {
1908
1959
  const start = Journal.open(repoRoot, runId).read().find((e) => e.event === "run-start");
1909
1960
  const snapshot = start?.data.effectivePolicy;
@@ -1915,6 +1966,11 @@ export async function runDaemon(repoRoot, opts = {}) {
1915
1966
  // Historical runs did not opt in; current settings cannot grant them a new allowance.
1916
1967
  cfg.executionPolicy = recorded?.config?.executionPolicy === undefined
1917
1968
  ? undefined : ExecutionPolicySchema.parse(recorded.config.executionPolicy);
1969
+ const recordedGates = recorded?.config?.gates;
1970
+ if (recordedGates?.repairSelection !== undefined && typeof recordedGates.repairSelection !== "boolean") {
1971
+ throw new Error("cannot restore malformed recorded repair selection");
1972
+ }
1973
+ repairSelection = repairSelectionEnabled({ executionPolicy: cfg.executionPolicy, gates: recordedGates });
1918
1974
  }
1919
1975
  if (cfg.executionPolicy && driver.id !== "subprocess") {
1920
1976
  throw new Error("experimental executionPolicy currently requires the subprocess driver");
@@ -2366,6 +2422,19 @@ export async function runDaemon(repoRoot, opts = {}) {
2366
2422
  void promise.finally(() => hostChecks.delete(promise)).catch(() => { });
2367
2423
  return promise;
2368
2424
  };
2425
+ // OBS-1190: every admission poll still probes (the probe gates admission), but the journal keeps only
2426
+ // the first observation and each change of state or reference — healthy, degraded, unreadable,
2427
+ // recovered — the way suite-wait already dedupes by count. A steady wait no longer floods the journal.
2428
+ let journaledHost;
2429
+ const journalObservation = (taskId, observation, resuming) => {
2430
+ const state = observation.medianMs === null ? "unreadable"
2431
+ : hostReferenceMs !== undefined && hostDegraded(observation, hostReferenceMs) ? "degraded" : "healthy";
2432
+ const key = `${state}:${hostReferenceMs ?? ""}`;
2433
+ if (key === journaledHost)
2434
+ return;
2435
+ journaledHost = key;
2436
+ journal.append("host-observation", taskId, { ...observation, referenceMs: hostReferenceMs ?? null, resuming, state });
2437
+ };
2369
2438
  const recordReference = (observation) => {
2370
2439
  if (observation.medianMs === null)
2371
2440
  return;
@@ -2389,7 +2458,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2389
2458
  const probeBudget = HOST_PROBE_SAMPLE_MS * HOST_PROBE_SAMPLES;
2390
2459
  if (!observation || (!first && remaining >= probeBudget)) {
2391
2460
  observation = await observeHost(signal, remaining > 0 ? Math.min(probeBudget, remaining) : undefined);
2392
- journal.append("host-observation", taskId, { ...observation, referenceMs: hostReferenceMs ?? null, resuming });
2461
+ journalObservation(taskId, observation, resuming);
2393
2462
  }
2394
2463
  first = false;
2395
2464
  if (hostReferenceMs === undefined && observation.medianMs !== null)
@@ -2433,7 +2502,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2433
2502
  const initializeHost = () => trackHost((async () => {
2434
2503
  const signal = hostSignal();
2435
2504
  const observation = await observeHost(signal);
2436
- journal.append("host-observation", undefined, { ...observation, referenceMs: hostReferenceMs ?? null, resuming: !!opts.resume });
2505
+ journalObservation(undefined, observation, !!opts.resume);
2437
2506
  if (hostReferenceMs === undefined)
2438
2507
  recordReference(observation);
2439
2508
  if (opts.resume || observation.medianMs === null)
@@ -2479,7 +2548,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2479
2548
  waited = true;
2480
2549
  journal.append("suite-wait", taskId, { count, ...(gate ? { gate } : {}) });
2481
2550
  }, SUITE_POLL_MS, signal);
2482
- }, async () => {
2551
+ }, () => runWithInterruptionSink(journalInterruption(journal, taskId), async () => {
2483
2552
  try {
2484
2553
  const result = await run();
2485
2554
  // Some command oracles turn launch errors into results. Admission failure still parks infra.
@@ -2491,7 +2560,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2491
2560
  if (taskId)
2492
2561
  activeGatePhases.delete(taskId);
2493
2562
  }
2494
- });
2563
+ }));
2495
2564
  };
2496
2565
  let baseRef;
2497
2566
  let baseline;
@@ -2710,24 +2779,39 @@ export async function runDaemon(repoRoot, opts = {}) {
2710
2779
  // owns listing/parsing/closing. Cosmetic by contract: failures are swallowed and subprocess has no
2711
2780
  // reconcile (optional chain → no-op), so gates and the oracle suite never feel this. keepPanes
2712
2781
  // "forever" is the keep-everything debug override — it disables the sweep entirely.
2713
- const resuming = !!opts.resume;
2782
+ // OBS-1109: an interrupted attempt's pane is evidence — its nonce-bound trailer decides whether
2783
+ // the attempt is harvested, declined as foreign, or recovered — so the fold's run-resume clear
2784
+ // must not reach it before harvestInterruptedAttempt has read it and reaped its processes. Only a
2785
+ // resumed daemon can hold such an attempt: a fresh run's sweep is the baseline sweep.
2786
+ // OBS-1204: the set is FROZEN here, once, from the rows up to this daemon's own run-resume row: an
2787
+ // attempt launched after it is this daemon's own and reconciles exactly as in a fresh run. Each
2788
+ // frozen spare lapses for good once its harvest, decline or a superseding dispatch is journaled.
2789
+ const resumeInterrupted = new Map();
2790
+ if (opts.resume) {
2791
+ const rows = journal.read();
2792
+ let resumedAt = rows.length - 1;
2793
+ while (resumedAt >= 0 && rows[resumedAt].event !== "run-resume")
2794
+ resumedAt--;
2795
+ const prior = rows.slice(0, resumedAt + 1);
2796
+ for (const t of graph.tasks) {
2797
+ const owned = interruptedAttempt(prior, t.id);
2798
+ if (owned?.launch)
2799
+ resumeInterrupted.set(t.id, { attempt: owned.attempt, nonce: owned.launch.nonce });
2800
+ }
2801
+ }
2714
2802
  const reconcile = async (sweep) => {
2715
2803
  if (keepForever)
2716
2804
  return;
2717
2805
  try {
2718
2806
  const rows = journal.read();
2719
2807
  const desired = desiredPanes(rows, runId);
2720
- // OBS-1109: an interrupted attempt's pane is evidence — its nonce-bound trailer decides whether
2721
- // the attempt is harvested, declined as foreign, or recovered — so the fold's run-resume clear
2722
- // must not reach it before harvestInterruptedAttempt has read it and reaped its processes. Once
2723
- // that harvest (or a superseding dispatch) is journaled the fold no longer names it: swept then.
2724
- // Only a resumed daemon can hold such an attempt: a fresh run's sweep is the baseline sweep.
2725
- if (resuming)
2726
- for (const t of graph.tasks) {
2727
- const owned = interruptedAttempt(rows, t.id);
2728
- if (owned?.launch)
2729
- desired.add(formatOwnedName({ role: "worker", taskId: t.id, attempt: owned.attempt, runId }));
2730
- }
2808
+ for (const [taskId, frozen] of resumeInterrupted) {
2809
+ const owned = interruptedAttempt(rows, taskId);
2810
+ if (owned?.launch?.nonce !== frozen.nonce)
2811
+ resumeInterrupted.delete(taskId);
2812
+ else
2813
+ desired.add(formatOwnedName({ role: "worker", taskId, attempt: frozen.attempt, runId }));
2814
+ }
2731
2815
  // The watch pane is never the DRIVER sweep's candidate (panesToClose spares role "watch":
2732
2816
  // herdr's watches bookkeeping lives in close(), and a raw pane-close in the sweep would
2733
2817
  // leave narrator() a stale cache) — the driver always sees it as desired; its lifecycle is
@@ -2835,6 +2919,80 @@ export async function runDaemon(repoRoot, opts = {}) {
2835
2919
  await reconcile({ spareLiveLlm: true }); // task-human is a terminal event — sweep, sparing sibling tasks' live LLM panes
2836
2920
  await driver.notify(`tickmarkr ${runId}: ${t.id} needs a human — ${reason}${token ? `\npark ${token} — bind the decision with \`--park ${token}\`` : ""}`, { tier: "attention" });
2837
2921
  };
2922
+ // OBS-1106 residual: a retry that landed nothing on a timeout-class red re-observes it ONCE fresh:
2923
+ // neither a journal replay nor the verdict cache may re-state that red. Owed when the prior round's
2924
+ // test red sits on this very subject, is timeout-shaped, and has not yet been observed twice
2925
+ // independently — once it has, the fingerprint cap owns the next move. Read from the journal alone,
2926
+ // so the live gate loop and a resume battery decide alike.
2927
+ const owesTimeoutRefresh = (t, priorRows, commit) => {
2928
+ const red = priorRows.find((e) => e.event === "gate-result" && e.data.gate === "test" && e.data.pass === false
2929
+ && e.data.skipped !== true && e.data.commit === commit && typeof e.data.details === "string" && timeoutShaped(e.data.details));
2930
+ return red !== undefined
2931
+ && identicalGateFailures(journal.read(), t.id, "test", normalizeGateFailure(String(red.data.details))) < GATE_FINGERPRINT_CAP;
2932
+ };
2933
+ // OBS-1106 residual: before a timeout or wall-budget red accompanied by runner infra diagnostics is
2934
+ // charged, its attributed failing files are re-run ONCE, on the same checkout, under the suite
2935
+ // lease. Only a reproduced assertion is charged. A pass, an infra rerun, a rerun naming none of the
2936
+ // files, or an unattributable red parks with the ORIGINAL red journaled and unresolved: the rerun is
2937
+ // diagnostic, never a verdict, so it can neither green the gate nor permit a merge. Returns true
2938
+ // when it parked.
2939
+ const adjudicateInfraShapedRed = async (t, results, wt, subject, attempt, replayed, parkInfra) => {
2940
+ const red = results.find((g) => g.gate === "test" && gateFailed(g) && !isInfraResult(g)
2941
+ && timeoutShaped(g.details) && RUNNER_INFRA_DIAGNOSTIC_RE.test(g.details));
2942
+ if (!red || commands.test === undefined)
2943
+ return false;
2944
+ // The observation this red states: its subject, its fingerprint and, when it carries one, its
2945
+ // receipt's invocation. A copy (journal replay, cache hit) is never re-run when THAT observation's
2946
+ // adjudication is on the ledger — its recorded outcome decides, so a crash after the adjudication
2947
+ // but before its park still parks. A crash before the adjudication leaves none, so the copy is
2948
+ // adjudicated now. A fresh red always is.
2949
+ const fingerprint = normalizeGateFailure(red.details);
2950
+ const observation = receiptOrigin({ evidenceReceipt: red.evidenceReceipt ?? red.meta?.evidenceReceipt,
2951
+ evidenceReceipts: red.evidenceReceipts ?? red.meta?.evidenceReceipts });
2952
+ const recorded = replayed || red.meta?.reused === true
2953
+ ? journal.read().reverse().find((e) => e.taskId === t.id && e.event === "gate-reobserved"
2954
+ && e.data.commit === subject && e.data.fingerprint === fingerprint
2955
+ && (observation === undefined || e.data.observation === undefined || e.data.observation === observation))
2956
+ : undefined;
2957
+ let files = [];
2958
+ let outcome = "unattributed";
2959
+ let selection;
2960
+ if (recorded) {
2961
+ files = Array.isArray(recorded.data.files) ? recorded.data.files.filter((f) => typeof f === "string") : [];
2962
+ outcome = ["reproduced", "passed", "unattributed"].includes(String(recorded.data.outcome)) ? recorded.data.outcome : "ambiguous";
2963
+ }
2964
+ else {
2965
+ for (const file of attributedFailingFiles(red)) {
2966
+ if (posix.normalize(file) === file && !file.startsWith("../") && !file.startsWith("/") && existsSync(join(wt, file)))
2967
+ files.push(file);
2968
+ }
2969
+ let rerun;
2970
+ if (files.length) {
2971
+ rerun = await withCommandContext(t.id, () => reobserveTestFiles(wt, commands.test, baseline, files, journal.dir))
2972
+ .catch((error) => ({ gate: "test", pass: false, details: `infra: rerun failed: ${error instanceof Error ? error.message : String(error)}`, meta: { infra: true } }));
2973
+ classifyInfraResult(rerun);
2974
+ // Reproduced only by failure evidence the rerun ATTRIBUTES to one of exactly these files, from an
2975
+ // OBSERVED selection that was exactly these files (D-607): a path merely printed in output, a
2976
+ // positional filter that also collected another file, or a runner that ignored the file
2977
+ // arguments and reported no manifest of its selection, says nothing about the original red.
2978
+ const failing = rerunFailingFiles(rerun);
2979
+ selection = observedSelection(rerun);
2980
+ const isolated = selection !== undefined && selection.length === files.length
2981
+ && files.every((file) => selection.includes(file)) && failing.every((file) => files.includes(file));
2982
+ outcome = rerun.pass ? "passed" : isolated && !isInfraResult(rerun) && failing.length ? "reproduced" : "ambiguous";
2983
+ }
2984
+ journal.append("gate-reobserved", t.id, {
2985
+ gate: "test", attempt, commit: subject, fingerprint, ...(observation ? { observation } : {}), files, outcome,
2986
+ ...(selection ? { selection } : {}),
2987
+ ...(rerun ? { pass: rerun.pass, details: rerun.details } : {}),
2988
+ ...(rerun?.evidenceReceipt ? { evidenceReceipt: rerun.evidenceReceipt } : {}),
2989
+ });
2990
+ }
2991
+ if (outcome === "reproduced")
2992
+ return false;
2993
+ await parkInfra(`test: timeout-shaped red with runner infra diagnostics was not reproduced by one isolated rerun of ${files.length ? files.join(", ") : "its attributed files (none attributable)"} (${outcome}); the original red stands unresolved — recheck to re-measure:\n${readableExcerpt(red.details)}`);
2994
+ return true;
2995
+ };
2838
2996
  // OBS-547: cross-reference a scope red against the prediction this run already computed. Every hard
2839
2997
  // offender predicted ⇒ an AUTHORING defect whose repair is pre-written: journal the classification
2840
2998
  // with the verbatim files[] lines and park unchargeable. It owes nothing to the quality machinery —
@@ -2853,7 +3011,8 @@ export async function runDaemon(repoRoot, opts = {}) {
2853
3011
  const verdict = scopeRed?.meta?.collateral;
2854
3012
  const worker = journal.read().reverse().find((e) => e.taskId === t.id && e.event === "worker-result");
2855
3013
  const disposition = classifyRepairDisposition({
2856
- results, files: t.files, inventory: changed,
3014
+ // OBS-1072 add.2: hints are lexed from the readable excerpt, never from terminal control bytes.
3015
+ results: results.map((g) => ({ ...g, details: readableExcerpt(g.details) })), files: t.files, inventory: changed,
2857
3016
  refusalSummary: worker?.data.ok === false && typeof worker.data.summary === "string" ? worker.data.summary : undefined,
2858
3017
  });
2859
3018
  for (const diagnostic of disposition.diagnostics) {
@@ -2965,7 +3124,13 @@ export async function runDaemon(repoRoot, opts = {}) {
2965
3124
  if (changed)
2966
3125
  saveGraph(repoRoot, graph);
2967
3126
  };
3127
+ // OBS-1168(c): every gate callback is bound to the engagement that asked for it. Once the scheduler
3128
+ // settles that engagement (done, parked or failed), a late sibling's event is dropped, never journaled.
3129
+ const engagements = new Map();
2968
3130
  const execTask = async (t) => {
3131
+ const engagement = {};
3132
+ engagements.set(t.id, engagement);
3133
+ const liveEngagement = () => engagements.get(t.id) === engagement;
2969
3134
  const approvalAction = pendingDaemonApprovalActions(journal.read()).get(t.id);
2970
3135
  const fundedRerun = approvalAction?.authority === "worker";
2971
3136
  const startMs = Date.now();
@@ -3049,9 +3214,13 @@ export async function runDaemon(repoRoot, opts = {}) {
3049
3214
  // OBS-1161: no `attempts > 0` guard — every release already clears lastAssignment in the replay,
3050
3215
  // and a lastAssignment at zero attempts is a first dispatch whose capacity requeue was taken back:
3051
3216
  // the seat is still in force, so restore it instead of failing over its own tried[] entry early.
3217
+ // OBS-1187: a restored seat must still be a member of the task's pool (the operator may have closed it).
3218
+ // F/W (D-718): and not excluded under the router's one meaning — its raw key, or a probed identity
3219
+ // it shares with an excluded channel; an unprobed seat is excluded by its raw key alone.
3052
3220
  if (!hintsChanged && rs?.lastAssignment
3053
3221
  && channels.some((c) => channelKey(c) === channelKey(rs.lastAssignment))
3054
- && !demotedChannels.has(channelKey(rs.lastAssignment))) {
3222
+ && inTaskPool(t, cfg, channelKey(rs.lastAssignment))
3223
+ && !channelExclusion(channels, demotedChannels)(rs.lastAssignment)) {
3055
3224
  assignment = rs.lastAssignment; // restore the consult-chosen assignment (bypasses route()'s static re-pick)
3056
3225
  }
3057
3226
  else if (!hintsChanged && rs && rs.tried.length && !t.routingHints?.pin) {
@@ -3079,7 +3248,8 @@ export async function runDaemon(repoRoot, opts = {}) {
3079
3248
  const lastApproval = taskHistory.map((e) => e.event).lastIndexOf("task-approved");
3080
3249
  const priorClimb = taskHistory.slice(lastApproval + 1).reverse().find((e) => e.event === "tier-escalated");
3081
3250
  if (!hintsChanged && priorClimb && taskHistory.indexOf(priorClimb) > taskHistory.map((e) => e.event).lastIndexOf("task-dispatch")) {
3082
- const target = channels.find((c) => channelKey(c) === priorClimb.data.to && !demotedChannels.has(channelKey(c)));
3251
+ const target = channels.find((c) => channelKey(c) === priorClimb.data.to && !demotedChannels.has(channelKey(c))
3252
+ && inTaskPool(t, cfg, channelKey(c)));
3083
3253
  if (target)
3084
3254
  assignment = seatAssignment(target);
3085
3255
  }
@@ -3122,7 +3292,7 @@ export async function runDaemon(repoRoot, opts = {}) {
3122
3292
  workerDispatchOrdinal: previousDispatch?.data.workerDispatchOrdinal ?? null,
3123
3293
  });
3124
3294
  if (bootstrapExhausted === channelKey(assignment)) { // a later tier-climb restore may have moved it off
3125
- await park(t, `${bootstrapExhausted} failed at bootstrap and no eligible channel remains after resume`, "infra", assignment, rs?.attempts ?? 0, startMs, 0, 0, undefined, 0, "fresh", { cause: "bootstrap", channel: bootstrapExhausted });
3295
+ await park(t, `${bootstrapExhausted} failed at bootstrap and no eligible channel remains after resume${poolExhaustion(t, cfg)}`, "infra", assignment, rs?.attempts ?? 0, startMs, 0, 0, undefined, 0, "fresh", { cause: "bootstrap", channel: bootstrapExhausted });
3126
3296
  return;
3127
3297
  }
3128
3298
  // Keep one live list: recovery retries must see exclusions added by onGate during the round.
@@ -3133,18 +3303,33 @@ export async function runDaemon(repoRoot, opts = {}) {
3133
3303
  if (!badReviewers.includes(e.payload.reviewer))
3134
3304
  badReviewers.push(e.payload.reviewer);
3135
3305
  }
3306
+ // OBS-1196: a seat whose same-subject re-emission delivered a valid verdict was a delivery flake,
3307
+ // not a bad reviewer — it stays seatable for this task's later rounds.
3308
+ if (e.name === "review-reemission" && e.payload.delivered === true && typeof e.payload.reviewer === "string") {
3309
+ const at = badReviewers.indexOf(e.payload.reviewer);
3310
+ if (at >= 0)
3311
+ badReviewers.splice(at, 1);
3312
+ }
3136
3313
  };
3137
3314
  // Keep the measured siblings, including a red judge, while replacing only a missing review.
3138
3315
  // Every dispatched seat is excluded for this task, so the loop is bounded by the eligible pool.
3139
3316
  const runReviewRecovery = async (task, ctx, allowSelection = true) => {
3140
3317
  ctx.authorizeInfraRetry = cfg.executionPolicy?.boundedInfrastructure
3141
3318
  ? (subject, cause) => reserveInfrastructureRetry(journal.read(), task.id, subject, journal.append.bind(journal), cause) : undefined;
3142
- if (cfg.executionPolicy?.repairSelection && allowSelection) {
3319
+ if (repairSelection && allowSelection) {
3143
3320
  const decision = repairSelectionDecision(journal.read(), task.id, true);
3144
3321
  ctx.selectTests = decision.selectTests;
3145
3322
  ctx.requiredRepairTests = decision.requiredFiles;
3146
3323
  ctx.selectionReason = decision.reason;
3147
3324
  }
3325
+ else if (repairSelection) {
3326
+ // A recheck runs the full suite; its recorded scope keeps a red there attributable.
3327
+ ctx.selectTests = false;
3328
+ ctx.selectionReason = "recheck-full-suite";
3329
+ }
3330
+ // OBS-1151: every round judges fresh; the journal's earlier judgments (this run and before a
3331
+ // resume) are only the subjects a reversed ruling is compared against.
3332
+ ctx.priorJudgments = priorJudgments(journal.read(), task.id);
3148
3333
  // Resume and ordinary verification share this boundary. Count persisted rounds so a
3149
3334
  // resumed daemon cannot reuse a previous round's identity within the same attempt.
3150
3335
  ctx.buildReceiptIdentity = {
@@ -3247,9 +3432,9 @@ export async function runDaemon(repoRoot, opts = {}) {
3247
3432
  ...(typeof g.meta.producer === "string" ? { producer: g.meta.producer } : {}),
3248
3433
  ...(typeof g.meta.producerAttempt === "number" ? { producerAttempt: g.meta.producerAttempt } : {}),
3249
3434
  } : {}),
3250
- ...(cfg.executionPolicy && !g.pass ? { disposition: failureDisposition(g) } : {}),
3435
+ ...((cfg.executionPolicy || repairSelection) && !g.pass ? { disposition: failureDisposition(g) } : {}),
3251
3436
  ...Object.fromEntries(["runnerInfraRerun", "hostStarvedRerun", "recoveryBlocked", "failingFiles", "selectionDecision", "failureEvidence",
3252
- "forgivenFingerprints", "freshFingerprints", "baselineProvenance"]
3437
+ "forgivenFingerprints", "freshFingerprints", "baselineProvenance", "judgment", "adjudication", "judgeDisagreement", "interruptions"]
3253
3438
  .filter((key) => g.meta?.[key] !== undefined).map((key) => [key, g.meta[key]])),
3254
3439
  // OBS-540: preserve terminal-vs-retryable infra exactly. normalizeGateOutcome deliberately
3255
3440
  // defaults a legacy infra row to retryable, so dropping an explicit false here reverses the
@@ -3275,7 +3460,7 @@ export async function runDaemon(repoRoot, opts = {}) {
3275
3460
  } : {}),
3276
3461
  ...(g.gate === "review" && typeof g.meta?.reviewer === "string" ? {
3277
3462
  reviewer: g.meta.reviewer,
3278
- ...Object.fromEntries(["cause", "seatAuthoredBytes", "bytes", "rawPath", "briefPath", "timeoutMs", "unparseable", "noVerdict", "resolved", "reraised", "reviewerFloor", "reviewerFloorCause", "reviewerTier"]
3463
+ ...Object.fromEntries(["cause", "seatAuthoredBytes", "bytes", "rawPath", "briefPath", "timeoutMs", "unparseable", "noVerdict", "resolved", "reraised", "settledAnchors", "reviewerFloor", "reviewerFloorCause", "reviewerTier"]
3279
3464
  .filter((key) => g.meta?.[key] !== undefined).map((key) => [key, g.meta[key]])),
3280
3465
  ...(typeof g.meta.vendor === "string" ? { vendor: g.meta.vendor } : {}),
3281
3466
  ...(typeof g.meta.provider === "string" ? { provider: g.meta.provider } : {}),
@@ -3409,7 +3594,8 @@ export async function runDaemon(repoRoot, opts = {}) {
3409
3594
  return;
3410
3595
  const from = channelKey(assignment);
3411
3596
  const to = channelKey(next);
3412
- const poolBefore = channels.filter((c) => !tried.includes(channelKey(c)) && !demotedChannels.has(channelKey(c))).map(channelKey);
3597
+ const poolBefore = channels.filter((c) => !tried.includes(channelKey(c)) && !demotedChannels.has(channelKey(c))
3598
+ && inTaskPool(t, cfg, channelKey(c))).map(channelKey);
3413
3599
  journal.append("tier-escalated", t.id, {
3414
3600
  attempt, cause, gate: gate.gate, fingerprint: normalizeGateFailure(gate.details).slice(0, 500),
3415
3601
  from, to, fromTier: assignment.tier, toTier: next.tier, poolBefore,
@@ -3467,14 +3653,15 @@ export async function runDaemon(repoRoot, opts = {}) {
3467
3653
  // on artificial scarcity; MAX_ATTEMPTS and the review round cap are the real bounds. Only a
3468
3654
  // fleet whose every channel is DEMOTED (verified dead: auth/setup/provider outage) has nothing
3469
3655
  // left to spawn — that case alone still parks.
3656
+ // OBS-1187: shared with the demotion seam's pooled recycle so neither relaunches an escalated adapter.
3657
+ const recycleExcluded = () => [channelKey(assignment), ...channels.filter((c) => escalatedAdapters.has(c.adapter)).map(channelKey)];
3470
3658
  const failoverOrRecycle = (site) => {
3471
3659
  const next = failover(site);
3472
3660
  if (next)
3473
3661
  return next;
3474
3662
  // OBS-1169: a vendor escalated away from stays out of the recycle pool until its release —
3475
3663
  // recycling any of its channels, launched or not, would launch the broken bootstrap again.
3476
- const recycleExcluded = [channelKey(assignment), ...channels.filter((c) => escalatedAdapters.has(c.adapter)).map(channelKey)];
3477
- const recycled = nextChannel(assignment, t, cfg, channels, recycleExcluded, profile, demotedChannels)
3664
+ const recycled = nextChannel(assignment, t, cfg, channels, recycleExcluded(), profile, demotedChannels)
3478
3665
  ?? (demotedChannels.has(channelKey(assignment)) ? null : assignment);
3479
3666
  if (recycled)
3480
3667
  journal.append("channel-recycle", t.id, { site, channel: channelKey(recycled) });
@@ -3493,7 +3680,7 @@ export async function runDaemon(repoRoot, opts = {}) {
3493
3680
  // D-07: consult panes self-clean when the verdict is read (keepLlm) — only "forever" keeps them.
3494
3681
  // v1.54 T1: channels = this run's doctor-filtered live list — consult.prefer seat liveness
3495
3682
  // is judged against it, never rebuilt from config (installed-but-unauthed seats would stall).
3496
- { keep: keepLlm, onSlot: keepLlm ? (s) => keptSlots.push(s) : undefined, onInvocation: (inv) => invocations.push(inv), runId, channels: pools.consult });
3683
+ { keep: keepLlm, onSlot: keepLlm ? (s) => keptSlots.push(s) : undefined, onInvocation: (inv) => invocations.push(inv), runId, channels: pools.consult, health });
3497
3684
  // A lone answering seat is already the row's adapter/model/vendor/effort; the list is journaled
3498
3685
  // only when a seat failed, so the row never repeats itself and a failed seat never vanishes.
3499
3686
  return invocations.some((inv) => inv.outcome === "failed") ? { ...v, invocations } : v;
@@ -3539,7 +3726,7 @@ export async function runDaemon(repoRoot, opts = {}) {
3539
3726
  tried.push(k);
3540
3727
  return true;
3541
3728
  }
3542
- await park(t, "consult said reroute but every channel is demoted (verified dead) — nothing left to spawn", "reroute-exhausted", assignment, attempts, startMs, gateFails, consults, tokens, metered, retryMode);
3729
+ await park(t, `consult said reroute but every channel is demoted (verified dead) — nothing left to spawn${poolExhaustion(t, cfg)}`, "reroute-exhausted", assignment, attempts, startMs, gateFails, consults, tokens, metered, retryMode);
3543
3730
  return false;
3544
3731
  }
3545
3732
  await park(t, `consult verdict: ${v.action} — ${v.notes}`, trigger, assignment, attempts, startMs, gateFails, consults, tokens, metered, retryMode); // decompose|human
@@ -3620,20 +3807,19 @@ export async function runDaemon(repoRoot, opts = {}) {
3620
3807
  // died. A pane the driver cannot read is DECLINED, never classified: an unreadable pane proves
3621
3808
  // neither a matching trailer nor the absence of a foreign one, so it is neither harvested nor
3622
3809
  // gated as trailerless evidence. A driver whose terminals outlive it (Orca) adopts the owned
3623
- // terminal: bound only on ownership evidence — the journaled owned title AND the task checkout —
3624
- // never a title alone. The journaled slot.id is NEVER trusted across daemon instances: a fresh
3625
- // driver numbers its slots from one again, so an interrupted task's old id can name whatever
3626
- // terminal THIS instance bound under that id (another task's adoption or allocation).
3810
+ // terminal (Orca) or pane (Herdr, OBS-1203): bound only on ownership evidence — the journaled owned
3811
+ // name AND the task checkout — never a name alone. The journaled slot.id is NEVER trusted across
3812
+ // daemon instances: a fresh driver numbers its slots from one again, so an interrupted task's old
3813
+ // id can name whatever terminal THIS instance bound under that id (another task's adoption or allocation).
3627
3814
  // A driver with no read-only adoption is declined BEFORE anything is bound: slot() allocates —
3628
3815
  // herdr's reclaims the same-named pane (closing the evidence unread) and holds a dispatch lease
3629
3816
  // only run() releases — so it is never an adoption fallback; that driver keeps ordinary recovery.
3630
- const adopt = driver.adopt;
3631
- if (!adopt)
3817
+ if (!driver.adopt)
3632
3818
  return decline("no read-only adoption on this driver");
3633
3819
  let ownedSlot;
3634
3820
  let pane;
3635
3821
  try {
3636
- ownedSlot = await adopt.call(driver, slot);
3822
+ ownedSlot = await driver.adopt(slot);
3637
3823
  pane = await driver.read(ownedSlot, PANE_READ_ROWS);
3638
3824
  }
3639
3825
  catch (error) {
@@ -3661,6 +3847,47 @@ export async function runDaemon(repoRoot, opts = {}) {
3661
3847
  return found.assignment;
3662
3848
  };
3663
3849
  const harvestAuthor = rs && !satisfiedGate && !replayedGates && !recheck ? await harvestInterruptedAttempt() : undefined;
3850
+ // OBS-1202: a recheck bound to a stall park that recorded a reapFailure re-verifies the parked
3851
+ // attempt's owned census through the same reapWorker a live harvest uses, BEFORE any gate reads
3852
+ // its tree. Only a complete, empty census releases the harvested commits to the battery; an
3853
+ // unreadable (null) or surviving census keeps the task parked on the same kind — a stall carrying
3854
+ // the fresh reapFailure, bound by its own new token — and no worker is funded either way. Plain
3855
+ // approve on that park keeps its dispatch semantics.
3856
+ if (recheck) {
3857
+ const approvalRow = effectiveEvents(journal.read()).reverse()
3858
+ .find((e) => e.taskId === t.id && e.event === "task-approved" && e.data.release === RECHECK_RELEASE);
3859
+ if (typeof approvalRow?.data.reapFailure === "string") {
3860
+ const launch = [...journal.read()].reverse().find((e) => e.taskId === t.id && e.event === "worker-launch");
3861
+ const owned = launch?.data.slot;
3862
+ let uncertain;
3863
+ if (typeof launch?.data.nonce !== "string" || typeof launch.data.dispatchScript !== "string"
3864
+ || typeof owned?.name !== "string" || typeof owned.cwd !== "string") {
3865
+ uncertain = "the parked attempt recorded no launch ownership evidence";
3866
+ }
3867
+ else {
3868
+ const key = { ...owned };
3869
+ workerOwners.set(key, { taskId: t.id, attempt: Number(launch.data.attempt ?? 0),
3870
+ groupFile: `${launch.data.dispatchScript}.${launch.data.nonce}.pgid`, marker: launch.data.dispatchScript,
3871
+ identities: new Map(), descendants: new Map() });
3872
+ try {
3873
+ await reapWorker(key);
3874
+ // reapWorker returns normally for a null census when no ownership file was readable (a
3875
+ // missing .pgid/.session with no descendants); recovery needs a RECORDED empty census.
3876
+ const census = reapReports.get(key)?.survivors;
3877
+ if (!Array.isArray(census) || census.length > 0) {
3878
+ uncertain = `worker group ${reapReports.get(key)?.processGroup ?? "unknown"} census unrecorded`;
3879
+ }
3880
+ }
3881
+ catch (error) {
3882
+ uncertain = error instanceof Error ? error.message : String(error);
3883
+ }
3884
+ }
3885
+ if (uncertain !== undefined) {
3886
+ await park(t, `worker could not be reaped before recheck: ${uncertain}`, "stall", null, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode, { reapFailure: uncertain });
3887
+ return;
3888
+ }
3889
+ }
3890
+ }
3664
3891
  resumeGateReplay: if (satisfiedGate || replayedGates || recheck || harvestAuthor) {
3665
3892
  const taskBase = await integrationHead(intWt);
3666
3893
  taskBases.set(t.id, taskBase);
@@ -3791,7 +4018,10 @@ export async function runDaemon(repoRoot, opts = {}) {
3791
4018
  const followsApproved = GATE_NAMES.indexOf(gate) > satisfiedIndex;
3792
4019
  const otherRed = prior?.pass === false;
3793
4020
  const needsFullSuite = gate === "test" && prior?.fullSuite !== true;
3794
- return followsApproved || otherRed || needsFullSuite;
4021
+ // W: a red evidence/scope screen ends the round before the battery, so a gate that PRECEDES
4022
+ // the waived one may never have run: no prior-round row is no verdict — run it as declared.
4023
+ const unobserved = prior === undefined;
4024
+ return followsApproved || otherRed || needsFullSuite || unobserved;
3795
4025
  });
3796
4026
  }
3797
4027
  else {
@@ -3887,14 +4117,27 @@ export async function runDaemon(repoRoot, opts = {}) {
3887
4117
  }
3888
4118
  await trackedDriver.project?.(t.id, "in-review");
3889
4119
  await waitForBaseline(t.id);
4120
+ // OBS-1106 residual: the live loop's timeout refresh, decided again from the journal, so a crash
4121
+ // cannot launder the copy it forbids. A harvested retry that landed nothing owes it exactly as
4122
+ // the live loop would have; a crash inside a forced battery before its test row still owes it.
4123
+ const lastRound = (() => {
4124
+ const own = journal.read().filter((e) => e.taskId === t.id);
4125
+ return own.slice(own.map((e) => e.event === "phase-start" && e.data.phase === "gates").lastIndexOf(true) + 1);
4126
+ })();
4127
+ const freshTimeout = !recheck && !satisfiedGate && ((harvestAuthor !== undefined && owesTimeoutRefresh(t, lastRound, gateSubject.commit))
4128
+ || (lastRound.some((e) => e.event === "gate-fresh-forced" && e.data.commit === gateSubject.commit)
4129
+ && !lastRound.some((e) => e.event === "gate-result" && e.data.gate === "test")));
3890
4130
  journal.phaseStart(t.id, "gates");
4131
+ if (freshTimeout) {
4132
+ journal.append("gate-fresh-forced", t.id, { gate: "test", attempt: gateSubject.attempt, commit: gateSubject.commit, reason: "no-commit-timeout-red", resumed: true });
4133
+ }
3891
4134
  const { results } = await withCommandContext(t.id, async () => runReviewRecovery(resumedTask, {
3892
4135
  carriedAuthors: await subjectAuthors(journal.read(), t.id, wt, taskBase),
3893
4136
  producer: producerNow(),
3894
4137
  carriedFindings: outstandingReviewFindings(journal.read(), t.id),
3895
4138
  operatorContext,
3896
4139
  worktree: wt, baseRef: taskBase, result: priorResult, author: gateAuthor,
3897
- commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
4140
+ commands, baseline, channels: pools.review, judgeChannels: pools.judge, health, adapters, cfg, artifactDir: journal.dir,
3898
4141
  collateral: collateral.get(t.id) ?? [],
3899
4142
  // a recheck re-verifies a human's release: it never selects tests down, it runs the suite.
3900
4143
  via: cfg.visibility.llm === "pane"
@@ -3912,7 +4155,10 @@ export async function runDaemon(repoRoot, opts = {}) {
3912
4155
  // flaking seat is re-asked on every task.
3913
4156
  reviewNoVerdicts,
3914
4157
  recheck, // OBS-1055: a recheck discards cached reds — the battery re-measures what the operator questioned
4158
+ cachedRedBypass: freshTimeout ? "timeout-fresh" : undefined,
3915
4159
  onGate: async (e) => {
4160
+ if (!liveEngagement())
4161
+ return;
3916
4162
  if (e.phase === "start") {
3917
4163
  notePhaseStart(e);
3918
4164
  journal.phaseStart(t.id, phaseForGate(e.gate), { gate: e.gate, index: e.index, total: e.total, ...(e.parentAt === undefined ? {} : { parallel: true }) });
@@ -3928,10 +4174,8 @@ export async function runDaemon(repoRoot, opts = {}) {
3928
4174
  inParallelOrder(g.gate, () => {
3929
4175
  journalGateResult(g);
3930
4176
  noteReviewRetry(g);
3931
- if (g.gate === "review" && !g.pass && /unparseable/.test(g.details)
3932
- && typeof g.meta?.reviewer === "string") {
4177
+ if (isGarbageReview(g))
3933
4178
  badReviewers.push(g.meta.reviewer);
3934
- }
3935
4179
  });
3936
4180
  },
3937
4181
  }, false));
@@ -3974,6 +4218,8 @@ export async function runDaemon(repoRoot, opts = {}) {
3974
4218
  // erase an EARLIER chargeable attempt and attribute the park to the assignment the rewind
3975
4219
  // restored. Ask the journal which state this is, and take the classified dispatch's own
3976
4220
  // assignment back with it.
4221
+ if (await adjudicateInfraShapedRed(t, results, wt, gateSubject.commit, rs?.attempts ?? 0, false, (reason) => park(t, reason, "infra", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode)))
4222
+ return;
3977
4223
  const classified = journal.classifiedDispatch(t.id);
3978
4224
  if (await dispositionScopeRed(t, results, classified?.assignment ?? gateAuthor, classified ? (rs?.attempts ?? 0) : Math.max(0, (rs?.attempts ?? 0) - 1), startMs, gateFails, consults, tokens, metered, retryMode, await treeOrDiffPaths(taskBase, wt)))
3979
4225
  return;
@@ -4103,13 +4349,21 @@ export async function runDaemon(repoRoot, opts = {}) {
4103
4349
  }
4104
4350
  // OBS-57: a demoted channel must not be re-dispatched on consult retry or provider requeue.
4105
4351
  if (demotedChannels.has(channelKey(assignment))) {
4106
- const next = nextChannel(assignment, t, cfg, channels, tried, profile, demotedChannels);
4352
+ // OBS-1187: a pooled task recycles a live pool member (OBS-202) before it may park naming the pool;
4353
+ // it never keeps a demoted seat and never leaves the pool.
4354
+ const pooled = taskPool(t, cfg) !== undefined;
4355
+ const next = nextChannel(assignment, t, cfg, channels, tried, profile, demotedChannels)
4356
+ ?? (pooled ? nextChannel(assignment, t, cfg, channels, recycleExcluded(), profile, demotedChannels) : null);
4107
4357
  if (next) {
4108
4358
  assignment = next;
4109
4359
  const k = channelKey(next);
4110
4360
  if (!tried.includes(k))
4111
4361
  tried.push(k);
4112
4362
  }
4363
+ else if (pooled) {
4364
+ await park(t, `${channelKey(assignment)} is demoted (verified dead) and no live pool member remains${poolExhaustion(t, cfg)}`, "reroute-exhausted", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
4365
+ return;
4366
+ }
4113
4367
  }
4114
4368
  // v1.29: consume the prior gate-failed session once. Same channel + known under-threshold context
4115
4369
  // + adapter capability resumes; every other path is today's fresh dispatch.
@@ -4134,7 +4388,7 @@ export async function runDaemon(repoRoot, opts = {}) {
4134
4388
  gate: banned, from: channelKey(assignment), to: next ? channelKey(next) : null,
4135
4389
  });
4136
4390
  if (!next) {
4137
- await park(t, gateFailApprovalReason(t.id, `identical ${banned} failure twice this engagement — an identical retry is banned and no untried channel is left`), "gate-fail", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
4391
+ await park(t, gateFailApprovalReason(t.id, `identical ${banned} failure twice this engagement — an identical retry is banned and no untried channel is left${poolExhaustion(t, cfg)}`), "gate-fail", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode);
4138
4392
  return;
4139
4393
  }
4140
4394
  assignment = next;
@@ -4348,10 +4602,18 @@ export async function runDaemon(repoRoot, opts = {}) {
4348
4602
  ? `${new TextDecoder().decode(rawBytes.subarray(0, cap))}\n… diff truncated at gates.diffCap (${cap} bytes)`
4349
4603
  + `\nPARTIAL DIFF: the diff above is incomplete; files cut by the cap: ${droppedFiles.join(", ") || "(unknown)"}`
4350
4604
  : raw.stdout;
4351
- const brief = repairBrief(repairFindings, diff, taskBase);
4605
+ const repeated = reraisedReviewChains(journaledSoFar, t.id).filter((chain) => chain.reraises > 1);
4606
+ const brief = repairBrief(repairFindings, diff, taskBase, repeated);
4352
4607
  // anything the live brief holds beyond the journaled findings (a consult's guidance) is kept:
4353
4608
  // a repair adds the diff and the fix-only contract, it never subtracts what was already known.
4354
- feedback = feedback && !brief.includes(feedback) ? `${brief}\n\n${feedback}` : brief;
4609
+ // OBS-1195: a resumed upheld repair re-derives the same review bytes on both carries; when the
4610
+ // repair already quotes them verbatim they are said once, and the operator's uphold line stays.
4611
+ // Both carries had their deferrals cut above, so the quote is compared after the same cut.
4612
+ const upheldQuote = upheldFeedback ? withoutDeferrals(`review: ${upheldFeedback}`) : undefined;
4613
+ const rest = upheldQuote && brief.includes(upheldQuote)
4614
+ ? feedback.replace(upheldQuote, "").replace(/\n{3,}/g, "\n\n").trim()
4615
+ : feedback;
4616
+ feedback = rest && !brief.includes(rest) ? `${brief}\n\n${rest}` : brief;
4355
4617
  journal.append("repair-dispatch", t.id, { workerDispatchOrdinal, diffBytes: Buffer.byteLength(diff, "utf8"), capped, ...(capped ? { droppedFiles } : {}) });
4356
4618
  }
4357
4619
  if (feedback || priorNamed.length > 0) {
@@ -5675,7 +5937,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5675
5937
  attempt--; // the dead bootstrap bought no worker turn
5676
5938
  continue;
5677
5939
  }
5678
- await park(t, `${from} failed at bootstrap after ${retries} same-channel retry and no eligible channel remains`, "infra", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode, { cause: "bootstrap", channel: from, retries });
5940
+ await park(t, `${from} failed at bootstrap after ${retries} same-channel retry and no eligible channel remains${poolExhaustion(t, cfg)}`, "infra", assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode, { cause: "bootstrap", channel: from, retries });
5679
5941
  return;
5680
5942
  }
5681
5943
  const preHarvestResult = result;
@@ -5787,7 +6049,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5787
6049
  tried.push(channelKey(next));
5788
6050
  continue;
5789
6051
  }
5790
- await park(t, `capacity exhausted on ${from} after ${requeues} requeues and no eligible channel at floor`, "quota", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode, { cause: "capacity", channel: from, requeues });
6052
+ await park(t, `capacity exhausted on ${from} after ${requeues} requeues and no eligible channel at floor${poolExhaustion(t, cfg)}`, "quota", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode, { cause: "capacity", channel: from, requeues });
5791
6053
  return;
5792
6054
  }
5793
6055
  // quota exhaustion → failover within floor; does NOT consume the ladder (spec §4)
@@ -5850,7 +6112,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5850
6112
  tried.push(channelKey(next));
5851
6113
  continue;
5852
6114
  }
5853
- await park(t, "quota exhausted on every eligible channel", "quota", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
6115
+ await park(t, `quota exhausted on every eligible channel${poolExhaustion(t, cfg)}`, "quota", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
5854
6116
  return;
5855
6117
  }
5856
6118
  // v1.65 T1: typed dead-channel failure — the parse boundary classified this no-trailer result
@@ -5898,7 +6160,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5898
6160
  continue;
5899
6161
  }
5900
6162
  const chargedAttempts = adapterStartupFailure || startupFailure ? attempt : attempt + 1;
5901
- await park(t, `dead channel (${dead}) and no eligible channel remains`, "reroute-exhausted", assignment, chargedAttempts, startMs, gateFails, consults, tokens, metered, retryMode);
6163
+ await park(t, `dead channel (${dead}) and no eligible channel remains${poolExhaustion(t, cfg)}`, "reroute-exhausted", assignment, chargedAttempts, startMs, gateFails, consults, tokens, metered, retryMode);
5902
6164
  return;
5903
6165
  }
5904
6166
  if (!finished) {
@@ -5943,6 +6205,8 @@ export async function runDaemon(repoRoot, opts = {}) {
5943
6205
  }
5944
6206
  }
5945
6207
  const onGate = async (e) => {
6208
+ if (!liveEngagement())
6209
+ return;
5946
6210
  if (e.phase === "start") {
5947
6211
  notePhaseStart(e);
5948
6212
  journal.phaseStart(t.id, phaseForGate(e.gate), { gate: e.gate, index: e.index, total: e.total, ...(e.parentAt === undefined ? {} : { parallel: true }) });
@@ -5976,9 +6240,8 @@ export async function runDaemon(repoRoot, opts = {}) {
5976
6240
  journalGateResult(g);
5977
6241
  noteReviewRetry(g);
5978
6242
  // v1.1 failover: never re-ask a reviewer channel that produced garbage for this task
5979
- if (g.gate === "review" && !g.pass && /unparseable/.test(g.details) && typeof g.meta?.reviewer === "string") {
6243
+ if (isGarbageReview(g))
5980
6244
  badReviewers.push(g.meta.reviewer);
5981
- }
5982
6245
  });
5983
6246
  };
5984
6247
  let results = [];
@@ -5994,12 +6257,16 @@ export async function runDaemon(repoRoot, opts = {}) {
5994
6257
  // sees the replay, without buying another command or reviewer invocation.
5995
6258
  const taskEvents = journal.read().filter((e) => e.taskId === t.id);
5996
6259
  const previousRound = taskEvents.map((e) => e.event === "phase-start" && e.data.phase === "gates").lastIndexOf(true);
5997
- const previousRows = retryMode === "repair"
5998
- ? taskEvents.slice(previousRound + 1).filter((e) => e.event === "gate-result"
5999
- && e.data.attempt === attempt - 1)
6000
- : [];
6260
+ const lastRoundRows = taskEvents.slice(previousRound + 1).filter((e) => e.event === "gate-result"
6261
+ && e.data.attempt === attempt - 1);
6262
+ const previousRows = retryMode === "repair" ? lastRoundRows : [];
6263
+ // OBS-1106 residual: see owesTimeoutRefresh — the replay below and the verdict cache are both refused.
6264
+ const freshTimeout = !explicitlyFundedAttempt && owesTimeoutRefresh(t, lastRoundRows, gateSubject.commit);
6001
6265
  journal.phaseStart(t.id, "gates");
6002
- const replay = !explicitlyFundedAttempt && previousRows.length > 0
6266
+ if (freshTimeout) {
6267
+ journal.append("gate-fresh-forced", t.id, { gate: "test", attempt, priorAttempt: attempt - 1, commit: gateSubject.commit, reason: "no-commit-timeout-red" });
6268
+ }
6269
+ const replay = !explicitlyFundedAttempt && !freshTimeout && previousRows.length > 0
6003
6270
  && previousRows.every((e) => e.data.commit === gateSubject.commit)
6004
6271
  && previousRows.some((e) => e.data.pass === false && e.data.skipped !== true);
6005
6272
  if (replay) {
@@ -6029,10 +6296,10 @@ export async function runDaemon(repoRoot, opts = {}) {
6029
6296
  carriedFindings: outstandingFindings,
6030
6297
  operatorContext,
6031
6298
  worktree: wt, baseRef: taskBase, result, author: assignment,
6032
- commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
6299
+ commands, baseline, channels: pools.review, judgeChannels: pools.judge, health, adapters, cfg, artifactDir: journal.dir,
6033
6300
  collateral: collateral.get(t.id) ?? [],
6034
6301
  selectTests: !testGateFailed,
6035
- cachedRedBypass: explicitlyFundedAttempt ? "operator-rerun" : undefined,
6302
+ cachedRedBypass: explicitlyFundedAttempt ? "operator-rerun" : freshTimeout ? "timeout-fresh" : undefined,
6036
6303
  via: cfg.visibility.llm === "pane"
6037
6304
  ? {
6038
6305
  driver: trackedDriver,
@@ -6139,6 +6406,8 @@ export async function runDaemon(repoRoot, opts = {}) {
6139
6406
  await park(t, `${capTrip.gate}: ${capTrip.details}`, "diff-cap", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
6140
6407
  return;
6141
6408
  }
6409
+ if (await adjudicateInfraShapedRed(t, results, wt, gateSubject.commit, attempt, gateSubject.replayedFromAttempt !== undefined, (reason) => park(t, reason, "infra", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode)))
6410
+ return;
6142
6411
  // OBS-547: who pays for this red is decided by the run's collateral prediction (see
6143
6412
  // dispositionScopeRed) — an authoring defect parks unchargeable before any accounting below.
6144
6413
  if (await dispositionScopeRed(t, results, assignment, attempt, startMs, gateFails, consults, tokens, metered, retryMode, await treeOrDiffPaths(taskBase, wt)))
@@ -6371,6 +6640,7 @@ export async function runDaemon(repoRoot, opts = {}) {
6371
6640
  for (const t of ready) {
6372
6641
  const p = executeWithPolicy(t)
6373
6642
  .catch(async (err) => {
6643
+ engagements.delete(t.id);
6374
6644
  const cleanupErrors = [];
6375
6645
  for (const [slot, owner] of workerOwners) {
6376
6646
  if (owner.taskId !== t.id || !liveSlots.has(slot))
@@ -6429,7 +6699,7 @@ export async function runDaemon(repoRoot, opts = {}) {
6429
6699
  journal.telemetry({ taskId: t.id, shape: t.shape, adapter: "-", model: "-", channel: "-", attempts: 0, outcome: "failed", durationMs: 0 });
6430
6700
  await reconcile({ spareLiveLlm: true }); // task-failed is a terminal event
6431
6701
  })
6432
- .finally(() => inflight.delete(t.id));
6702
+ .finally(() => { inflight.delete(t.id); engagements.delete(t.id); });
6433
6703
  inflight.set(t.id, p);
6434
6704
  }
6435
6705
  if (inflight.size === 0) {
@@ -6529,7 +6799,7 @@ export async function runDaemon(repoRoot, opts = {}) {
6529
6799
  poll = setTimeout(pollApprovals, APPROVAL_POLL_MS);
6530
6800
  let tipFailed;
6531
6801
  try {
6532
- const verification = withCommandContext(undefined, () => verifyIntegrationTipCached(intWt, commands, journal, { lastMergedTask, baseline, signal: controller.signal }), controller.signal);
6802
+ const verification = withCommandContext(undefined, () => verifyIntegrationTipCached(intWt, commands, journal, { lastMergedTask, baseline, signal: controller.signal, evidence: { quotaBytes: cfg.gates?.evidenceQuotaBytes } }), controller.signal);
6533
6803
  activeTipVerify = { controller, settled: verification.then(() => { }, () => { }) };
6534
6804
  tipFailed = await verification;
6535
6805
  }