omp-conductor 0.18.2 → 0.19.0

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 (60) hide show
  1. package/README.md +105 -40
  2. package/REFERENCE.md +865 -30
  3. package/package.json +1 -1
  4. package/schema/config.schema.json +26 -0
  5. package/src/admission.ts +212 -26
  6. package/src/ask.ts +288 -1
  7. package/src/briefs/orchestrator.md +6 -5
  8. package/src/cli.ts +5 -1
  9. package/src/command-help.ts +9 -1
  10. package/src/command-manifest.ts +36 -3
  11. package/src/commands/arm.ts +5 -1
  12. package/src/commands/context.ts +2 -0
  13. package/src/commands/message.ts +26 -2
  14. package/src/commands/reconcile-units.ts +104 -0
  15. package/src/commands/release-composition.ts +232 -0
  16. package/src/commands/resume.ts +2 -27
  17. package/src/commands/setup.ts +101 -16
  18. package/src/commands/stats.ts +11 -30
  19. package/src/commands/tail.ts +31 -1
  20. package/src/commands/upgrade.ts +20 -3
  21. package/src/commands/verb.ts +2 -1
  22. package/src/config-schema.ts +19 -0
  23. package/src/config.ts +80 -0
  24. package/src/credential-class.ts +366 -0
  25. package/src/daemon.ts +1218 -288
  26. package/src/dashboard/app.js +504 -2
  27. package/src/dashboard/controls.ts +336 -0
  28. package/src/dashboard/index.html +30 -0
  29. package/src/dashboard/server.ts +271 -30
  30. package/src/dashboard/style.css +116 -0
  31. package/src/dashboard/transcript.ts +173 -0
  32. package/src/doctor.ts +377 -20
  33. package/src/failure-class.ts +59 -0
  34. package/src/fleet.ts +497 -15
  35. package/src/host.ts +6 -130
  36. package/src/omp.ts +29 -0
  37. package/src/orchestrator-tick.ts +343 -88
  38. package/src/pause.ts +233 -0
  39. package/src/settlement.ts +159 -2
  40. package/src/setup-answers.ts +97 -0
  41. package/src/setup-host.ts +321 -1155
  42. package/src/setup-install.ts +204 -27
  43. package/src/setup-wizard.ts +111 -50
  44. package/src/setup.ts +33 -0
  45. package/src/spend-telemetry.ts +117 -0
  46. package/src/stats.ts +35 -0
  47. package/src/status-render.ts +348 -19
  48. package/src/store.ts +1229 -55
  49. package/src/telegram-freshness.ts +269 -0
  50. package/src/to-spec.ts +27 -0
  51. package/src/types.ts +697 -4
  52. package/src/unblock.ts +22 -0
  53. package/src/unit-reconcile.ts +303 -0
  54. package/src/upgrade-verify.ts +8 -1
  55. package/src/upgrade.ts +299 -12
  56. package/src/verbs/actions.ts +124 -10
  57. package/src/verbs/protocol.ts +70 -2
  58. package/src/verbs/server.ts +447 -8
  59. package/src/wake.ts +48 -0
  60. package/src/worker.ts +403 -3
package/src/types.ts CHANGED
@@ -51,6 +51,29 @@ export interface Caps {
51
51
  * still apply). `0` is a hard stop — deliberate, not "unset".
52
52
  */
53
53
  dailySpendUsd: number | null;
