omp-conductor 0.18.0 → 0.18.2

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 (65) hide show
  1. package/README.md +35 -1
  2. package/REFERENCE.md +61 -11
  3. package/agents/to-spec.md +94 -0
  4. package/package.json +2 -1
  5. package/schema/config.schema.json +35 -1
  6. package/src/admission.ts +204 -75
  7. package/src/arm-challenge.ts +250 -57
  8. package/src/ask.ts +268 -7
  9. package/src/board.ts +17 -3
  10. package/src/briefs/orchestrator.md +62 -21
  11. package/src/briefs/to-spec.md +88 -0
  12. package/src/briefs/worker.md +2 -1
  13. package/src/cli.ts +124 -1
  14. package/src/command-help.ts +11 -0
  15. package/src/command-manifest.ts +38 -5
  16. package/src/commands/arm.ts +1 -1
  17. package/src/commands/context.ts +1 -0
  18. package/src/commands/drain.ts +176 -0
  19. package/src/commands/extend.ts +6 -10
  20. package/src/commands/intake.ts +4 -19
  21. package/src/commands/status.ts +5 -1
  22. package/src/commands/watch.ts +51 -16
  23. package/src/commands/worker.ts +9 -10
  24. package/src/config-schema.ts +43 -6
  25. package/src/config.ts +65 -9
  26. package/src/daemon.ts +879 -41
  27. package/src/dashboard/app.js +4 -1
  28. package/src/dashboard/server.ts +5 -2
  29. package/src/decisions.ts +243 -17
  30. package/src/diff-flags.ts +75 -1
  31. package/src/doctor.ts +60 -82
  32. package/src/escalate.ts +31 -14
  33. package/src/failure-class.ts +28 -2
  34. package/src/fleet.ts +239 -240
  35. package/src/gitops.ts +188 -81
  36. package/src/graph-health.ts +35 -1
  37. package/src/graph.ts +66 -1
  38. package/src/harness-loader.ts +59 -0
  39. package/src/host.ts +242 -2
  40. package/src/lifecycle.ts +122 -1
  41. package/src/omp-settings.ts +19 -0
  42. package/src/omp.ts +183 -21
  43. package/src/orchestrator-tick.ts +1591 -32
  44. package/src/orchestrator.ts +12 -0
  45. package/src/privileged.ts +1 -4
  46. package/src/release-policy.ts +503 -9
  47. package/src/session-host.ts +65 -6
  48. package/src/settlement.ts +69 -17
  49. package/src/setup-host.ts +1225 -9
  50. package/src/setup-install.ts +28 -0
  51. package/src/setup-wizard.ts +154 -3
  52. package/src/setup.ts +83 -17
  53. package/src/shell.ts +15 -0
  54. package/src/status-render.ts +216 -12
  55. package/src/store.ts +443 -42
  56. package/src/to-spec.ts +408 -0
  57. package/src/tracker/github.ts +104 -14
  58. package/src/types.ts +405 -19
  59. package/src/upgrade-verify.ts +209 -2
  60. package/src/upgrade.ts +175 -1
  61. package/src/verbs/protocol.ts +39 -0
  62. package/src/verbs/server.ts +765 -56
  63. package/src/verbs/socket.ts +24 -5
  64. package/src/worker.ts +12 -2
  65. package/src/worktree.ts +29 -12
package/src/types.ts CHANGED
@@ -638,26 +638,76 @@ export const REVIEW_MAX_ROUNDS_MAX = 6;
638
638
  * for real correction cycles, and a hard stop against endless polishing. */
639
639
  export const DEFAULT_REVIEW_MAX_ROUNDS = 3;
640
640
 
641
+ /**
642
+ * The one spelling a review policy may ask the adjudicator for: a token naming
643
+ * an OMP model role (`"task"`, `"worker"`, a project's own `modelRoles` key).
644
+ * A role name is never a provider/model — OMP owns provider selection and
645
+ * named model roles, and conductor stores only the role. Used by the config
646
+ * grammar and by the setup dialog's availability check; they must read the
647
+ * same shape or one of them accepts an answer the other rejects.
648
+ */
649
+ export const REVIEW_ADJUDICATOR_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
650
+
651
+ /**
652
+ * Which OMP model role carries a PR's terminal review-ceiling adjudication
653
+ * (#875) when no project policy names one. The deterministic migration default
654
+ * every existing project loads until its operator answers setup: the general
655
+ * OMP session role each install can launch without a provider or model being
656
+ * pinned here.
657
+ */
658
+ export const DEFAULT_REVIEW_ADJUDICATOR_ROLE = "task";
659
+
641
660
  /**
642
661
  * One project's review policy (#678): the strictness its orchestrator applies
643
- * when deciding whether a green PR is returned to its worker, and the hard
644
- * ceiling on how many such rounds one PR lifecycle may consume. The ceiling is
645
- * enforced by the dispatch side, never left to the session's reading of the
646
- * brief: at the ceiling `conductor_pr_review` refuses, the orchestrator leaves
647
- * the PR open, records the unresolved findings and escalates once.
662
+ * when deciding whether a green PR is returned to its worker, the hard
663
+ * ceiling on how many such rounds one PR lifecycle may consume, and which OMP
664
+ * model role carries the final adjudication once that ceiling is reached
665
+ * (#875). The ceiling is enforced by the dispatch side, never left to the
666
+ * session's reading of the brief: at the ceiling `conductor_pr_review`
667
+ * refuses, the orchestrator leaves the PR open, records the unresolved
668
+ * findings and escalates once.
648
669
  */