54
+ /**
55
+ * Ceiling on what ONE run may spend, and the size of the reservation
56
+ * admission takes out of the daily budget before it launches anything
57
+ * (#851).
58
+ *
59
+ * The daily cap alone is a post-spend stop: it compares spend-to-date
60
+ * against the ceiling at admission time and pauses the fleet on the *next*
61
+ * pass, so one expensive run can cross it by its whole cost before anything
62
+ * notices. On 2026-08-21 a single `@slow` run spent an estimated $25.16 of a
63
+ * $25.00 daily cap unopposed, and the fleet paused only once it had settled.
64
+ *
65
+ * `null` means "derive it": `dailySpendUsd / maxConcurrentWorkers`, the
66
+ * largest per-run allowance that still lets the fleet run at its configured
67
+ * concurrency. Reserving the whole daily cap per run would bound overshoot
68
+ * equally well and silently serialise every fleet on defaults, turning a
69
+ * spend guard into a concurrency change nobody asked for; N reservations of
70
+ * cap/N are still the cap, so nothing is given away. Set it explicitly to
71
+ * bound one run harder. It may never exceed `dailySpendUsd` — a per-run
72
+ * allowance larger than the day's budget is a reservation that can never be
73
+ * satisfied. With `dailySpendUsd: null` the fleet is unmetered and an
74
+ * explicit value still bounds one run.
75
+ */
76
+ maxRunSpendUsd: number | null;
54
77
  /**
55
78
  * Subscription/plan allowance guard, independent of `dailySpendUsd` and
56
79
  * enforced from the provider's own reported usage rather than from a dollar
@@ -708,6 +731,105 @@ export interface ReviewAdjudicationProvenance {
708
731
  readonly resolvedAt: number;
709
732
  }
710
733
 
734
+ /**
735
+ * The lifecycle of one PR's terminal review-ceiling adjudication (#874).
736
+ *
737
+ * The terminal states ARE the verdict — there is deliberately no second
738
+ * `verdict` column beside this one. A state plus an independently-stored verdict
739
+ * is two sources of truth for one fact, and the copy nobody updates is the one a
740
+ * renderer reads. What is stored alongside is what the state cannot say:
741
+ * the evidence that produced it and the disposition applied because of it.
742
+ *
743
+ * - `pending` — recorded, one-shot claimed, no session launched yet.
744
+ * - `running` — an adjudicator session is live on it.
745
+ * - `cleared` — the PR may take the ordinary orchestrator-owned merge path.
746
+ * - `rejected` — it may not; #876 owns the autonomous disposition.
747
+ * - `unavailable-model` — the configured role could not be resolved or
748
+ * launched. Distinct from `failed` because the remedy is configuration, not
749
+ * a retry.
750
+ * - `stale-head` — the PR moved after this adjudication was recorded, so its
751
+ * verdict can no longer describe what would merge.
752
+ * - `failed` — it ran and could not produce a verdict.
753
+ */
754
+ export type ReviewAdjudicationState =
755
+ | "pending"
756
+ | "running"
757
+ | "cleared"
758
+ | "rejected"
759
+ | "unavailable-model"
760
+ | "stale-head"
761
+ | "failed";
762
+
763
+ /** The terminal states: a row in one of these is finished forever, and is the
764
+ * verdict. Exported so the store's transition guard and every reader agree on
765
+ * the same set rather than each spelling its own list. */
766
+ export const TERMINAL_REVIEW_ADJUDICATION_STATES: readonly ReviewAdjudicationState[] = [
767
+ "cleared",
768
+ "rejected",
769
+ "unavailable-model",
770
+ "stale-head",
771
+ "failed",
772
+ ];
773
+
774
+ /**
775
+ * One durable review-ceiling adjudication (#874): the record that makes the
776
+ * final adjudication a lifecycle rather than an in-memory fourth review round.
777
+ *
778
+ * Its identity is the exact reviewed head, and that is the one-shot guarantee:
779
+ * a repeated tick, a racing dispatcher or a daemon restart re-reads this row
780
+ * instead of launching a second adjudicator. A round in `review_revisions` is
781
+ * deliberately NOT reused for this — that would spend a worker review round the
782
+ * ceiling has already exhausted, and would let the same session and model
783
+ * produce a nominal fourth opinion, which is exactly what #873 forbids.
784
+ */
785
+ export interface ReviewAdjudicationRecord {
786
+ id: string;
787
+ project: string;
788
+ issue: number;
789
+ prUrl: string;
790
+ /** The exact head this adjudication is about. The one-shot key, with
791
+ * `project` and `prUrl`: a moved head is a different adjudication, and a
792
+ * verdict never travels to a head it did not read. */
793
+ headSha: string;
794
+ /** The policy's adjudicator role at admission, verbatim — what was asked for,
795
+ * which stays readable even when the role later resolves differently. */
796
+ role: string;
797
+ /** What the role actually resolved to at launch (#875). Absent while
798
+ * `pending`: nothing has resolved a model yet, and recording an intention as
799
+ * provenance would make status claim a launch that never happened. */
800
+ provenance?: ReviewAdjudicationProvenance;
801
+ state: ReviewAdjudicationState;
802
+ /** Why the terminal state is what it is: the adjudicator's verdict text, or
803
+ * the diagnostic for a fail-closed outcome (the two heads of a `stale-head`,
804
+ * the unresolvable role, the failure). */
805
+ evidence?: string;
806
+ /** What was done about the verdict (#876 writes it), kept separate from the
807
+ * verdict itself so "rejected" and "rejected, and here is what happened"
808
+ * are never conflated. */
809
+ disposition?: string;
810
+ requestedAt: number;
811
+ /** Set when an adjudicator session was launched for this row. */
812
+ dispatchedAt?: number;
813
+ settledAt?: number;
814
+ /** The findings that provoked the escalation (#932): what the orchestrator was
815
+ * submitting when `conductor_pr_review` hit the ceiling. Evidence for the
816
+ * adjudicator, deliberately not a `review_revisions` round — a round would
817
+ * spend a ceiling that is already exhausted and would wake an implementer. */
818
+ findings?: string;
819
+ }
820
+
821
+ /**
822
+ * What admitting an adjudication did (#874). Idempotent by construction: the
823
+ * second caller for one head is told the row already exists rather than being
824
+ * allowed to create a twin, and a non-terminal adjudication at a DIFFERENT head
825
+ * on the same PR refuses — that PR moved, and adjudicating two heads at once is
826
+ * the duplicate this lifecycle exists to prevent.
827
+ */
828
+ export type ReviewAdjudicationAdmission =
829
+ | { kind: "created"; record: ReviewAdjudicationRecord }
830
+ | { kind: "existing"; record: ReviewAdjudicationRecord }
831
+ | { kind: "refused"; block: "in-flight-different-head"; record: ReviewAdjudicationRecord };
832
+
711
833
  /**
712
834
  * One durable review-revision request (#677): the orchestrator returned a
713
835
  * green, run-owned pull request to its worker with blocking findings, and
@@ -738,6 +860,12 @@ export interface ReviewRevisionRecord {
738
860
  dispatchedAt?: number;
739
861
  settledAt?: number;
740
862
  outcome?: ReviewRevisionOutcome;
863
+ /** How many times an infrastructure kill has returned this round to the
864
+ * pending set (#903). A dispatch that died before the resumed session took
865
+ * a turn spent no review round, so the row is re-queued rather than settled
866
+ * `failed` — and this counter is what bounds that, durably, so an outage
867
+ * loop cannot retry one round forever. Absent means zero. */
868
+ infraRetries?: number;
741
869
  }
742
870
 
743
871
  /**
@@ -755,6 +883,72 @@ export type ReviewRevisionEnqueue =
755
883
  | { kind: "appended"; record: ReviewRevisionRecord }
756
884
  | { kind: "refused"; block: "pending-different-head" };
757
885
 
886
+ /**
887
+ * How many times one review round may be returned to the pending set after an
888
+ * infrastructure kill before the daemon stops retrying it (#903).
889
+ *
890
+ * The same bound, and the same reasoning, as the run-level infra strike caps:
891
+ * three dispatches killed before the resumed session took a turn mean the host
892
+ * or the harness is broken rather than unlucky, and a fourth retry per tick
893
+ * would loop forever against it. Below the bound the round is free — it spent
894
+ * no worker turn, so charging it against the review-round ceiling would let an
895
+ * outage close a PR's last route to a merge; at the bound it settles `failed`
896
+ * exactly as it does today and escalates once.
897
+ */
898
+ export const REVIEW_ROUND_INFRA_MAX_RETRIES = 3;
899
+
900
+ /**
901
+ * What the store did with a review round whose dispatch was killed by
902
+ * infrastructure (#903). `requeued` cleared the dispatch marker so the next
903
+ * pass resumes the same session on the same round; `exhausted` means the bound
904
+ * is spent and the caller must settle the round the ordinary way.
905
+ */
906
+ export type ReviewRevisionRetry =
907
+ | { kind: "requeued"; retries: number }
908
+ | { kind: "exhausted"; retries: number };
909
+
910
+ /**
911
+ * One recorded review clearance (#913): an explicit orchestrator disposition
912
+ * that the durable review findings standing at one exact head are settled, so
913
+ * `conductor_pr_merge` may proceed at that head.
914
+ *
915
+ * It exists because the #888 merge gate had no reachable clearance at an
916
+ * unchanged head: a re-review at the same head *adds* a blocking round rather
917
+ * than superseding one, and nothing an orchestrator can call settles a round —
918
+ * so a PR at the review-round ceiling was permanently unmergeable. Rows are
919
+ * append-only audit records, and they clear only **backwards**: a clearance
920
+ * suppresses a revision row whose own last activity is at or before the
921
+ * clearance's `at`, never a finding recorded afterwards. It bypasses nothing
922
+ * but the review gate — authority, pause, base-red-freeze, exact-head,
923
+ * green-checks, release composition and single-flight all still apply.
924
+ */
925
+ export interface ReviewClearance {
926
+ id: string;
927
+ project: string;
928
+ prUrl: string;
929
+ /** The exact head the findings were cleared at. Matched case-insensitively. */
930
+ headSha: string;
931
+ at: number;
932
+ /** The caller's role, as the verb channel resolved it. */
933
+ by: string;
934
+ reason: string;
935
+ }
936
+
937
+ /**
938
+ * The durable review evidence standing between one pushed-green PR and a
939
+ * merge at its exact recorded head (#888). Derived from the same
940
+ * `review_revisions` rows `conductor_pr_merge` refuses on, so status can
941
+ * never present a green PR as merge-ready while the privileged verb will
942
+ * (correctly) refuse it.
943
+ */
944
+ export interface ReviewHeadBlocker {
945
+ /** The review round whose findings still stand at this head. */
946
+ round: number;
947
+ /** Why the round still blocks: queued and not yet addressed, dispatched but
948
+ * never finished (crashed mid-review), or settled `failed` at this head. */
949
+ state: "pending" | "crashed" | "failed";
950
+ }
951
+
758
952
  /**
759
953
  * A model-supplied justification: one value out of a closed set, plus prose that
760
954
  * is written down and read by nothing that decides.
@@ -861,6 +1055,36 @@ export interface ProjectConfig {
861
1055
  * when absent or unusable.
862
1056
  */
863
1057
  modelFallbackThreshold?: number;
1058
+ /**
1059
+ * An opaque omp selector for one stronger worker tier, used exactly once per
1060
+ * issue chain when a run spun to a cap with nothing to show (#807).
1061
+ *
1062
+ * Conductor never resolves it: `@slow`, a role name or a bare model pattern
1063
+ * are all the same string here, handed to the harness at dispatch the way
1064
+ * {@link workerModel} is. It is deliberately NOT part of
1065
+ * {@link modelFallbacks} — that chain answers provider faults and must stay
1066
+ * provider-only — and absent or blank preserves today's settle/escalate
1067
+ * behaviour byte for byte.
1068
+ */
1069
+ workerEscalationModel?: string;
1070
+ /**
1071
+ * Providers whose routes must bill to a subscription (OAuth) credential, and
1072
+ * never to an API key (#852).
1073
+ *
1074
+ * Declared per **provider**, not per model or per run, because that is the only
1075
+ * truthful unit: a credential belongs to a provider — a model has none of its
1076
+ * own — and a run's provider is not knowable in advance, since
1077
+ * {@link workerModel} is an opaque selector that may be a role alias and the
1078
+ * provider-fault fallback chain (#286) can move a live run onto another
1079
+ * provider mid-flight. So the declaration reads "this provider must bill to its
1080
+ * subscription", and every candidate is held while that is untrue.
1081
+ *
1082
+ * Enforced by conductor rather than omp because omp cannot express it:
1083
+ * `AuthStorage.getApiKey` cascades from a disabled OAuth credential into the
1084
+ * same provider's API key, and no setting, selector suffix or request option
1085
+ * stops it. Absent or empty dispatches byte-for-byte as it always has.
1086
+ */
1087
+ requireOauthProviders?: readonly string[];
864
1088
  /**
865
1089
  * An opaque omp settings map layered into every worker session this project
866
1090
  * dispatches (#537). At dispatch it is materialised verbatim to a fleet-owned
@@ -1467,9 +1691,23 @@ export type FailureClass = (typeof FAILURE_CLASSES)[number];
1467
1691
  /**
1468
1692
  * What the daemon does about a class.
1469
1693
  *
1470
- * `none` completes the vocabulary rather than naming an outcome: an
1471
- * unclassifiable run escalates and never silently retries, so nothing in the
1472
- * decision table maps to it.
1694
+ * Two of these perform nothing, and they are not the same nothing:
1695
+ *
1696
+ * - `hold` recorded and deliberately left alone, because acting would destroy
1697
+ * something. `orphan-dirty` is the case: the tree is the only copy, so
1698
+ * `unblock` refuses rather than re-claiming over it.
1699
+ * - `none` — the classifier declining to name a failure at all. Two callers
1700
+ * reach it, and neither is "unclassifiable": `classifyRun` returns it for a
1701
+ * terminal row whose PR is open and fully green, where `classifyAndRecover`
1702
+ * restores `pushed-green` *without* persisting a class (#766); and the settle
1703
+ * sweep persists it beside `returned-for-revision` when a reviewer closes
1704
+ * pushed work without merging, where the remedy is the queue label the sweep
1705
+ * deliberately leaves on.
1706
+ *
1707
+ * An *unclassifiable* run is `unknown` → `escalate`, and never a silent retry.
1708
+ * This comment previously said nothing in the decision table maps to `none`,
1709
+ * which both callers above contradict; it cost a reading of two modules to
1710
+ * discover, which is what a wrong comment costs (#132).
1473
1711
  */