649
670
  export interface ReviewPolicy {
650
671
  strictness: ReviewStrictness;
651
672
  maxRounds: number;
673
+ /**
674
+ * The OMP model role that runs a PR's terminal review-ceiling adjudication:
675
+ * a named role from OMP's configured model roles (`"task"`, `"worker"`, or a
676
+ * project's own `modelRoles` key), in omp's model-role syntax — never a
677
+ * provider/model, which OMP owns. Absent in config loads as
678
+ * {@link DEFAULT_REVIEW_ADJUDICATOR_ROLE}, the deterministic default every
679
+ * existing install upgrades to.
680
+ */
681
+ adjudicator: string;
652
682
  }
653
683
 
654
- /** The documented migration default: what a config written before the key
684
+ /** The documented migration default: what a config written before the fields
655
685
  * existed loads as, deterministically, until the operator answers setup. */
656
686
  export const DEFAULT_REVIEW_POLICY: ReviewPolicy = {
657
687
  strictness: DEFAULT_REVIEW_STRICTNESS,
658
688
  maxRounds: DEFAULT_REVIEW_MAX_ROUNDS,
689
+ adjudicator: DEFAULT_REVIEW_ADJUDICATOR_ROLE,
659
690
  };
660
691
 
692
+ /**
693
+ * The resolved launch provenance of one review-ceiling adjudication (#875):
694
+ * what the policy named and what the role actually resolved to when the
695
+ * adjudicator session launched. OMP owns named roles and exact provider
696
+ * selection, so this record is where a launch's real model/provider lands —
697
+ * the typed fields later status/ledger code reads to show what genuinely
698
+ * adjudicated, not what a config said it should.
699
+ */
700
+ export interface ReviewAdjudicationProvenance {
701
+ /** The policy's adjudicator role, verbatim from {@link ReviewPolicy.adjudicator}. */
702
+ readonly role: string;
703
+ /** The model pattern the role resolved to at launch, in omp's model syntax. */
704
+ readonly model: string;
705
+ /** The provider OMP selected, when the harness could name one at launch. */
706
+ readonly provider?: string;
707
+ /** Epoch ms when this launch resolution was recorded. */
708
+ readonly resolvedAt: number;
709
+ }
710
+
661
711
  /**
662
712
  * One durable review-revision request (#677): the orchestrator returned a
663
713
  * green, run-owned pull request to its worker with blocking findings, and
@@ -690,6 +740,21 @@ export interface ReviewRevisionRecord {
690
740
  outcome?: ReviewRevisionOutcome;
691
741
  }
692
742
 
743
+ /**
744
+ * The outcome of recording one `conductor_pr_review` finding against a run's
745
+ * review-revision outbox (#786). `created` opened a new revision round for the
746
+ * run; `appended` folded the finding into the revision already pending
747
+ * undelivered on the same reviewed head — one round, one row, one worker
748
+ * attempt either way, and the resumed session reads both findings. `refused`
749
+ * names the one in-flight shape that cannot be amended: a pending revision at
750
+ * a different head, which is a review of a moved PR and must dispatch and
751
+ * settle before another finding for the same run is recorded.
752
+ */
753
+ export type ReviewRevisionEnqueue =
754
+ | { kind: "created"; record: ReviewRevisionRecord }
755
+ | { kind: "appended"; record: ReviewRevisionRecord }
756
+ | { kind: "refused"; block: "pending-different-head" };
757
+
693
758
  /**
694
759
  * A model-supplied justification: one value out of a closed set, plus prose that
695
760
  * is written down and read by nothing that decides.
@@ -926,6 +991,38 @@ export const CONFIG_VERSION = 2;
926
991
  */
927
992
  export const READABLE_CONFIG_VERSIONS = [1, CONFIG_VERSION] as const;
928
993
 
994
+ /**
995
+ * Operator-authored host facts rendered into every worker brief (#721) — the
996
+ * typed replacement for host prose hand-written into an untracked agent
997
+ * context file. Only what cannot be derived lives here: cores and RAM are
998
+ * read from the host by `host.ts` at render time, and the guarded shell
999
+ * suites stay `SHARED_HOST_SCRIPTS`'s single source — nothing in this object
1000
+ * may re-name a refused command.
1001
+ *
1002
+ * Every key is optional; a fleet that never fills the object loads a config
1003
+ * with no `host` field at all and dispatches briefs byte-for-byte as before.
1004
+ */
1005
+ export interface HostConstraints {
1006
+ /**
1007
+ * What this host is and what else it runs, in the operator's words ("a
1008
+ * shared 4-core VPS that also runs Langfuse and the fleet"). The renderer
1009
+ * folds the derived core/RAM count into this line.
1010
+ */
1011
+ description?: string;
1012
+ /**
1013
+ * The non-interactive PATH a script or `ssh host '<cmd>'` invocation must
1014
+ * export (`/root/.bun/bin:/root/.local/bin:$PATH` on this fleet) — commands
1015
+ * run from an ssh session do not see the interactive shell's PATH.
1016
+ */
1017
+ path?: string;
1018
+ /**
1019
+ * Per-repo command conventions, keyed by the repo slug the brief renders
1020
+ * as its `{{REPO}}` (`owner/repo`). A repo without an entry renders nothing
1021
+ * for it.
1022
+ */
1023
+ conventions?: Record<string, string>;
1024
+ }
1025
+
929
1026
  /**
930
1027
  * On-disk root config. `version` is present from day one so a format change
931
1028
  * can be migrated instead of silently misread by an older daemon.
@@ -938,6 +1035,9 @@ export interface ConductorConfig {
938
1035
  * `<stateDir()>/backups/db` when omitted. A snapshot that cannot land there
939
1036
  * is `doctor`'s `db-backup` failure. */
940
1037
  dbBackupDir?: string;
1038
+ /** Host facts every worker brief renders (#721). Absent or empty renders
1039
+ * nothing — no section — and the brief stays exactly what it always was. */
1040
+ host?: HostConstraints;
941
1041
  }
942
1042
 
943
1043
  /**
@@ -1025,6 +1125,11 @@ export const SETTLEMENT_FLAG_KINDS = [
1025
1125
  "assertions-removed",
1026
1126
  /** A named timeout in a test file went up. */
1027
1127
  "test-timeout-raised",
1128
+ /** The PR diff touches files outside the effective declared file lane —
1129
+ * the declaration admission enforced and the brief rendered, so a widened
1130
+ * lane is named on evidence rather than found by reading the file list by
1131
+ * hand, and the queued work it blocks is explainable (#739). */
1132
+ "lane-escape",
1028
1133
  /** A command the PR body claims as proof, with no matching command in the
1029
1134
  * run's transcript — the claim has no attempt behind it at all. */
1030
1135
  "claimed-proof-missing",
@@ -1046,8 +1151,8 @@ export type SettlementFlagKind = (typeof SETTLEMENT_FLAG_KINDS)[number];
1046
1151
  */