1474
1712
  export const RECOVERY_ACTIONS = [
1475
1713
  "requeue",
@@ -1529,6 +1767,48 @@ export interface BaseFreeze {
1529
1767
  clearedReason?: string;
1530
1768
  }
1531
1769
 
1770
+ /**
1771
+ * The active release composition for one project (#850): the campaign whose
1772
+ * release is being assembled, and the exact PRs allowed into it. While
1773
+ * `closedAt` is absent the composition is active and `prMergeVerb` refuses
1774
+ * every `conductor_pr_merge` whose PR is neither named here nor covered by a
1775
+ * recorded operator override, with the `outside-active-release` refusal — the
1776
+ * mechanical form of "a patch contains no unrelated merges". The row is
1777
+ * SQLite-backed, so the guard survives daemon and orchestrator restarts, and
1778
+ * it retires only through an explicit completion or cancellation; each
1779
+ * transition also writes an immutable material event as its audit trail.
1780
+ */
1781
+ export interface ReleaseComposition {
1782
+ /** The campaign identifier, e.g. `v0.18.1-reliability`. */
1783
+ campaign: string;
1784
+ /** Full PR URLs explicitly allowed into this release. */
1785
+ allowedPrUrls: string[];
1786
+ declaredAt: number;
1787
+ declaredBy?: string;
1788
+ /** When the composition retired — absent means it is active. */
1789
+ closedAt?: number;
1790
+ /** `operator` via the CLI on every path that retires a composition. */
1791
+ closedBy?: string;
1792
+ closedReason?: string;
1793
+ }
1794
+
1795
+ /**
1796
+ * One operator override admitting exactly one PR into one release (#850).
1797
+ * Rows are append-only audit records: an override applies to its campaign
1798
+ * only, never outlives it, and bypasses nothing but the composition check —
1799
+ * authority, pause, base-red-freeze, exact-head, green-checks and
1800
+ * single-flight all still apply to the overridden merge.
1801
+ */
1802
+ export interface ReleaseCompositionOverride {
1803
+ id: string;
1804
+ project: string;
1805
+ campaign: string;
1806
+ prUrl: string;
1807
+ at: number;
1808
+ by: string;
1809
+ reason: string;
1810
+ }
1811
+
1532
1812
  /**
1533
1813
  * Execution state is separate from the tracker's own labels on purpose: labels
1534
1814
  * are coarse and human-editable, while the loop needs to distinguish "pushed
@@ -1649,6 +1929,18 @@ export interface RunRecord {
1649
1929
  /** Effective turn ceiling for this run; operators may only raise it. */
1650
1930
  maxTurns: number;
1651
1931
  spendUsd: number;
1932
+ /**
1933
+ * The per-run spend allowance admission reserved out of the daily budget
1934
+ * before this run launched (#851). Held for as long as the row is live, and
1935
+ * released the moment it is not — reservations are summed over live rows, so
1936
+ * a settle releases the unspent remainder with no bookkeeping to forget and
1937
+ * a daemon restart re-reads the same reservations off the same rows.
1938
+ *
1939
+ * Absent means the row predates the reservation, or was admitted while the
1940
+ * fleet had no spend cap at all. Never "reserved nothing": a run admitted
1941
+ * under a cap always records what it reserved.
1942
+ */
1943
+ spendReservedUsd?: number;
1652
1944
  /** How many in-session HTTP 429 responses the harness retried before the run
1653
1945
  * ended, counted by the worker from the session's `message_end` events while
1654
1946
  * the run was live (#573). Absent means the column predates the count or no
@@ -1684,6 +1976,42 @@ export interface RunRecord {
1684
1976
  autoCompactionCount?: number;
1685
1977
  /** omp session transcript, so a human can read what the worker actually did. */
1686
1978
  sessionFile?: string;
1979
+ /**
1980
+ * The exact `session-host` pid this run's Herdr representation was reported
1981
+ * for, and the pane Herdr knows it as (#842).
1982
+ *
1983
+ * Durable so a restart can find the pane its dead worker left behind, rather
1984
+ * than leaving a workspace showing a live worker that no longer exists. All
1985
+ * three are absent together — a run with a pane always has a pid, because the
1986
+ * pid is what the pane was opened from.
1987
+ *
1988
+ * A matching pid is deliberately NOT treated as proof of life: pids are reused,
1989
+ * and a session-host child dies with the daemon that owns its socket, so a live
1990
+ * row after a restart is an orphan whatever pid it recorded.
1991
+ */
1992
+ workerPid?: number;
1993
+ paneId?: string;
1994
+ paneLabel?: string;
1995
+ /**
1996
+ * Why this run has no workspace representation right now (#841).
1997
+ *
1998
+ * Present only beside a live run with no pane, and cleared the moment one is
1999
+ * established. This is what makes Herdr unavailability a *named* degraded state
2000
+ * rather than an absence: `status` reads it, so a fleet running blind says so
2001
+ * instead of looking identical to a fleet with no workers.
2002
+ */
2003
+ paneUnavailable?: string;
2004
+ /**
2005
+ * Cumulative output and reasoning tokens for this run (#518).
2006
+ *
2007
+ * Both are read off the same `usage` block the spend already comes from, so
2008
+ * they cost no extra provider call. Their ratio is the fact that explains a run
2009
+ * reading `24/180 turns $0.05` after 43 minutes: 96% of its output was
2010
+ * reasoning. Absent rather than zero when a provider reports no token block —
2011
+ * a share computed from a fabricated zero is worse than no share.
2012
+ */
2013
+ outputTokens?: number;
2014
+ reasoningTokens?: number;
1687
2015
  /** The run id of the orphan-clean attempt whose session this row resumes
1688
2016
  * (#536, #567). Set only when the dispatch-time resume verdict fired;
1689
2017
  * absent means a fresh dispatch, never "resumed from unknown". Readable
@@ -1796,6 +2124,25 @@ export interface TurnOverride {
1796
2124
  setAt: number;
1797
2125
  }
1798
2126
 
2127
+ /**
2128
+ * The one-shot model escalation a run chain has already spent (#807).
2129
+ *
2130
+ * Written before the continuation it authorises is queued, and never removed:
2131
+ * its presence is both "this chain dispatches on the stronger tier" and "this
2132
+ * chain may never escalate again", so a restart mid-recovery cannot hand the
2133
+ * same chain a second tier.
2134
+ */
2135
+ export interface ModelEscalation {
2136
+ /** The opaque selector the chain escalated to — the project's
2137
+ * `workerEscalationModel` as configured when the marker was claimed. */
2138
+ model: string;
2139
+ /** The spinning-cap class that bought the escalation. */
2140
+ failureClass: FailureClass;
2141
+ /** The run whose settlement claimed it. */
2142
+ runId: string;
2143
+ at: number;
2144
+ }
2145
+
1799
2146
  export type AdmissionHoldReason =
1800
2147
  | "capacity"
1801
2148
  | "issue-active"
@@ -1815,7 +2162,20 @@ export type AdmissionHoldReason =
1815
2162
  | "open-pr"
1816
2163
  | "unsalvaged-wip"
1817
2164
  | "daily-spend-cap"
2165
+ /** The candidate's own per-run spend reservation does not fit in what is
2166
+ * left of the daily budget once spend-to-date and every live run's
2167
+ * reservation are counted (#851). Distinct from `daily-spend-cap`, which is
2168
+ * the fleet-stopping "today is already spent" verdict: this one holds a
2169
+ * candidate while admitted work is still running, and clears by itself as
2170
+ * those runs settle for less than they reserved. */
2171
+ | "spend-reservation"
1818
2172
  | "plan-usage-cap"
2173
+ /** A provider this project requires to bill to its subscription is currently
2174
+ * resolving to something else, or could not be verified (#852). Fleet-wide
2175
+ * like `plan-usage-cap`, and self-clearing: the operator re-authenticates and
2176
+ * the next pass admits. The hold's detail carries which provider, what it
2177
+ * resolves to, and the remediation. */
2178
+ | "credential-class"
1819
2179
  | "shutting-down"
1820
2180
  | "stale-base"
1821
2181
  /** A ready issue whose lifecycle state label is residual: its newest run is
@@ -2042,6 +2402,42 @@ export interface ReportEnqueue {
2042
2402
  * stay first, so a busy day drains deterministically over later digests. */
2043
2403
  export const DIGEST_BACKLOG_LIMIT = 20;
2044
2404
 
2405
+ /**
2406
+ * The three install identities one host carried at one moment (#919).
2407
+ *
2408
+ * Recorded by the daemon's dispatch pass rather than probed at render time:
2409
+ * reading them costs three subprocesses (`omp-conductor --version`, `omp plugin
2410
+ * list --json`, `herdr … plugin list`), and calling that from the tick took the
2411
+ * tick suite from 8.4s to 83.4s — three spawns every fifteen minutes, forever,
2412
+ * to answer a question whose answer changes only when someone installs
2413
+ * something. So the surfaces that an operator and an agent actually read
2414
+ * consult this row and spawn nothing.
2415
+ *
2416
+ * A missing `ompVersion` or `herdrSource` is "this host does not carry that
2417
+ * surface", which `doctor` reports with full nuance. Only a proven
2418
+ * disagreement — two different releases live at once — is a fault the cheap
2419
+ * surfaces name.
2420
+ */
2421
+ export interface InstallSurfaceObservation {
2422
+ /** When the daemon read the surfaces. */
2423
+ at: number;
2424
+ cliVersion: string;
2425
+ ompVersion?: string;
2426
+ herdrSource?: string;
2427
+ /**
2428
+ * The `omp-telegram` peer's three versions (#961), each absent when that
2429
+ * surface did not answer.
2430
+ *
2431
+ * Absent is not agreement. A pass with no registry access records the two it
2432
+ * could read and leaves `telegramPublished` off, and every renderer must show
2433
+ * that as unverified — the whole point being that a stale plugin silently
2434
+ * broke a contract the brief mandates, and "no finding" is what let it.
2435
+ */
2436
+ telegramInstalled?: string;
2437
+ telegramDaemon?: string;
2438
+ telegramPublished?: string;
2439
+ }
2440
+
2045
2441
  /** One ordinary material outcome waiting for a rendered digest (#274). */
2046
2442
  export interface MaterialEvent {
2047
2443
  id: string;
@@ -2196,6 +2592,21 @@ export interface DecisionRecord {
2196
2592
  blocks?: string;
2197
2593
  askedAt: number;
2198
2594
  expiresAt: number;
2595
+ /**
2596
+ * The questionnaire this row belongs to (#947): N rows written in one
2597
+ * transaction, delivered as one message, resolved independently. Absent on
2598
+ * every ordinary single ask, which is why the column is nullable rather than
2599
+ * defaulted — a historical row was never part of a group and must not read as
2600
+ * a group of one.
2601
+ */
2602
+ groupId?: string;
2603
+ /**
2604
+ * The issue being specced when this question was asked (#947). A
2605
+ * questionnaire's whole point is that its answers are the spec's provenance,
2606
+ * so a later reader can see why a slice is shaped the way it is instead of
2607
+ * re-litigating it; a group without one is refused at the store boundary.
2608
+ */
2609
+ specIssue?: number;
2199
2610
  /** Raw machine-checkable precondition, e.g. `pr-merged:<url>`. Stored as
2200
2611
  * written and parsed on read, so a grammar this build does not know is a row
2201
2612
  * that still lists rather than a row that fails to load. */
@@ -2226,6 +2637,10 @@ export interface DecisionDraft {
2226
2637
  kind?: DecisionKind;
2227
2638
  blocks?: string;
2228
2639
  condition?: string;
2640
+ /** Set by {@link Store.createDecisionGroup}; never by a single ask (#947). */
2641
+ groupId?: string;
2642
+ /** The issue this question specs out (#947). Required inside a group. */
2643
+ specIssue?: number;
2229
2644
  at: number;
2230
2645
  }
2231
2646
 
@@ -2281,6 +2696,19 @@ export interface Store {
2281
2696
  activeRuns(project: string): RunRecord[];
2282
2697
  /** Runs backed by a worker process — what capacity counts. Subset of {@link Store.activeRuns}. */
2283
2698
  liveRuns(project: string): RunRecord[];
2699
+ /**
2700
+ * Runs holding an **active mutation lease** (#899, #925): something is
2701
+ * writing through them right now — a live worker, or a review revision the
2702
+ * daemon has dispatched and that has not settled. Narrower than
2703
+ * {@link Store.activeRuns}, whose worker-free `pushed-*` rows own an issue
2704
+ * and an artifact but no longer own their files.
2705
+ *
2706
+ * This is the one answer to "may this file be written", and it is a single
2707
+ * query because two independent readers ask it — admission's file-lane gate
2708
+ * and `conductor_pr_recover`'s fail-closed lane check — and a lease rule that
2709
+ * disagreed between them would be the exact bug both exist to prevent.
2710
+ */
2711
+ leasedRuns(project: string): RunRecord[];
2284
2712
  /** Failed/killed/orphaned rows whose retained tree has not been reaped. */
2285
2713
  retainedRuns(project: string): RunRecord[];
2286
2714
  /** Newest attempt per issue for the live board. Non-merged work remains
@@ -2325,11 +2753,107 @@ export interface Store {
2325
2753
  * marker) so the next dispatch pass wakes it again — the reopen half of
2326
2754
  * restart recovery, paired with restoring its run to `pushed-green`. */
2327
2755
  requeueReviewRevision(id: string): void;
2756
+ /** Return one round to the pending set after an infrastructure kill, and
2757
+ * count it (#903), or report the bound spent. One transaction: the count is
2758
+ * read and written with the requeue, so two daemons cannot both spend the
2759
+ * same retry. `max` is {@link REVIEW_ROUND_INFRA_MAX_RETRIES}, passed in so
2760
+ * the bound stays a caller's policy rather than a number buried in SQL. A
2761
+ * settled row is never revived — `exhausted` is the honest answer for one. */
2762
+ retryReviewRevisionAfterInfra(id: string, max: number): ReviewRevisionRetry;
2328
2763
  /** Atomically take a settled green run out of reviewable state:
2329
2764
  * `pushed-green` → `running`, and only from `pushed-green`. False when the
2330
2765
  * row already moved (a prior revision was dispatched, the PR settled), so
2331
2766
  * two concurrent dispatchers cannot both wake one run. */
2332
2767
  claimRunForReview(runId: string): boolean;
2768
+ /** Every review-revision row that blocks merging one PR at one exact head
2769
+ * (#888): rows not yet settled (queued, or dispatched and crashed
2770
+ * mid-review) and rows settled `failed` at that head, oldest first.
2771
+ * Keyed by project + PR + head — never by a label or eventually consistent
2772
+ * issue search — so a finding tied only to an older head never poisons a
2773
+ * corrected new head. A row is excluded when a {@link ReviewClearance} for
2774
+ * the same project + PR + head was recorded at or after that row's own last
2775
+ * activity (#913) — the clearance is the one reachable disposition at an
2776
+ * unchanged head, and the timestamp bound keeps it from blessing findings
2777
+ * recorded after it. */
2778
+ mergeBlockingReviews(project: string, prUrl: string, headSha: string): ReviewRevisionRecord[];
2779
+ /** Append one review clearance for an exact head (#913). Append-only: a
2780
+ * second clearance at the same head is another audit row, never an update. */
2781
+ recordReviewClearance(clearance: {
2782
+ project: string;
2783
+ prUrl: string;
2784
+ headSha: string;
2785
+ by: string;
2786
+ reason: string;
2787
+ at?: number;
2788
+ }): ReviewClearance;
2789
+ /** Clearances recorded for one PR at one exact head, newest first (#913).
2790
+ * Head comparison is case-insensitive, exactly as the merge gate's. */
2791
+ reviewClearances(project: string, prUrl: string, headSha: string): ReviewClearance[];
2792
+ /**
2793
+ * Admit one terminal review-ceiling adjudication for an exact PR head (#874),
2794
+ * idempotently. The second caller for the same head is handed the existing
2795
+ * row (`existing`), and a non-terminal adjudication on the same PR at a
2796
+ * DIFFERENT head refuses (`in-flight-different-head`) — the PR moved, and two
2797
+ * live adjudications on one PR is the duplicate this lifecycle prevents.
2798
+ *
2799
+ * Never consumes a review round: this is a separate table from
2800
+ * `review_revisions` precisely so a ceiling that is already exhausted is not
2801
+ * charged again.
2802
+ */
2803
+ openReviewAdjudication(draft: {
2804
+ project: string;
2805
+ issue: number;
2806
+ prUrl: string;
2807
+ headSha: string;
2808
+ role: string;
2809
+ requestedAt?: number;
2810
+ /** The findings that provoked the escalation, when a caller has them. */
2811
+ findings?: string;
2812
+ }): ReviewAdjudicationAdmission;
2813
+ /** `pending` → `running`, recording what the role actually resolved to at
2814
+ * launch. The transition is the WHERE clause, so two dispatchers racing one
2815
+ * row cannot both launch: the loser changes nothing and reads `false`. */
2816
+ markReviewAdjudicationRunning(
2817
+ id: string,
2818
+ provenance: ReviewAdjudicationProvenance,
2819
+ at?: number,
2820
+ ): boolean;
2821
+ /** Settle into a terminal state, once, with the evidence that produced it.
2822
+ * A non-terminal target state or an already-settled row changes nothing and
2823
+ * reads `false`, so an invalid transition preserves the diagnostic the row
2824
+ * already carried instead of overwriting it. */
2825
+ settleReviewAdjudication(
2826
+ id: string,
2827
+ state: ReviewAdjudicationState,
2828
+ evidence?: string,
2829
+ at?: number,
2830
+ ): boolean;
2831
+ /** Record what was done about a verdict (#876's half), kept separate from the
2832
+ * verdict so the two are never conflated. */
2833
+ recordReviewAdjudicationDisposition(id: string, disposition: string): boolean;
2834
+ /** One adjudication by id. */
2835
+ reviewAdjudication(id: string): ReviewAdjudicationRecord | undefined;
2836
+ /** The adjudication for one exact PR head, if any. Case-insensitive on the
2837
+ * SHA, exactly as the merge gate and the clearance lookup are. */
2838
+ reviewAdjudicationForHead(
2839
+ project: string,
2840
+ prUrl: string,
2841
+ headSha: string,
2842
+ ): ReviewAdjudicationRecord | undefined;
2843
+ /** Every non-terminal adjudication for a project, oldest first: what a daemon
2844
+ * restart re-reads instead of launching a second adjudicator, and what status
2845
+ * projects as pending/running. */
2846
+ openReviewAdjudications(project: string): ReviewAdjudicationRecord[];
2847
+ /** Every adjudication recorded for one PR, newest first. */
2848
+ reviewAdjudicationsForPr(project: string, prUrl: string): ReviewAdjudicationRecord[];
2849
+ /**
2850
+ * Every review round recorded for one PR, oldest first (#932): the history an
2851
+ * adjudication is shown. Keyed by PR rather than by run so a continuation that
2852
+ * inherited the PR cannot hide its predecessor's findings — and a finding
2853
+ * without its settled outcome reads as an outstanding complaint even when the
2854
+ * worker fixed it, so both travel together.
2855
+ */
2856
+ reviewRevisionsForPr(project: string, prUrl: string): ReviewRevisionRecord[];
2333
2857
  /** Merged rows whose post-merge workflow verdict is still pending, oldest first. */
2334
2858
  runsNeedingBaseCheck(project: string, limit?: number): RunRecord[];
2335
2859
  /** Replace the current live-head health row for one routed repository. */
@@ -2370,6 +2894,44 @@ export interface Store {
2370
2894
  reason: string,
2371
2895
  at?: number,
2372
2896
  ): boolean;
2897
+ /** The project's active release composition, if one is declared (#850). */
2898
+ activeReleaseComposition(project: string): ReleaseComposition | undefined;
2899
+ /**
2900
+ * Declare the active release composition for a project (#850). One at a
2901
+ * time per project: returns the still-active conflicting composition
2902
+ * unchanged when one exists (the caller refuses), `undefined` when the
2903
+ * declaration took. Replaces only already-closed rows.
2904
+ */
2905
+ declareReleaseComposition(
2906
+ project: string,
2907
+ composition: {
2908
+ campaign: string;
2909
+ allowedPrUrls: string[];
2910
+ declaredBy?: string;
2911
+ declaredAt?: number;
2912
+ },
2913
+ ): ReleaseComposition | undefined;
2914
+ /**
2915
+ * Retire the active composition as completed (#850). Records who closed it
2916
+ * and why on the row itself. Returns true only when an active composition
2917
+ * was actually retired, so the CLI event fires once.
2918
+ */
2919
+ completeReleaseComposition(project: string, by: string, reason: string, at?: number): boolean;
2920
+ /** Retire the active composition as cancelled (#850), same contract as completion. */
2921
+ cancelReleaseComposition(project: string, by: string, reason: string, at?: number): boolean;
2922
+ /** Append one operator override admitting `prUrl` into `campaign` (#850). */
2923
+ grantReleaseCompositionOverride(
2924
+ project: string,
2925
+ override: { campaign: string; prUrl: string; by: string; reason: string; at?: number },
2926
+ ): ReleaseCompositionOverride;
2927
+ /** The newest live override for one PR in one campaign, if recorded (#850). */
2928
+ releaseCompositionOverride(
2929
+ project: string,
2930
+ campaign: string,
2931
+ prUrl: string,
2932
+ ): ReleaseCompositionOverride | undefined;
2933
+ /** Every override recorded for one campaign, newest first (#850). */
2934
+ releaseCompositionOverrides(project: string, campaign: string): ReleaseCompositionOverride[];
2373
2935
  /** Newest attempt per issue that preserved work or failed to, so `status`
2374
2936
  * can name every WIP tip a re-claim would build on and every tree that is
2375
2937
  * still the only copy. */
@@ -2396,6 +2958,29 @@ export interface Store {
2396
2958
  /** Every attempt for one issue, oldest first, so an escalation can group the
2397
2959
  * continuation budget by failure class (#439). */
2398
2960
  runsForIssue(project: string, issue: number): RunRecord[];
2961
+ /**
2962
+ * Spends the single model escalation an issue chain may buy (#807): the
2963
+ * one-shot marker, the `swapFrom` → `queueLabel` hand-back that the
2964
+ * escalation authorises, and the settling run's recovery stamp, all in one
2965
+ * transaction. `true` when this call spent it, `false` when the chain had
2966
+ * already escalated and nothing was written — the second-cap decomposition
2967
+ * verdict. Atomic on purpose: a restart must never find a chain that reads
2968
+ * as escalated with the continuation it promised never queued. Throws when
2969
+ * `runId` names no row.
2970
+ */
2971
+ escalateModel(escalation: {
2972
+ project: string;
2973
+ issue: number;
2974
+ model: string;
2975
+ failureClass: FailureClass;
2976
+ runId: string;
2977
+ swapFrom: string;
2978
+ queueLabel: string;
2979
+ at?: number;
2980
+ }): boolean;
2981
+ /** The escalation this chain has already spent; `undefined` for every chain
2982
+ * that never bought one, which is what dispatch reads for a fresh issue. */
2983
+ modelEscalation(project: string, issue: number): ModelEscalation | undefined;
2399
2984
  /** Persist a one-shot ceiling and append the operator action to its audit ledger. */
2400
2985
  setTurnOverride(project: string, issue: number, maxTurns: number, setAt?: number): void;
2401
2986
  turnOverride(project: string, issue: number): number | undefined;
@@ -2409,11 +2994,26 @@ export interface Store {
2409
2994
  ): TurnOverride[];
2410
2995
  runsStartedSince(project: string, sinceEpochMs: number): number;
2411
2996
  spendSince(project: string, sinceEpochMs: number): number;
2997
+ /** Total per-run allowance currently reserved by LIVE runs (#851). What
2998
+ * admission must subtract from the daily budget on top of spend-to-date:
2999
+ * a run that has spent little has still committed its whole allowance, and
3000
+ * reading the sum off the live rows means a settle releases the remainder
3001
+ * and a restart re-reads it rather than losing it. */
3002
+ reservedSpendUsd(project: string): number;
2412
3003
  /** The rows one stats window needs (#282): every run settling at or after
2413
3004
  * `sinceEpochMs`, plus the full attempt chain of each issue whose merge
2414
3005
  * settled there (so continuation chains collapse into one journey), ordered
2415
3006
  * by issue then startedAt. Read-only. */
2416
3007
  statsRuns(project: string, sinceEpochMs: number): RunRecord[];
3008
+ /**
3009
+ * The newest terminal runs, as `{ turns, spendUsd }`, for judging whether the
3010
+ * spend cap's input is still arriving (#970).
3011
+ *
3012
+ * Bounded by `limit` rows, not by `limit` *working* runs: the judgement
3013
+ * discards 0-turn rows, so the caller over-samples and lets the shared
3014
+ * predicate pick the window. Read-only.
3015
+ */
3016
+ recentSpendSamples(project: string, limit: number): { turns: number; spendUsd: number }[];
2417
3017
  /** Total tracked `gh` calls between two UTC day keys, inclusive (#198). */
2418
3018
  ghCallsBetween(sinceDay: string, untilDay: string): number;
2419
3019
  /** Idempotence guard so a retry loop cannot page a human repeatedly for the
@@ -2443,6 +3043,14 @@ export interface Store {
2443
3043
  categories?: readonly InterruptCategory[],
2444
3044
  urgent?: boolean,
2445
3045
  ): HeldNotice[];
3046
+ /** Record the install identities this host carried on this pass (#919).
3047
+ * One row, replaced: the question is "what is installed now", and a history
3048
+ * of installs is what the upgrade journal already keeps. */
3049
+ recordInstallSurfaces(observation: InstallSurfaceObservation): void;
3050
+ /** The newest recorded install-surface observation, or `undefined` when no
3051
+ * pass has read them yet — which the surfaces must render as "not observed",
3052
+ * never as agreement (#919). */
3053
+ installSurfaces(): InstallSurfaceObservation | undefined;
2446
3054
  /** Persist one ordinary material outcome without sending it. */
2447
3055
  recordMaterialEvent(event: MaterialEventDraft): MaterialEvent;
2448
3056
  getMaterialEvent(id: string): MaterialEvent | undefined;
@@ -2477,6 +3085,21 @@ export interface Store {
2477
3085
  project: string,
2478
3086
  holds: readonly { issue: number; reason: AdmissionHoldReason; detail?: string }[],
2479
3087
  ): void;
3088
+ /**
3089
+ * Delete every grooming row whose issue is not in a **complete** open-issue
3090
+ * snapshot, returning the issues retired (#964).
3091
+ *
3092
+ * A verdict describes an open issue: "promotable" means "promote this", and a
3093
+ * closed issue cannot be promoted. Nothing retired one before, so the rows
3094
+ * accumulated and `status` reported a groomed backlog that did not exist —
3095
+ * measured at 64 of 65 rows, including all 26 promotable ones.
3096
+ *
3097
+ * The completeness of the snapshot is the caller's contract, not this
3098
+ * method's: pass the result of `Tracker.listOpenIssues` (which throws rather
3099
+ * than returning a short list), never a label-filtered read. A partial
3100
+ * snapshot here deletes verdicts nobody re-derived.
3101
+ */
3102
+ retireGroomingNotOpen(project: string, openIssues: readonly number[]): number[];
2480
3103
  /** Count and age source for status and digest prompt bounds. */
2481
3104
  digestBacklog(project: string): DigestBacklog;
2482
3105
  /** Add one bounded observation to the per-day friction rollup. */
@@ -2644,8 +3267,24 @@ export interface Store {
2644
3267
  * into recovery. False when the row was no longer `ci-deterministic`: a
2645
3268
  * no-op, exactly what a second repair pass must be. */
2646
3269
  reclassifyInfra(id: string): boolean;
2647
- /** Unrecovered rows per class, for `status`. Empty when nothing is carrying one. */
3270
+ /**
3271
+ * Rows still **awaiting a recovery the sweep will take**, per class (#132).
3272
+ *
3273
+ * Excludes the recorded-only actions (`none`, `hold`), which never stamp
3274
+ * `recoveredAt` by design — see {@link Store.recordedOnlyClassCounts}. Empty
3275
+ * when nothing is waiting, which is the healthy state and reads as one.
3276
+ */
2648
3277
  failureClassCounts(project: string): { cls: FailureClass; n: number }[];
3278
+ /**
3279
+ * Rows classified but deliberately not acted on, per class (#132): the
3280
+ * `none`/`hold` actions.
3281
+ *
3282
+ * A separate reader because these are a *signal*, not a backlog — 44
3283
+ * `returned-for-revision` rows say something real about review outcomes, and
3284
+ * nothing about work that is stuck. Counting them as unrecovered is what hid
3285
+ * every actionable class behind a monotonically growing tally.
3286
+ */
3287
+ recordedOnlyClassCounts(project: string): { cls: FailureClass; n: number }[];
2649
3288
  /** Rows whose recovery ran at or after `since`, newest first — the tick's
2650
3289
  * "auto-recovered since last tick" line, and its count. */
2651
3290
  recoveredSince(project: string, since: number): RunRecord[];
@@ -2655,6 +3294,17 @@ export interface Store {
2655
3294
  * remembering things.
2656
3295
  */
2657
3296
  createDecision(draft: DecisionDraft): DecisionRecord;
3297
+ /**
3298
+ * Record one spec-out questionnaire (#947): every item durable in a single
3299
+ * transaction, before its single delivery, sharing a minted group id and the
3300
+ * issue being specced. Refuses an empty batch and a non-issue spec binding —
3301
+ * a questionnaire with nothing waiting on it, or with no spec, is a caller
3302
+ * bug rather than a degenerate case worth supporting.
3303
+ */
3304
+ createDecisionGroup(drafts: readonly DecisionDraft[], specIssue: number): DecisionRecord[];
3305
+ /** One questionnaire's rows in asked order, whatever their states — the
3306
+ * per-item view a digest renders and a timeout pass resolves against. */
3307
+ decisionGroup(groupId: string): DecisionRecord[];
2658
3308
  /** Everything still owed an answer, oldest first — what a tick digest reads. */
2659
3309
  openDecisions(project: string): DecisionRecord[];
2660
3310
  /** One row by id, whatever its state — what a bounded ask polls while waiting. */
@@ -2889,6 +3539,10 @@ export const DEFAULT_CAPS: Caps = {
2889
3539
  maxConcurrentWorkers: 2,
2890
3540
  maxConcurrentWorkersPerRepo: 1,
2891
3541
  dailySpendUsd: 25,
3542
+ // Derived as `dailySpendUsd / maxConcurrentWorkers` unless an operator names
3543
+ // a per-run allowance: bounded overshoot without changing how many workers
3544
+ // the fleet runs (#851).
3545
+ maxRunSpendUsd: null,
2892
3546
  // Off unless an operator names a window. A default threshold would need a
2893
3547
  // default window id, and guessing which allowance a fleet lives on is how a
2894
3548
  // guard silently watches the wrong meter (#110).
@@ -2923,6 +3577,11 @@ export const VERB_NAMES = [
2923
3577
  * OMP session on the existing branch and PR. No close, no reopen, no
2924
3578
  * redispatch. */
2925
3579
  "conductor_pr_review",
3580
+ /** Record that the durable review findings standing at one exact head are
3581
+ * settled, so the #888 merge gate stops refusing that head (#913). A
3582
+ * separate verb, never an argument on the merge: the disposition is its own
3583
+ * recorded act, and it clears only rows older than itself. */
3584
+ "conductor_pr_review_clear",
2926
3585
  /** The orchestrator-only, settled-run recovery operation (#806): open (or
2927
3586
  * adopt) the missing pull request for a terminal run whose stored branch
2928
3587
  * exists at its exact recorded head. The recovery proves everything a
@@ -3024,6 +3683,15 @@ export const VERB_REFUSALS = [
3024
3683
  /** The routed repository a terminal run recorded no longer has an entry in
3025
3684
  * this project's routing, so the daemon cannot create a PR for it. */
3026
3685
  "recovery-repo-unrouted",
3686
+ /** A live mutation lease already holds a file the preserved branch carries
3687
+ * (#925): publishing it now would put a frozen diff into review beside a
3688
+ * worker still rewriting the same file. Retryable — the lease releases when
3689
+ * that worker settles. */
3690
+ "recovery-lane-occupied",
3691
+ /** The lane a recovery would publish could not be read (#925). Unlike
3692
+ * admission's fail-open interlock, a single deliberate recovery call refuses
3693
+ * on "could not tell" rather than publishing on an unproven lane. */
3694
+ "recovery-lane-unprovable",
3027
3695
  /** The run's durable `prUrl` is a non-URL value the tracker cannot address
3028
3696
  * (other than the normalized `pending` settlement sentinel, #866). There is
3029
3697
  * no PR to read and none to guess at — a corrupt record, not an unreadable
@@ -3068,6 +3736,31 @@ export const VERB_REFUSALS = [
3068
3736
  * repos are unaffected.
3069
3737
  */
3070
3738
  "base-red-freeze",
3739
+ /**
3740
+ * The requested merge head carries durable, unresolved review evidence
3741
+ * (#888): a review revision for this PR that is queued or crashed at
3742
+ * exactly that head, or one settled `failed` there. Green checks do not
3743
+ * clear it — the head must be re-reviewed with `conductor_pr_review` or
3744
+ * superseded by a corrected push to a different head.
3745
+ */
3746
+ "merge-blocked-by-review",
3747
+ /**
3748
+ * A review clearance was asked for at a head where nothing blocks (#913).
3749
+ * A clearance is a disposition of specific evidence, so recording one "just
3750
+ * in case" — before a round exists, or after a corrected push moved the
3751
+ * head — is refused rather than stored as a standing pass.
3752
+ */
3753
+ "review-clearance-vacuous",
3754
+ /**
3755
+ * The project has an active release composition (#850) — a declared
3756
+ * campaign plus the exact PRs allowed into it — and this PR is neither,
3757
+ * with no operator override recorded for it. Merging would smuggle
3758
+ * unrelated work into an assembling release. Refused before the merge lock
3759
+ * and any tracker call; the guard clears only by completing or cancelling
3760
+ * the release, or recording an explicit per-PR override through
3761
+ * `omp-conductor release-composition`, never through the merge reason.
3762
+ */
3763
+ "outside-active-release",
3071
3764
  /** The label is not in this project's own vocabulary. */
3072
3765
  "label-not-in-vocabulary",
3073
3766
  /** The label is a lifecycle label; those transitions stay the daemon's (#26). */