1047
1152
  export interface SettlementFlag {
1048
1153
  kind: SettlementFlagKind;
1049
- /** Repo-relative path, `(report)` for report evidence, or `(base branch)` for
1050
- * a post-merge workflow result. */
1154
+ /** Repo-relative path, `(report)` for report evidence, `(lane)` for a
1155
+ * lane-escape finding, or `(base branch)` for a post-merge workflow result. */
1051
1156
  file: string;
1052
1157
  /** 1-based line in the side of the diff the evidence came from: the
1053
1158
  * post-image for an added line, the pre-image for a removed one. */
@@ -1193,8 +1298,19 @@ export interface Tracker {
1193
1298
  issueBody(issue: number): Promise<string | undefined>;
1194
1299
  /**
1195
1300
  * The state of one specific pull request, or undefined when this adapter
1196
- * could not tell — a network failure, a deleted PR, a URL it cannot parse.
1197
- * Undefined never means "no".
1301
+ * could not tell — a network failure, a revoked token, a URL it cannot
1302
+ * parse, or a 404 the adapter could not corroborate. Undefined never means
1303
+ * "no". The one exception is a definitively missing PR: a REST 404 on the
1304
+ * individual read that a same-repository pulls-list read corroborates (a
1305
+ * deleted or never-created number in a repository the credential can still
1306
+ * read) is reported by throwing a classified `GhPrMissingError`. A
1307
+ * repository hidden from the credential answers the same 404 bytes, so a
1308
+ * bare 404 — token scope/SSO loss included — is never "missing": it stays
1309
+ * undefined and the row stays retryable. Every other tracker implementation
1310
+ * keeps the "never throws, undefined is could-not-tell" contract, and every
1311
+ * caller not acting on a missing PR keeps its existing fail-closed handling
1312
+ * by catching the throw exactly as it already catches any other tracker
1313
+ * failure.
1198
1314
  *
1199
1315
  * Deliberately separate from {@link Tracker.openCloserFor}, which asks
1200
1316
  * whether any OPEN closing PR exists and answers undefined for merged,
@@ -1203,6 +1319,11 @@ export interface Tracker {
1203
1319
  * is how a PR a human rejected gets recorded as merged.
1204
1320
  */
1205
1321
  prState(url: string): Promise<PrState | undefined>;
1322
+ /** The live head commit SHA of one pull request, or undefined when the
1323
+ * tracker could not tell. `pr-checks-green` binds its green verdict to the
1324
+ * head it was read at, so a head read that cannot answer is "not yet
1325
+ * satisfied" for that pass, never a green verdict (#808). */
1326
+ prHead(url: string): Promise<string | undefined>;
1206
1327
  /** The merge commit and base ref for a merged pull request. */
1207
1328
  mergedPrInfo(url: string): Promise<MergedPrInfo | undefined>;
1208
1329
  /** Workflow runs GitHub associated with one exact commit SHA. */
@@ -1292,9 +1413,9 @@ export interface Tracker {
1292
1413
  /** Issues carrying `label`, open or closed, bounded. Empty on any failure —
1293
1414
  * a reconcile that cannot list must remove no labels. */
1294
1415
  listLabeled(label: string, limit?: number): Promise<{ number: number; state: IssueState }[]>;
1295
- /** Sub-issues of `issue`, with their states. Empty when there are none *or*
1296
- * when the lookup failed: both mean "no evidence this was decomposed", and
1297
- * the reconcile below only ever acts on positive evidence. */
1416
+ /** Sub-issues of `issue`, with their states. Propagates lookup failures so
1417
+ * callers can choose the safe polarity: settlement catches and treats an
1418
+ * unread probe as no positive evidence, while launch selection fails closed. */
1298
1419
  childrenOf(issue: number): Promise<{ number: number; state: IssueState }[]>;
1299
1420
  }
1300
1421
 
@@ -1432,6 +1553,72 @@ export type RunState =
1432
1553
  /** In flight when its daemon process died; reconciled at the next startup. */
1433
1554
  | "orphaned";
1434
1555
 
1556
+ /** One parsed `File lane:` declaration: the paths and the verbatim source
1557
+ * line, so a renderer can reproduce the declaration itself rather than a
1558
+ * summary of it. Exported for the brief's guarantee that the gate's effective
1559
+ * lane is always visible to the worker (#608). */
1560
+ export interface LaneDeclaration {
1561
+ files: string[];
1562
+ /** The declaration line verbatim, as written on the surface it came from. */
1563
+ source: string;
1564
+ }
1565
+
1566
+ /**
1567
+ * The effective file lane as both admission and the worker brief must read it
1568
+ * (#608): the latest `File lane:` declaration among the issue body and every
1569
+ * comment, in the tracker's oldest-first order. This is the "later correction
1570
+ * visibly supersedes" contract applied across both surfaces at once, and it is
1571
+ * the single source of truth the gate enforces and the brief renders — so a
1572
+ * declaration can never control admission while staying invisible to the
1573
+ * worker. `at` records which surface won (`"body"`, or the 0-based comment
1574
+ * index), letting the brief reproduce the declaration verbatim when the
1575
+ * winning comment sits beyond its rendered discussion budget.
1576
+ */
1577
+ export interface FileLane extends LaneDeclaration {
1578
+ at: "body" | number;
1579
+ }
1580
+
1581
+ /** One parsed `Model:` declaration (#535): the selector and the verbatim
1582
+ * source line, so a renderer can reproduce the declaration itself rather
1583
+ * than a summary of it. */
1584
+ export interface ModelDeclaration {
1585
+ /** The selector in omp's own model syntax — a role alias (`@slow`) or a
1586
+ * concrete `provider/model`, either optionally `:thinking`-suffixed —
1587
+ * passed straight through as the session's `modelPattern`, for omp's
1588
+ * resolver to interpret. */
1589
+ model: string;
1590
+ /** The declaration line verbatim, as written on the surface it came from. */
1591
+ source: string;
1592
+ }
1593
+
1594
+ /**
1595
+ * The effective model as both dispatch and the worker brief must read it
1596
+ * (#535): the latest `Model:` declaration among the issue body and every
1597
+ * comment, in the tracker's oldest-first order — the same "later correction
1598
+ * visibly supersedes" contract applied across both surfaces at once as
1599
+ * {@link FileLane}, so a pre-dispatch comment can change the model a
1600
+ * promotion brief named in the body. `at` records which surface won
1601
+ * (`"body"`, or the 0-based comment index).
1602
+ */
1603
+ export interface EffectiveModel extends ModelDeclaration {
1604
+ at: "body" | number;
1605
+ }
1606
+
1607
+ /**
1608
+ * One dispatched run's observation of the code-graph MCP tools (#726): a
1609
+ * runtime fact read off the session's own registry at session start, never a
1610
+ * config guess. `present: true` and `present: false` are both observations —
1611
+ * an absent field (no `graphTools` on the row at all) means no observation was
1612
+ * recorded, which is deliberately distinct from "tools absent": the whole
1613
+ * point of the boolean is that "the model ignored a tool it had" and "the tool
1614
+ * was missing" no longer look identical from outside the session.
1615
+ */
1616
+ export interface GraphToolsObservation {
1617
+ present: boolean;
1618
+ /** Session-start wall clock (ms epoch), the moment the registry was read. */
1619
+ at: number;
1620
+ }
1621
+
1435
1622
  /**
1436
1623
  * One attempt at one issue. Persisted so a daemon restart can reconcile
1437
1624
  * orphaned worktrees and branches instead of leaking them.
@@ -1447,6 +1634,17 @@ export interface RunRecord {
1447
1634
  state: RunState;
1448
1635
  /** 1-based attempt number, checked against `Caps.maxAttemptsPerIssue`. */
1449
1636
  attempt: number;
1637
+ /**
1638
+ * The file-lane declaration admission resolved for this run, persisted at
1639
+ * dispatch from the carried `Admission.lane` (#744). It is the exact
1640
+ * snapshot the overlap gate enforced — the same value the brief renders —
1641
+ * and it is durable on purpose: the next dispatch pass builds lane occupancy
1642
+ * from it, so a run admitted with a declared lane it has not yet written
1643
+ * still holds those files across passes, until the row reaches a terminal
1644
+ * state and leaves the active set. Absent means the run predates the column
1645
+ * or was admitted with no declaration (fail open) — never "empty lane".
1646
+ */
1647
+ lane?: FileLane;
1450
1648
  turns: number;
1451
1649
  /** Effective turn ceiling for this run; operators may only raise it. */
1452
1650
  maxTurns: number;
@@ -1519,6 +1717,18 @@ export interface RunRecord {
1519
1717
  * (#286).
1520
1718
  */
1521
1719
  model?: string;
1720
+ /**
1721
+ * The code-graph session observation (#726): whether the graph MCP tools were
1722
+ * in this run's own session registry when the session started. Read off the
1723
+ * session itself, once, at dispatch — never derived from `mcp.json`, which
1724
+ * only says what a session *should* mount. It is the difference between "the
1725
+ * model ignored a tool it had" and "the tool was missing", which look
1726
+ * identical from outside a session. Absent means the run predates the column
1727
+ * or no worker session recorded an observation — never "graph tools were
1728
+ * absent": `present: false` is the only absence claim, and it is a runtime
1729
+ * fact, not an inference.
1730
+ */
1731
+ graphTools?: GraphToolsObservation;
1522
1732
  /** When an operator accepted the loss or recovered the tree by hand
1523
1733
  * (`unblock --force`). Clears the hold without erasing what happened. */
1524
1734
  salvageAckAt?: number;
@@ -1530,6 +1740,18 @@ export interface RunRecord {
1530
1740
  * Absent means never quarantined, or no longer — never "quarantine not
1531
1741
  * checked", because every retained-tree pass checks. */
1532
1742
  quarantineDetail?: string;
1743
+ /** Failed-attempt budget charges this row has accumulated through review
1744
+ * claims (#795 review round 2). Every chargeable terminal event that was
1745
+ * claimed into a review round adds one here, so `failuresFor` keeps
1746
+ * counting each event even after the round leaves the row `pushed-green`;
1747
+ * repeated failed rounds therefore keep consuming the failed-attempt
1748
+ * budget. Absent means zero charges were ever preserved on this row. */
1749
+ failureCharges?: number;
1750
+ /** Continuation budget charges this row has accumulated through review
1751
+ * claims, same semantics as {@link RunRecord.failureCharges}: each
1752
+ * chargeable cap or returned-for-revision event claimed into a round adds
1753
+ * one, and `continuationsFor` sums the column. */
1754
+ continuationCharges?: number;
1533
1755
  startedAt: number;
1534
1756
  endedAt?: number;
1535
1757
  /** Last failure text, surfaced verbatim in escalations. */
@@ -1640,6 +1862,14 @@ export interface DispatchSummary {
1640
1862
  * sweep and by classification recovery. Optional: persisted old rows lack
1641
1863
  * it, so readers use `?? 0`. (#497) */
1642
1864
  settled?: number;
1865
+ /**
1866
+ * Queue-labelled candidates the operator's park label disqualified — the
1867
+ * same eligibility read the claim gate uses, so the status number cannot
1868
+ * disagree with what admission would hold (#507). Distinguishes "0 claimable,
1869
+ * 12 parked" from "0 claimable, nothing to do". Optional: persisted old rows
1870
+ * lack it, and a pass with nothing parked omits it, so readers use `?? 0`.
1871
+ */
1872
+ parked?: number;
1643
1873
  /** True for a held pass's own record (#497): the queue was never routed, so
1644
1874
  * ready/routed/claimed are absent queue facts, not an empty queue. */
1645
1875
  paused?: boolean;
@@ -1879,6 +2109,49 @@ export interface IntakeDraft {
1879
2109
  at: number;
1880
2110
  }
1881
2111
 
2112
+ /**
2113
+ * The verdict surface of per-issue grooming state (#735). Admission writes
2114
+ * `blocked` when it holds a candidate for a file-lane or dependency reason —
2115
+ * the durable half of the misleading "groom the backlog" fix, because the tick
2116
+ * reads these instead of re-deriving why the runway cannot move. `promotable`
2117
+ * and `considered` are the outcomes #679's scout loop writes into the same
2118
+ * table, so grooming knowledge lives in one store rather than a second memory.
2119
+ */
2120
+ export const GROOMING_VERDICTS = ["promotable", "blocked", "considered"] as const;
2121
+
2122
+ export type GroomingVerdict = (typeof GROOMING_VERDICTS)[number];
2123
+
2124
+ /**
2125
+ * One issue's current grooming verdict, durable across restarts and keyed by
2126
+ * project + issue — one row per issue, replaced in place by upsert, never a
2127
+ * history. Deliberately NOT a decision row: a verdict is neither owed to the
2128
+ * operator nor TTL-exempt, so it sits outside `decisions` (7-day expiry,
2129
+ * operator-facing) and `material_events` (append-only digest outbox) alike.
2130
+ */
2131
+ export interface GroomingRecord {
2132
+ project: string;
2133
+ issue: number;
2134
+ verdict: GroomingVerdict;
2135
+ /** Why this verdict — admission's hold reason for `blocked` (`file-lane`,
2136
+ * `depends-on`), or the groomer's own label for #679's verdicts. */
2137
+ reason: string;
2138
+ /** What proved it: the hold detail naming the overlapping file/holder run,
2139
+ * or a scout summary. Free text, bounded at the write site. */
2140
+ evidence: string;
2141
+ recordedAt: number;
2142
+ }
2143
+
2144
+ /** What a caller hands over. The store owns the timestamp and the replace-in-
2145
+ * place semantics, nothing else. */
2146
+ export interface GroomingDraft {
2147
+ project: string;
2148
+ issue: number;
2149
+ verdict: GroomingVerdict;
2150
+ reason: string;
2151
+ evidence: string;
2152
+ at: number;
2153
+ }
2154
+
1882
2155
  /**
1883
2156
  * Where one operator decision stands (#136).
1884
2157
  *
@@ -1930,6 +2203,12 @@ export interface DecisionRecord {
1930
2203
  /** When that precondition was first observed true. Set once; the digest
1931
2204
  * promotes the row from "parked" to "act on this now". */
1932
2205
  conditionMetAt?: number;
2206
+ /** For `pr-checks-green` / `pr-review-ready`: the exact PR head whose checks
2207
+ * satisfied the condition, read in the same pass as the green verdict. A
2208
+ * head change invalidates the verdict, so a met row whose PR head has moved
2209
+ * returns to pending instead of rendering `[CONDITION MET]` for a stale
2210
+ * head (#808). */
2211
+ conditionHead?: string;
1933
2212
  state: DecisionState;
1934
2213
  resolvedAt?: number;
1935
2214
  /** The answer, the withdrawal reason, or the expiry note. */
@@ -1987,6 +2266,17 @@ export interface Store {
1987
2266
  /** Partial patch; an explicit `null` clears a column, `undefined`/absence leaves it alone (#468). */
1988
2267
  updateRun(id: string, patch: RunPatch): void;
1989
2268
  getRun(id: string): RunRecord | undefined;
2269
+ /**
2270
+ * Counts of this project's runs that recorded a graph-tools session
2271
+ * observation (#726), split by the observed truth value. The doctor reads
2272
+ * these to report what dispatched sessions actually saw, and reports nothing
2273
+ * as "observed" when no run has recorded one.
2274
+ */
2275
+ graphToolsObservationCounts(project: string): {
2276
+ recorded: number;
2277
+ present: number;
2278
+ absent: number;
2279
+ };
1990
2280
  /** Runs whose issue is occupied: a live worker, or a green PR awaiting merge. */
1991
2281
  activeRuns(project: string): RunRecord[];
1992
2282
  /** Runs backed by a worker process — what capacity counts. Subset of {@link Store.activeRuns}. */
@@ -2002,10 +2292,20 @@ export interface Store {
2002
2292
  * predecessor opened. Merged rows keep the same recent-history bound. */
2003
2293
  runsForProjectPr(project: string, prUrl: string, mergedSinceEpochMs: number): RunRecord[];
2004
2294
  /** Persist one review-revision request (#677), durably, before anything is
2005
- * woken. Returns `undefined` when a revision for the same run is already
2006
- * pending undelivered — the atomic half of the duplicate in-flight guard
2007
- * (the other half is the run row's state once the revision is dispatched). */
2008
- createReviewRevision(draft: Omit<ReviewRevisionRecord, "id">): ReviewRevisionRecord | undefined;
2295
+ * woken. A same-run, same-head finding folds into the revision already
2296
+ * pending undelivered (`appended` one round, one row, one worker
2297
+ * attempt, and the resumed session gets both findings); otherwise it opens
2298
+ * a new round (`created`). A revision already pending at a different head
2299
+ * cannot be amended (`refused`) — that round must dispatch and settle
2300
+ * first. The duplicate in-flight guard is the same single transaction, so
2301
+ * two concurrent same-head findings cannot both create a row or overwrite
2302
+ * each other (the run row's state is the other half once the revision is
2303
+ * dispatched). */
2304
+ enqueueReviewRevision(draft: Omit<ReviewRevisionRecord, "id">): ReviewRevisionEnqueue;
2305
+ /** The pending revision for one run, if any — the header the verb reads to
2306
+ * tell "this call folds into the in-flight round" from "this call opens a
2307
+ * new round" before the round-ceiling gate applies. */
2308
+ pendingReviewForRun(project: string, runId: string): ReviewRevisionRecord | undefined;
2009
2309
  /** Review revisions not yet handed to a worker, oldest first — what the
2010
2310
  * daemon's dispatch pass wakes on its next tick. */
2011
2311
  pendingReviewRevisions(project: string): ReviewRevisionRecord[];
@@ -2155,6 +2455,28 @@ export interface Store {
2155
2455
  /** Resolve one idea to `groomed` (recording the issue URL #300 chose) or
2156
2456
  * `dismissed`. `false` when the id is unknown. */
2157
2457
  resolveIntake(id: string, state: "groomed" | "dismissed", issueUrl?: string): boolean;
2458
+ /** Record the current grooming verdict for one issue, replacing any prior
2459
+ * row — one row per project + issue, never a history (#735). */
2460
+ upsertGrooming(draft: GroomingDraft): void;
2461
+ /** The current grooming verdict for one issue, or undefined when none. */
2462
+ grooming(project: string, issue: number): GroomingRecord | undefined;
2463
+ /** Current grooming verdicts for a project, issue-ascending; `verdict`
2464
+ * narrows to one verdict — the tick's known-blocked read is
2465
+ * `groomingVerdicts(project, "blocked")`. */
2466
+ groomingVerdicts(project: string, verdict?: GroomingVerdict): GroomingRecord[];
2467
+ /**
2468
+ * Reconcile admission's lane/dependency holds against the persisted blocked
2469
+ * verdicts: each currently held issue is recorded as `blocked`, and in the
2470
+ * same transaction every `blocked` row whose lane/dependency hold this pass
2471
+ * does not name is deleted — the hold no longer applies, so the state
2472
+ * self-heals instead of lingering as a cache someone must invalidate. Runs
2473
+ * at the end of every admission pass. Never touches `promotable` or
2474
+ * `considered` rows, which belong to #679's scout loop.
2475
+ */
2476
+ reconcileGrooming(
2477
+ project: string,
2478
+ holds: readonly { issue: number; reason: AdmissionHoldReason; detail?: string }[],
2479
+ ): void;
2158
2480
  /** Count and age source for status and digest prompt bounds. */
2159
2481
  digestBacklog(project: string): DigestBacklog;
2160
2482
  /** Add one bounded observation to the per-day friction rollup. */
@@ -2340,8 +2662,21 @@ export interface Store {
2340
2662
  /** Answer or withdraw one. `false` when the id is unknown or already closed,
2341
2663
  * so a double-resolve cannot overwrite the first answer. */
2342
2664
  resolveDecision(id: string, state: "answered" | "withdrawn", resolution: string, at: number): boolean;
2343
- /** First observation that a row's condition came true. Idempotent. */
2344
- markDecisionConditionMet(id: string, at: number): boolean;
2665
+ /**
2666
+ * First observation that a row's condition came true. Idempotent per head:
2667
+ * a second call changes nothing, but a `pr-checks-green` / `pr-review-ready`
2668
+ * row whose PR head moved is cleared first and then re-marks against the new
2669
+ * head.
2670
+ * `head` binds the verdict to the PR head it was observed at (#808).
2671
+ */
2672
+ markDecisionConditionMet(id: string, at: number, head?: string): boolean;
2673
+ /**
2674
+ * Drop a condition's met state — timestamp and any head binding — when the
2675
+ * observation it was set from stopped holding. The head-bound conditions
2676
+ * call this when the PR head changed, so the row returns to pending instead
2677
+ * of rendering `[CONDITION MET]` for checks that are still running (#808).
2678
+ */
2679
+ clearDecisionConditionMet(id: string): boolean;
2345
2680
  /** Close every open row past its deadline and return them, so the caller can
2346
2681
  * say what it just closed rather than reporting a count. */
2347
2682
  expireDueDecisions(project: string, now: number): DecisionRecord[];
@@ -2588,6 +2923,16 @@ export const VERB_NAMES = [
2588
2923
  * OMP session on the existing branch and PR. No close, no reopen, no
2589
2924
  * redispatch. */
2590
2925
  "conductor_pr_review",
2926
+ /** The orchestrator-only, settled-run recovery operation (#806): open (or
2927
+ * adopt) the missing pull request for a terminal run whose stored branch
2928
+ * exists at its exact recorded head. The recovery proves everything a
2929
+ * worker's own `conductor_pr_create` took for granted from a live channel —
2930
+ * no worker is live, the routed repo matches, the remote branch is at the
2931
+ * exact recorded head, the issue is still open, and no PR already
2932
+ * represents that head — so a stranded `pushed-pending` / lost-PR run can
2933
+ * re-enter normal review/merge without a fresh clone. Idempotent: an
2934
+ * already-matching PR is returned, not duplicated. */
2935
+ "conductor_pr_recover",
2591
2936
  /** The one read verb. It answers with the merge gate's own verdict, so
2592
2937
  * "pushed-green" is the dispatcher's reading of the PR rather than a claim the
2593
2938
  * worker makes about itself from whatever it happened to run. */
@@ -2653,6 +2998,38 @@ export const VERB_REFUSALS = [
2653
2998
  "pr-not-this-run",
2654
2999
  /** An unrecorded recovery PR was authorized, but not for these exact inputs. */
2655
3000
  "recovery-authorization-mismatch",
3001
+ /** The recovery target resolved to a run that does not exist in the store. */
3002
+ "recovery-no-run",
3003
+ /** The run the recovery was pointed at still has a live worker on it. */
3004
+ "recovery-run-live",
3005
+ /** The run is terminal but not in a state the settlement sweep owns, so a
3006
+ * recovered PR would never re-enter verification/review/merge. */
3007
+ "recovery-unsettled-state",
3008
+ /** The tracker could not say whether the issue is open. Recovery fails
3009
+ * closed rather than opening a PR beside (or for) a possibly-closed
3010
+ * issue. */
3011
+ "issue-state-unreadable",
3012
+ /** The recovered work has already landed or been declined: the issue is
3013
+ * closed, the run settled merged, or its recorded PR merged. Nothing to
3014
+ * recover, and recovery must not reopen it. */
3015
+ "recovery-issue-resolved",
3016
+ /** The run records no exact 40-hex head to compare its stored branch
3017
+ * against, so there is nothing durable to verify a recovered PR would
3018
+ * publish. */
3019
+ "recovery-unrecorded-head",
3020
+ /** The branch exists but not at the run's recorded head (replaced,
3021
+ * force-pushed or superseded by a live PR), so a created PR would publish
3022
+ * work the daemon never verified. */
3023
+ "recovery-head-mismatch",
3024
+ /** The routed repository a terminal run recorded no longer has an entry in
3025
+ * this project's routing, so the daemon cannot create a PR for it. */
3026
+ "recovery-repo-unrouted",
3027
+ /** The run's durable `prUrl` is a non-URL value the tracker cannot address
3028
+ * (other than the normalized `pending` settlement sentinel, #866). There is
3029
+ * no PR to read and none to guess at — a corrupt record, not an unreadable
3030
+ * one, so it refuses by name instead of reading as a never-clearing
3031
+ * `head-unresolvable`. */
3032
+ "recorded-pr-corrupt",
2656
3033
  /** The run has no pull request to act on. */
2657
3034
  "pr-missing",
2658
3035
  /**
@@ -2695,6 +3072,15 @@ export const VERB_REFUSALS = [
2695
3072
  "label-not-in-vocabulary",
2696
3073
  /** The label is a lifecycle label; those transitions stay the daemon's (#26). */
2697
3074
  "label-is-lifecycle",
3075
+ /** The label is the operator's park gesture; only the operator may set or
3076
+ * clear it, orchestrator and worker alike (#507). */
3077
+ "label-is-operator-owned",
3078
+ /** Adding the queue label was refused because the issue carries a clearly
3079
+ * delimited write-lane section heading that parsed no path-like files
3080
+ * (#825): promoting it would fail open beside overlapping work, so the
3081
+ * verb refuses with an actionable syntax error instead of echoing a
3082
+ * fail-open the heading contradicts. */
3083
+ "file-lane-unparseable",
2698
3084
  /** The release grant does not permit this shape for this caller. */
2699
3085
  "release-not-granted",
2700
3086
  /** The artefact or environment is not one this project declared (#129). */