omp-conductor 0.17.1 → 0.18.1

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 +34 -0
  2. package/REFERENCE.md +71 -17
  3. package/agents/to-spec.md +90 -0
  4. package/package.json +2 -1
  5. package/schema/config.schema.json +53 -1
  6. package/src/admission.ts +308 -76
  7. package/src/ask.ts +307 -10
  8. package/src/backups.ts +2 -2
  9. package/src/board.ts +17 -3
  10. package/src/briefs/orchestrator.md +43 -14
  11. package/src/briefs/to-spec.md +84 -0
  12. package/src/briefs/worker.md +37 -19
  13. package/src/cli.ts +2 -0
  14. package/src/command-help.ts +19 -1
  15. package/src/command-manifest.ts +27 -2
  16. package/src/commands/context.ts +1 -0
  17. package/src/commands/drain.ts +176 -0
  18. package/src/commands/extend.ts +6 -10
  19. package/src/commands/status.ts +5 -1
  20. package/src/commands/watch.ts +110 -3
  21. package/src/commands/worker.ts +9 -10
  22. package/src/config-schema.ts +57 -0
  23. package/src/config.ts +102 -2
  24. package/src/daemon.ts +1220 -1517
  25. package/src/dashboard/app.js +4 -1
  26. package/src/dashboard/server.ts +5 -2
  27. package/src/decisions.ts +279 -16
  28. package/src/depends-on.ts +261 -1
  29. package/src/diff-flags.ts +425 -1
  30. package/src/digest-schedule.ts +37 -0
  31. package/src/doctor.ts +52 -0
  32. package/src/escalate.ts +9 -3
  33. package/src/failure-class.ts +43 -4
  34. package/src/fleet.ts +166 -24
  35. package/src/gitops.ts +188 -81
  36. package/src/graph-health.ts +55 -8
  37. package/src/graph.ts +379 -69
  38. package/src/harness-loader.ts +59 -0
  39. package/src/host.ts +567 -2
  40. package/src/lifecycle.ts +158 -6
  41. package/src/omp.ts +269 -20
  42. package/src/orchestrator-tick.ts +1489 -26
  43. package/src/orchestrator.ts +12 -0
  44. package/src/privileged.ts +1 -4
  45. package/src/release-policy.ts +503 -9
  46. package/src/routing.ts +11 -3
  47. package/src/session-host.ts +115 -5
  48. package/src/settlement.ts +1780 -0
  49. package/src/setup-host.ts +1205 -6
  50. package/src/setup-install.ts +119 -30
  51. package/src/setup-wizard.ts +88 -2
  52. package/src/setup.ts +119 -13
  53. package/src/shell.ts +15 -0
  54. package/src/status-render.ts +100 -11
  55. package/src/store.ts +519 -45
  56. package/src/to-spec.ts +387 -0
  57. package/src/tracker/github.ts +150 -14
  58. package/src/types.ts +470 -16
  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 +770 -40
  63. package/src/verbs/socket.ts +24 -5
  64. package/src/worker.ts +239 -9
  65. package/src/worktree.ts +142 -18
package/src/types.ts CHANGED
@@ -95,7 +95,8 @@ export interface RepoTarget {
95
95
  /**
96
96
  * Absolute path of the **conductor-owned, index-only clone** of this repo
97
97
  * whose code-graph index workers query. Optional: absent means this repo has
98
- * no graph, and the worker brief says nothing about one.
98
+ * no graph, and the worker brief says so explicitly rather than staying
99
+ * silent — a worker that knows there is no graph stops looking for one.
99
100
  *
100
101
  * Two things it deliberately is not, and both were paid for:
101
102
  *
@@ -609,6 +610,54 @@ export const REVIEW_REVISION_OUTCOMES = ["revised", "pending", "failed", "skippe
609
610
 
610
611
  export type ReviewRevisionOutcome = (typeof REVIEW_REVISION_OUTCOMES)[number];
611
612
 
613
+ /**
614
+ * How strictly this project's green PRs are reviewed before they merge
615
+ * (#678). One of three typed levels; each level's bar is documented once in
616
+ * {@link REVIEW_STRICTNESS_CHOICES} (setup.ts), which is the same single text
617
+ * the rendered orchestrator brief carries, so the level a config declares and
618
+ * the threshold a session actually enforces cannot disagree.
619
+ *
620
+ * `low` blocks only correctness, security, data-loss or explicit
621
+ * acceptance-criteria failures. `medium` (the recommended default for a new
622
+ * project) adds material maintainability or reliability defects likely to
623
+ * become incidents within six months. `high` adds concrete quality defects —
624
+ * never subjective style churn or unbounded refactoring.
625
+ */
626
+ export const REVIEW_STRICTNESS = ["low", "medium", "high"] as const;
627
+
628
+ export type ReviewStrictness = (typeof REVIEW_STRICTNESS)[number];
629
+
630
+ /** The recommended strictness for a project that has never answered. */
631
+ export const DEFAULT_REVIEW_STRICTNESS: ReviewStrictness = "medium";
632
+
633
+ /** The small validated range a project's own review-round ceiling may sit in. */
634
+ export const REVIEW_MAX_ROUNDS_MIN = 1;
635
+ export const REVIEW_MAX_ROUNDS_MAX = 6;
636
+
637
+ /** The typed default ceiling: three corrected heads per PR lifecycle — enough
638
+ * for real correction cycles, and a hard stop against endless polishing. */
639
+ export const DEFAULT_REVIEW_MAX_ROUNDS = 3;
640
+
641
+ /**
642
+ * 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.
648
+ */
649
+ export interface ReviewPolicy {
650
+ strictness: ReviewStrictness;
651
+ maxRounds: number;
652
+ }
653
+
654
+ /** The documented migration default: what a config written before the key
655
+ * existed loads as, deterministically, until the operator answers setup. */
656
+ export const DEFAULT_REVIEW_POLICY: ReviewPolicy = {
657
+ strictness: DEFAULT_REVIEW_STRICTNESS,
658
+ maxRounds: DEFAULT_REVIEW_MAX_ROUNDS,
659
+ };
660
+
612
661
  /**
613
662
  * One durable review-revision request (#677): the orchestrator returned a
614
663
  * green, run-owned pull request to its worker with blocking findings, and
@@ -641,6 +690,21 @@ export interface ReviewRevisionRecord {
641
690
  outcome?: ReviewRevisionOutcome;
642
691
  }
643
692
 
693
+ /**
694
+ * The outcome of recording one `conductor_pr_review` finding against a run's
695
+ * review-revision outbox (#786). `created` opened a new revision round for the
696
+ * run; `appended` folded the finding into the revision already pending
697
+ * undelivered on the same reviewed head — one round, one row, one worker
698
+ * attempt either way, and the resumed session reads both findings. `refused`
699
+ * names the one in-flight shape that cannot be amended: a pending revision at
700
+ * a different head, which is a review of a moved PR and must dispatch and
701
+ * settle before another finding for the same run is recorded.
702
+ */
703
+ export type ReviewRevisionEnqueue =
704
+ | { kind: "created"; record: ReviewRevisionRecord }
705
+ | { kind: "appended"; record: ReviewRevisionRecord }
706
+ | { kind: "refused"; block: "pending-different-head" };
707
+
644
708
  /**
645
709
  * A model-supplied justification: one value out of a closed set, plus prose that
646
710
  * is written down and read by nothing that decides.
@@ -716,8 +780,11 @@ export interface ProjectConfig {
716
780
  * orchestrator-tick.ts. */
717
781
  groomBelow?: number;
718
782
  /** Labels the dispatcher writes back so the tracker alone shows live state
719
- * to a human who never opens the daemon's logs. */
720
- stateLabels: { inProgress: string; blocked: string; failed: string };
783
+ * to a human who never opens the daemon's logs. `backlog` is the
784
+ * operator's own park gesture (#507) — not dispatcher-written, but read by
785
+ * eligibility so a parked issue can never be claimed while its queue label
786
+ * is still physically present. */
787
+ stateLabels: { inProgress: string; blocked: string; failed: string; backlog: string };
721
788
  /** Maps a `${labelPrefix}${name}` label on an issue to the checkout it
722
789
  * belongs in, so routing is declared by humans, not guessed. */
723
790
  routing: { labelPrefix: string; repos: Record<string, RepoTarget> };
@@ -811,6 +878,16 @@ export interface ProjectConfig {
811
878
  * reaching for `.merge` on a project some test hand-built.
812
879
  */
813
880
  policy?: ProjectPolicy;
881
+ /**
882
+ * How green PRs are reviewed and returned (#678): the strictness level and
883
+ * the hard ceiling on review rounds per PR lifecycle. Optional only for
884
+ * configs written before the key existed — the loader always materialises a
885
+ * complete value, so read it through `resolveReview` rather than reaching
886
+ * for `.review` on a project some test hand-built. A config that never
887
+ * answered migrates to {@link DEFAULT_REVIEW_POLICY} deterministically, and
888
+ * the schema's bounds agree with the runtime ones.
889
+ */
890
+ review?: ReviewPolicy;
814
891
  /**
815
892
  * Exact operator-authored exceptions for recovery PRs that no run recorded.
816
893
  * Optional and hand-edited: setup preserves entries but never invents them.
@@ -864,6 +941,38 @@ export const CONFIG_VERSION = 2;
864
941
  */
865
942
  export const READABLE_CONFIG_VERSIONS = [1, CONFIG_VERSION] as const;
866
943
 
944
+ /**
945
+ * Operator-authored host facts rendered into every worker brief (#721) — the
946
+ * typed replacement for host prose hand-written into an untracked agent
947
+ * context file. Only what cannot be derived lives here: cores and RAM are
948
+ * read from the host by `host.ts` at render time, and the guarded shell
949
+ * suites stay `SHARED_HOST_SCRIPTS`'s single source — nothing in this object
950
+ * may re-name a refused command.
951
+ *
952
+ * Every key is optional; a fleet that never fills the object loads a config
953
+ * with no `host` field at all and dispatches briefs byte-for-byte as before.
954
+ */
955
+ export interface HostConstraints {
956
+ /**
957
+ * What this host is and what else it runs, in the operator's words ("a
958
+ * shared 4-core VPS that also runs Langfuse and the fleet"). The renderer
959
+ * folds the derived core/RAM count into this line.
960
+ */
961
+ description?: string;
962
+ /**
963
+ * The non-interactive PATH a script or `ssh host '<cmd>'` invocation must
964
+ * export (`/root/.bun/bin:/root/.local/bin:$PATH` on this fleet) — commands
965
+ * run from an ssh session do not see the interactive shell's PATH.
966
+ */
967
+ path?: string;
968
+ /**
969
+ * Per-repo command conventions, keyed by the repo slug the brief renders
970
+ * as its `{{REPO}}` (`owner/repo`). A repo without an entry renders nothing
971
+ * for it.
972
+ */
973
+ conventions?: Record<string, string>;
974
+ }
975
+
867
976
  /**
868
977
  * On-disk root config. `version` is present from day one so a format change
869
978
  * can be migrated instead of silently misread by an older daemon.
@@ -876,6 +985,9 @@ export interface ConductorConfig {
876
985
  * `<stateDir()>/backups/db` when omitted. A snapshot that cannot land there
877
986
  * is `doctor`'s `db-backup` failure. */
878
987
  dbBackupDir?: string;
988
+ /** Host facts every worker brief renders (#721). Absent or empty renders
989
+ * nothing — no section — and the brief stays exactly what it always was. */
990
+ host?: HostConstraints;
879
991
  }
880
992
 
881
993
  /**
@@ -963,6 +1075,17 @@ export const SETTLEMENT_FLAG_KINDS = [
963
1075
  "assertions-removed",
964
1076
  /** A named timeout in a test file went up. */
965
1077
  "test-timeout-raised",
1078
+ /** The PR diff touches files outside the effective declared file lane —
1079
+ * the declaration admission enforced and the brief rendered, so a widened
1080
+ * lane is named on evidence rather than found by reading the file list by
1081
+ * hand, and the queued work it blocks is explainable (#739). */
1082
+ "lane-escape",
1083
+ /** A command the PR body claims as proof, with no matching command in the
1084
+ * run's transcript — the claim has no attempt behind it at all. */
1085
+ "claimed-proof-missing",
1086
+ /** A claimed proof command was attempted, but the shared-host guard refused
1087
+ * it — the honest blocked shape, told apart from a claim with no attempt. */
1088
+ "claimed-proof-blocked",
966
1089
  /** A terminal run recovered an OPEN PR by exact issue, repo and branch identity. */
967
1090
  "pr-adopted",
968
1091
  /** A workflow on the merge commit's base branch failed after settlement. */
@@ -978,8 +1101,8 @@ export type SettlementFlagKind = (typeof SETTLEMENT_FLAG_KINDS)[number];
978
1101
  */
979
1102
  export interface SettlementFlag {
980
1103
  kind: SettlementFlagKind;
981
- /** Repo-relative path, `(report)` for report evidence, or `(base branch)` for
982
- * a post-merge workflow result. */
1104
+ /** Repo-relative path, `(report)` for report evidence, `(lane)` for a
1105
+ * lane-escape finding, or `(base branch)` for a post-merge workflow result. */
983
1106
  file: string;
984
1107
  /** 1-based line in the side of the diff the evidence came from: the
985
1108
  * post-image for an added line, the pre-image for a removed one. */
@@ -1063,6 +1186,14 @@ export interface Tracker {
1063
1186
  * read exists to prevent (#517).
1064
1187
  */
1065
1188
  listComments(issue: number): Promise<IssueComment[]>;
1189
+ /**
1190
+ * One issue's body, or undefined when the tracker could not produce it.
1191
+ * The promotion echo (#724) renders the parsed file lane exactly where the
1192
+ * author asserts the claim; the queue label is being added to an issue the
1193
+ * `listReady` filter would have excluded by definition, so the verb needs
1194
+ * this read rather than a list the issue is not on.
1195
+ */
1196
+ getIssue(issue: number): Promise<ReadyIssue | undefined>;
1066
1197
  addLabel(issue: number, label: string): Promise<void>;
1067
1198
  removeLabel(issue: number, label: string): Promise<void>;
1068
1199
  comment(issue: number, body: string): Promise<void>;
@@ -1109,10 +1240,27 @@ export interface Tracker {
1109
1240
  * when either fact cannot be read safely.
1110
1241
  */
1111
1242
  issueSnapshot(issue: number): Promise<IssueSnapshot | undefined>;
1243
+ /**
1244
+ * An issue's raw body, or undefined when it cannot be read. Used only by the
1245
+ * dependency-graph cycle pass (#421) to follow a reachable prerequisite's
1246
+ * own `Depends-on:` declarations; never a "no body" claim.
1247
+ */
1248
+ issueBody(issue: number): Promise<string | undefined>;
1112
1249
  /**
1113
1250
  * The state of one specific pull request, or undefined when this adapter
1114
- * could not tell — a network failure, a deleted PR, a URL it cannot parse.
1115
- * Undefined never means "no".
1251
+ * could not tell — a network failure, a revoked token, a URL it cannot
1252
+ * parse, or a 404 the adapter could not corroborate. Undefined never means
1253
+ * "no". The one exception is a definitively missing PR: a REST 404 on the
1254
+ * individual read that a same-repository pulls-list read corroborates (a
1255
+ * deleted or never-created number in a repository the credential can still
1256
+ * read) is reported by throwing a classified `GhPrMissingError`. A
1257
+ * repository hidden from the credential answers the same 404 bytes, so a
1258
+ * bare 404 — token scope/SSO loss included — is never "missing": it stays
1259
+ * undefined and the row stays retryable. Every other tracker implementation
1260
+ * keeps the "never throws, undefined is could-not-tell" contract, and every
1261
+ * caller not acting on a missing PR keeps its existing fail-closed handling
1262
+ * by catching the throw exactly as it already catches any other tracker
1263
+ * failure.
1116
1264
  *
1117
1265
  * Deliberately separate from {@link Tracker.openCloserFor}, which asks
1118
1266
  * whether any OPEN closing PR exists and answers undefined for merged,
@@ -1121,6 +1269,11 @@ export interface Tracker {
1121
1269
  * is how a PR a human rejected gets recorded as merged.
1122
1270
  */
1123
1271
  prState(url: string): Promise<PrState | undefined>;
1272
+ /** The live head commit SHA of one pull request, or undefined when the
1273
+ * tracker could not tell. `pr-checks-green` binds its green verdict to the
1274
+ * head it was read at, so a head read that cannot answer is "not yet
1275
+ * satisfied" for that pass, never a green verdict (#808). */
1276
+ prHead(url: string): Promise<string | undefined>;
1124
1277
  /** The merge commit and base ref for a merged pull request. */
1125
1278
  mergedPrInfo(url: string): Promise<MergedPrInfo | undefined>;
1126
1279
  /** Workflow runs GitHub associated with one exact commit SHA. */
@@ -1175,6 +1328,13 @@ export interface Tracker {
1175
1328
  * better answer on a later tick.
1176
1329
  */
1177
1330
  prDiff(url: string): Promise<PrDiff | undefined>;
1331
+ /**
1332
+ * The pull request's body, or undefined when this adapter could not produce
1333
+ * one — the same advisory contract as {@link Tracker.prDiff}. Its only
1334
+ * consumer is the settlement audit's claimed-proof check (#582): a claim
1335
+ * extracted from a body this port could not read is no claim at all.
1336
+ */
1337
+ prBody(url: string): Promise<string | undefined>;
1178
1338
  /**
1179
1339
  * Every check on a PR with the state the tracker reports for it (#132).
1180
1340
  *
@@ -1203,9 +1363,9 @@ export interface Tracker {
1203
1363
  /** Issues carrying `label`, open or closed, bounded. Empty on any failure —
1204
1364
  * a reconcile that cannot list must remove no labels. */
1205
1365
  listLabeled(label: string, limit?: number): Promise<{ number: number; state: IssueState }[]>;
1206
- /** Sub-issues of `issue`, with their states. Empty when there are none *or*
1207
- * when the lookup failed: both mean "no evidence this was decomposed", and
1208
- * the reconcile below only ever acts on positive evidence. */
1366
+ /** Sub-issues of `issue`, with their states. Propagates lookup failures so
1367
+ * callers can choose the safe polarity: settlement catches and treats an
1368
+ * unread probe as no positive evidence, while launch selection fails closed. */
1209
1369
  childrenOf(issue: number): Promise<{ number: number; state: IssueState }[]>;
1210
1370
  }
1211
1371
 
@@ -1343,6 +1503,72 @@ export type RunState =
1343
1503
  /** In flight when its daemon process died; reconciled at the next startup. */
1344
1504
  | "orphaned";
1345
1505
 
1506
+ /** One parsed `File lane:` declaration: the paths and the verbatim source
1507
+ * line, so a renderer can reproduce the declaration itself rather than a
1508
+ * summary of it. Exported for the brief's guarantee that the gate's effective
1509
+ * lane is always visible to the worker (#608). */
1510
+ export interface LaneDeclaration {
1511
+ files: string[];
1512
+ /** The declaration line verbatim, as written on the surface it came from. */
1513
+ source: string;
1514
+ }
1515
+
1516
+ /**
1517
+ * The effective file lane as both admission and the worker brief must read it
1518
+ * (#608): the latest `File lane:` declaration among the issue body and every
1519
+ * comment, in the tracker's oldest-first order. This is the "later correction
1520
+ * visibly supersedes" contract applied across both surfaces at once, and it is
1521
+ * the single source of truth the gate enforces and the brief renders — so a
1522
+ * declaration can never control admission while staying invisible to the
1523
+ * worker. `at` records which surface won (`"body"`, or the 0-based comment
1524
+ * index), letting the brief reproduce the declaration verbatim when the
1525
+ * winning comment sits beyond its rendered discussion budget.
1526
+ */
1527
+ export interface FileLane extends LaneDeclaration {
1528
+ at: "body" | number;
1529
+ }
1530
+
1531
+ /** One parsed `Model:` declaration (#535): the selector and the verbatim
1532
+ * source line, so a renderer can reproduce the declaration itself rather
1533
+ * than a summary of it. */
1534
+ export interface ModelDeclaration {
1535
+ /** The selector in omp's own model syntax — a role alias (`@slow`) or a
1536
+ * concrete `provider/model`, either optionally `:thinking`-suffixed —
1537
+ * passed straight through as the session's `modelPattern`, for omp's
1538
+ * resolver to interpret. */
1539
+ model: string;
1540
+ /** The declaration line verbatim, as written on the surface it came from. */
1541
+ source: string;
1542
+ }
1543
+
1544
+ /**
1545
+ * The effective model as both dispatch and the worker brief must read it
1546
+ * (#535): the latest `Model:` declaration among the issue body and every
1547
+ * comment, in the tracker's oldest-first order — the same "later correction
1548
+ * visibly supersedes" contract applied across both surfaces at once as
1549
+ * {@link FileLane}, so a pre-dispatch comment can change the model a
1550
+ * promotion brief named in the body. `at` records which surface won
1551
+ * (`"body"`, or the 0-based comment index).
1552
+ */
1553
+ export interface EffectiveModel extends ModelDeclaration {
1554
+ at: "body" | number;
1555
+ }
1556
+
1557
+ /**
1558
+ * One dispatched run's observation of the code-graph MCP tools (#726): a
1559
+ * runtime fact read off the session's own registry at session start, never a
1560
+ * config guess. `present: true` and `present: false` are both observations —
1561
+ * an absent field (no `graphTools` on the row at all) means no observation was
1562
+ * recorded, which is deliberately distinct from "tools absent": the whole
1563
+ * point of the boolean is that "the model ignored a tool it had" and "the tool
1564
+ * was missing" no longer look identical from outside the session.
1565
+ */
1566
+ export interface GraphToolsObservation {
1567
+ present: boolean;
1568
+ /** Session-start wall clock (ms epoch), the moment the registry was read. */
1569
+ at: number;
1570
+ }
1571
+
1346
1572
  /**
1347
1573
  * One attempt at one issue. Persisted so a daemon restart can reconcile
1348
1574
  * orphaned worktrees and branches instead of leaking them.
@@ -1358,6 +1584,17 @@ export interface RunRecord {
1358
1584
  state: RunState;
1359
1585
  /** 1-based attempt number, checked against `Caps.maxAttemptsPerIssue`. */
1360
1586
  attempt: number;
1587
+ /**
1588
+ * The file-lane declaration admission resolved for this run, persisted at
1589
+ * dispatch from the carried `Admission.lane` (#744). It is the exact
1590
+ * snapshot the overlap gate enforced — the same value the brief renders —
1591
+ * and it is durable on purpose: the next dispatch pass builds lane occupancy
1592
+ * from it, so a run admitted with a declared lane it has not yet written
1593
+ * still holds those files across passes, until the row reaches a terminal
1594
+ * state and leaves the active set. Absent means the run predates the column
1595
+ * or was admitted with no declaration (fail open) — never "empty lane".
1596
+ */
1597
+ lane?: FileLane;
1361
1598
  turns: number;
1362
1599
  /** Effective turn ceiling for this run; operators may only raise it. */
1363
1600
  maxTurns: number;
@@ -1397,6 +1634,12 @@ export interface RunRecord {
1397
1634
  autoCompactionCount?: number;
1398
1635
  /** omp session transcript, so a human can read what the worker actually did. */
1399
1636
  sessionFile?: string;
1637
+ /** The run id of the orphan-clean attempt whose session this row resumes
1638
+ * (#536, #567). Set only when the dispatch-time resume verdict fired;
1639
+ * absent means a fresh dispatch, never "resumed from unknown". Readable
1640
+ * without opening the transcript, so "how often did resume fire and did it
1641
+ * save turns" is a query, not a file walk. */
1642
+ resumedFromRunId?: string;
1400
1643
  prUrl?: string;
1401
1644
  /** Pull request head the worker observed after its deterministic CI watcher exited. */
1402
1645
  headSha?: string;
@@ -1424,9 +1667,41 @@ export interface RunRecord {
1424
1667
  * (#286).
1425
1668
  */
1426
1669
  model?: string;
1670
+ /**
1671
+ * The code-graph session observation (#726): whether the graph MCP tools were
1672
+ * in this run's own session registry when the session started. Read off the
1673
+ * session itself, once, at dispatch — never derived from `mcp.json`, which
1674
+ * only says what a session *should* mount. It is the difference between "the
1675
+ * model ignored a tool it had" and "the tool was missing", which look
1676
+ * identical from outside a session. Absent means the run predates the column
1677
+ * or no worker session recorded an observation — never "graph tools were
1678
+ * absent": `present: false` is the only absence claim, and it is a runtime
1679
+ * fact, not an inference.
1680
+ */
1681
+ graphTools?: GraphToolsObservation;
1427
1682
  /** When an operator accepted the loss or recovered the tree by hand
1428
1683
  * (`unblock --force`). Clears the hold without erasing what happened. */
1429
1684
  salvageAckAt?: number;
1685
+ /** Why the retained tree is under quarantine (#737): the cleanup pass could
1686
+ * not make its object store sound (`repairAlternates`), so fetching into it
1687
+ * was refused and the tree's commits cannot be verified against any remote.
1688
+ * Present means the tree is quarantined right now; the pass clears it the
1689
+ * moment it reaps the tree or re-verifies the store on a later pass.
1690
+ * Absent means never quarantined, or no longer — never "quarantine not
1691
+ * checked", because every retained-tree pass checks. */
1692
+ quarantineDetail?: string;
1693
+ /** Failed-attempt budget charges this row has accumulated through review
1694
+ * claims (#795 review round 2). Every chargeable terminal event that was
1695
+ * claimed into a review round adds one here, so `failuresFor` keeps
1696
+ * counting each event even after the round leaves the row `pushed-green`;
1697
+ * repeated failed rounds therefore keep consuming the failed-attempt
1698
+ * budget. Absent means zero charges were ever preserved on this row. */
1699
+ failureCharges?: number;
1700
+ /** Continuation budget charges this row has accumulated through review
1701
+ * claims, same semantics as {@link RunRecord.failureCharges}: each
1702
+ * chargeable cap or returned-for-revision event claimed into a round adds
1703
+ * one, and `continuationsFor` sums the column. */
1704
+ continuationCharges?: number;
1430
1705
  startedAt: number;
1431
1706
  endedAt?: number;
1432
1707
  /** Last failure text, surfaced verbatim in escalations. */
@@ -1479,6 +1754,10 @@ export type AdmissionHoldReason =
1479
1754
  | "issue-closed"
1480
1755
  | "issue-state-lookup-error"
1481
1756
  | "issue-dequeued"
1757
+ /** An operator-parked issue: the park label beat the queue label at claim
1758
+ * time (#734). Unlike `issue-dequeued` it is an operator gesture, not a
1759
+ * queue edit — the queue label may still be physically present. */
1760
+ | "issue-parked"
1482
1761
  | "parent-lookup-error"
1483
1762
  | "sibling-active"
1484
1763
  | "repo-active"
@@ -1489,9 +1768,14 @@ export type AdmissionHoldReason =
1489
1768
  | "plan-usage-cap"
1490
1769
  | "shutting-down"
1491
1770
  | "stale-base"
1771
+ /** A ready issue whose lifecycle state label is residual: its newest run is
1772
+ * terminal (or absent), so nobody owns it — Duty 1 reconciliation work, not
1773
+ * occupied capacity (#611). */
1774
+ | "stale-lifecycle"
1492
1775
  | "critical-base-verify-error"
1493
1776
  | "file-lane"
1494
1777
  | "depends-on"
1778
+ | "dependency-cycle"
1495
1779
  | "unroutable:no-repo-label"
1496
1780
  | "unroutable:multiple-repo-labels"
1497
1781
  | "unroutable:unknown-repo";
@@ -1528,6 +1812,14 @@ export interface DispatchSummary {
1528
1812
  * sweep and by classification recovery. Optional: persisted old rows lack
1529
1813
  * it, so readers use `?? 0`. (#497) */
1530
1814
  settled?: number;
1815
+ /**
1816
+ * Queue-labelled candidates the operator's park label disqualified — the
1817
+ * same eligibility read the claim gate uses, so the status number cannot
1818
+ * disagree with what admission would hold (#507). Distinguishes "0 claimable,
1819
+ * 12 parked" from "0 claimable, nothing to do". Optional: persisted old rows
1820
+ * lack it, and a pass with nothing parked omits it, so readers use `?? 0`.
1821
+ */
1822
+ parked?: number;
1531
1823
  /** True for a held pass's own record (#497): the queue was never routed, so
1532
1824
  * ready/routed/claimed are absent queue facts, not an empty queue. */
1533
1825
  paused?: boolean;
@@ -1767,6 +2059,49 @@ export interface IntakeDraft {
1767
2059
  at: number;
1768
2060
  }
1769
2061
 
2062
+ /**
2063
+ * The verdict surface of per-issue grooming state (#735). Admission writes
2064
+ * `blocked` when it holds a candidate for a file-lane or dependency reason —
2065
+ * the durable half of the misleading "groom the backlog" fix, because the tick
2066
+ * reads these instead of re-deriving why the runway cannot move. `promotable`
2067
+ * and `considered` are the outcomes #679's scout loop writes into the same
2068
+ * table, so grooming knowledge lives in one store rather than a second memory.
2069
+ */
2070
+ export const GROOMING_VERDICTS = ["promotable", "blocked", "considered"] as const;
2071
+
2072
+ export type GroomingVerdict = (typeof GROOMING_VERDICTS)[number];
2073
+
2074
+ /**
2075
+ * One issue's current grooming verdict, durable across restarts and keyed by
2076
+ * project + issue — one row per issue, replaced in place by upsert, never a
2077
+ * history. Deliberately NOT a decision row: a verdict is neither owed to the
2078
+ * operator nor TTL-exempt, so it sits outside `decisions` (7-day expiry,
2079
+ * operator-facing) and `material_events` (append-only digest outbox) alike.
2080
+ */
2081
+ export interface GroomingRecord {
2082
+ project: string;
2083
+ issue: number;
2084
+ verdict: GroomingVerdict;
2085
+ /** Why this verdict — admission's hold reason for `blocked` (`file-lane`,
2086
+ * `depends-on`), or the groomer's own label for #679's verdicts. */
2087
+ reason: string;
2088
+ /** What proved it: the hold detail naming the overlapping file/holder run,
2089
+ * or a scout summary. Free text, bounded at the write site. */
2090
+ evidence: string;
2091
+ recordedAt: number;
2092
+ }
2093
+
2094
+ /** What a caller hands over. The store owns the timestamp and the replace-in-
2095
+ * place semantics, nothing else. */
2096
+ export interface GroomingDraft {
2097
+ project: string;
2098
+ issue: number;
2099
+ verdict: GroomingVerdict;
2100
+ reason: string;
2101
+ evidence: string;
2102
+ at: number;
2103
+ }
2104
+
1770
2105
  /**
1771
2106
  * Where one operator decision stands (#136).
1772
2107
  *
@@ -1818,6 +2153,12 @@ export interface DecisionRecord {
1818
2153
  /** When that precondition was first observed true. Set once; the digest
1819
2154
  * promotes the row from "parked" to "act on this now". */
1820
2155
  conditionMetAt?: number;
2156
+ /** For `pr-checks-green` / `pr-review-ready`: the exact PR head whose checks
2157
+ * satisfied the condition, read in the same pass as the green verdict. A
2158
+ * head change invalidates the verdict, so a met row whose PR head has moved
2159
+ * returns to pending instead of rendering `[CONDITION MET]` for a stale
2160
+ * head (#808). */
2161
+ conditionHead?: string;
1821
2162
  state: DecisionState;
1822
2163
  resolvedAt?: number;
1823
2164
  /** The answer, the withdrawal reason, or the expiry note. */
@@ -1875,6 +2216,17 @@ export interface Store {
1875
2216
  /** Partial patch; an explicit `null` clears a column, `undefined`/absence leaves it alone (#468). */
1876
2217
  updateRun(id: string, patch: RunPatch): void;
1877
2218
  getRun(id: string): RunRecord | undefined;
2219
+ /**
2220
+ * Counts of this project's runs that recorded a graph-tools session
2221
+ * observation (#726), split by the observed truth value. The doctor reads
2222
+ * these to report what dispatched sessions actually saw, and reports nothing
2223
+ * as "observed" when no run has recorded one.
2224
+ */
2225
+ graphToolsObservationCounts(project: string): {
2226
+ recorded: number;
2227
+ present: number;
2228
+ absent: number;
2229
+ };
1878
2230
  /** Runs whose issue is occupied: a live worker, or a green PR awaiting merge. */
1879
2231
  activeRuns(project: string): RunRecord[];
1880
2232
  /** Runs backed by a worker process — what capacity counts. Subset of {@link Store.activeRuns}. */
@@ -1890,10 +2242,20 @@ export interface Store {
1890
2242
  * predecessor opened. Merged rows keep the same recent-history bound. */
1891
2243
  runsForProjectPr(project: string, prUrl: string, mergedSinceEpochMs: number): RunRecord[];
1892
2244
  /** Persist one review-revision request (#677), durably, before anything is
1893
- * woken. Returns `undefined` when a revision for the same run is already
1894
- * pending undelivered — the atomic half of the duplicate in-flight guard
1895
- * (the other half is the run row's state once the revision is dispatched). */
1896
- createReviewRevision(draft: Omit<ReviewRevisionRecord, "id">): ReviewRevisionRecord | undefined;
2245
+ * woken. A same-run, same-head finding folds into the revision already
2246
+ * pending undelivered (`appended` — one round, one row, one worker
2247
+ * attempt, and the resumed session gets both findings); otherwise it opens
2248
+ * a new round (`created`). A revision already pending at a different head
2249
+ * cannot be amended (`refused`) — that round must dispatch and settle
2250
+ * first. The duplicate in-flight guard is the same single transaction, so
2251
+ * two concurrent same-head findings cannot both create a row or overwrite
2252
+ * each other (the run row's state is the other half once the revision is
2253
+ * dispatched). */
2254
+ enqueueReviewRevision(draft: Omit<ReviewRevisionRecord, "id">): ReviewRevisionEnqueue;
2255
+ /** The pending revision for one run, if any — the header the verb reads to
2256
+ * tell "this call folds into the in-flight round" from "this call opens a
2257
+ * new round" before the round-ceiling gate applies. */
2258
+ pendingReviewForRun(project: string, runId: string): ReviewRevisionRecord | undefined;
1897
2259
  /** Review revisions not yet handed to a worker, oldest first — what the
1898
2260
  * daemon's dispatch pass wakes on its next tick. */
1899
2261
  pendingReviewRevisions(project: string): ReviewRevisionRecord[];
@@ -1962,6 +2324,11 @@ export interface Store {
1962
2324
  * can name every WIP tip a re-claim would build on and every tree that is
1963
2325
  * still the only copy. */
1964
2326
  salvagedRuns(project: string): RunRecord[];
2327
+ /** Newest attempt per issue whose retained tree is under quarantine: the
2328
+ * object store could not be made sound, so the tree's commits cannot be
2329
+ * verified against any remote and the daemon refused to fetch into it
2330
+ * (#737). Newest-attempt-only for the same reason the board is. */
2331
+ quarantinedRuns(project: string): RunRecord[];
1965
2332
  /** Total run segments, used only for the monotonically increasing run number. */
1966
2333
  attemptsFor(project: string, issue: number): number;
1967
2334
  /** Terminal implementation failures that consume `maxAttemptsPerIssue`. */
@@ -2038,6 +2405,28 @@ export interface Store {
2038
2405
  /** Resolve one idea to `groomed` (recording the issue URL #300 chose) or
2039
2406
  * `dismissed`. `false` when the id is unknown. */
2040
2407
  resolveIntake(id: string, state: "groomed" | "dismissed", issueUrl?: string): boolean;
2408
+ /** Record the current grooming verdict for one issue, replacing any prior
2409
+ * row — one row per project + issue, never a history (#735). */
2410
+ upsertGrooming(draft: GroomingDraft): void;
2411
+ /** The current grooming verdict for one issue, or undefined when none. */
2412
+ grooming(project: string, issue: number): GroomingRecord | undefined;
2413
+ /** Current grooming verdicts for a project, issue-ascending; `verdict`
2414
+ * narrows to one verdict — the tick's known-blocked read is
2415
+ * `groomingVerdicts(project, "blocked")`. */
2416
+ groomingVerdicts(project: string, verdict?: GroomingVerdict): GroomingRecord[];
2417
+ /**
2418
+ * Reconcile admission's lane/dependency holds against the persisted blocked
2419
+ * verdicts: each currently held issue is recorded as `blocked`, and in the
2420
+ * same transaction every `blocked` row whose lane/dependency hold this pass
2421
+ * does not name is deleted — the hold no longer applies, so the state
2422
+ * self-heals instead of lingering as a cache someone must invalidate. Runs
2423
+ * at the end of every admission pass. Never touches `promotable` or
2424
+ * `considered` rows, which belong to #679's scout loop.
2425
+ */
2426
+ reconcileGrooming(
2427
+ project: string,
2428
+ holds: readonly { issue: number; reason: AdmissionHoldReason; detail?: string }[],
2429
+ ): void;
2041
2430
  /** Count and age source for status and digest prompt bounds. */
2042
2431
  digestBacklog(project: string): DigestBacklog;
2043
2432
  /** Add one bounded observation to the per-day friction rollup. */
@@ -2223,8 +2612,21 @@ export interface Store {
2223
2612
  /** Answer or withdraw one. `false` when the id is unknown or already closed,
2224
2613
  * so a double-resolve cannot overwrite the first answer. */
2225
2614
  resolveDecision(id: string, state: "answered" | "withdrawn", resolution: string, at: number): boolean;
2226
- /** First observation that a row's condition came true. Idempotent. */
2227
- markDecisionConditionMet(id: string, at: number): boolean;
2615
+ /**
2616
+ * First observation that a row's condition came true. Idempotent per head:
2617
+ * a second call changes nothing, but a `pr-checks-green` / `pr-review-ready`
2618
+ * row whose PR head moved is cleared first and then re-marks against the new
2619
+ * head.
2620
+ * `head` binds the verdict to the PR head it was observed at (#808).
2621
+ */
2622
+ markDecisionConditionMet(id: string, at: number, head?: string): boolean;
2623
+ /**
2624
+ * Drop a condition's met state — timestamp and any head binding — when the
2625
+ * observation it was set from stopped holding. The head-bound conditions
2626
+ * call this when the PR head changed, so the row returns to pending instead
2627
+ * of rendering `[CONDITION MET]` for checks that are still running (#808).
2628
+ */
2629
+ clearDecisionConditionMet(id: string): boolean;
2228
2630
  /** Close every open row past its deadline and return them, so the caller can
2229
2631
  * say what it just closed rather than reporting a count. */
2230
2632
  expireDueDecisions(project: string, now: number): DecisionRecord[];
@@ -2471,6 +2873,16 @@ export const VERB_NAMES = [
2471
2873
  * OMP session on the existing branch and PR. No close, no reopen, no
2472
2874
  * redispatch. */
2473
2875
  "conductor_pr_review",
2876
+ /** The orchestrator-only, settled-run recovery operation (#806): open (or
2877
+ * adopt) the missing pull request for a terminal run whose stored branch
2878
+ * exists at its exact recorded head. The recovery proves everything a
2879
+ * worker's own `conductor_pr_create` took for granted from a live channel —
2880
+ * no worker is live, the routed repo matches, the remote branch is at the
2881
+ * exact recorded head, the issue is still open, and no PR already
2882
+ * represents that head — so a stranded `pushed-pending` / lost-PR run can
2883
+ * re-enter normal review/merge without a fresh clone. Idempotent: an
2884
+ * already-matching PR is returned, not duplicated. */
2885
+ "conductor_pr_recover",
2474
2886
  /** The one read verb. It answers with the merge gate's own verdict, so
2475
2887
  * "pushed-green" is the dispatcher's reading of the PR rather than a claim the
2476
2888
  * worker makes about itself from whatever it happened to run. */
@@ -2536,6 +2948,32 @@ export const VERB_REFUSALS = [
2536
2948
  "pr-not-this-run",
2537
2949
  /** An unrecorded recovery PR was authorized, but not for these exact inputs. */
2538
2950
  "recovery-authorization-mismatch",
2951
+ /** The recovery target resolved to a run that does not exist in the store. */
2952
+ "recovery-no-run",
2953
+ /** The run the recovery was pointed at still has a live worker on it. */
2954
+ "recovery-run-live",
2955
+ /** The run is terminal but not in a state the settlement sweep owns, so a
2956
+ * recovered PR would never re-enter verification/review/merge. */
2957
+ "recovery-unsettled-state",
2958
+ /** The tracker could not say whether the issue is open. Recovery fails
2959
+ * closed rather than opening a PR beside (or for) a possibly-closed
2960
+ * issue. */
2961
+ "issue-state-unreadable",
2962
+ /** The recovered work has already landed or been declined: the issue is
2963
+ * closed, the run settled merged, or its recorded PR merged. Nothing to
2964
+ * recover, and recovery must not reopen it. */
2965
+ "recovery-issue-resolved",
2966
+ /** The run records no exact 40-hex head to compare its stored branch
2967
+ * against, so there is nothing durable to verify a recovered PR would
2968
+ * publish. */
2969
+ "recovery-unrecorded-head",
2970
+ /** The branch exists but not at the run's recorded head (replaced,
2971
+ * force-pushed or superseded by a live PR), so a created PR would publish
2972
+ * work the daemon never verified. */
2973
+ "recovery-head-mismatch",
2974
+ /** The routed repository a terminal run recorded no longer has an entry in
2975
+ * this project's routing, so the daemon cannot create a PR for it. */
2976
+ "recovery-repo-unrouted",
2539
2977
  /** The run has no pull request to act on. */
2540
2978
  "pr-missing",
2541
2979
  /**
@@ -2559,6 +2997,13 @@ export const VERB_REFUSALS = [
2559
2997
  * running on it, or the run settled otherwise. The detail names the state.
2560
2998
  */
2561
2999
  "review-in-flight",
3000
+ /**
3001
+ * The PR has already been through the configured review-round ceiling
3002
+ * (#678): the policy's hard bound against endless polishing. Refused rather
3003
+ * than recorded — the orchestrator leaves the PR open, records the
3004
+ * unresolved findings and escalates once instead of spending a worker round.
3005
+ */
3006
+ "review-round-ceiling",
2562
3007
  /**
2563
3008
  * The routed repository's base branch is frozen because a watched merge (or
2564
3009
  * live base observation) turned it red — a base-red-freeze. Further merges to
@@ -2571,6 +3016,15 @@ export const VERB_REFUSALS = [
2571
3016
  "label-not-in-vocabulary",
2572
3017
  /** The label is a lifecycle label; those transitions stay the daemon's (#26). */
2573
3018
  "label-is-lifecycle",
3019
+ /** The label is the operator's park gesture; only the operator may set or
3020
+ * clear it, orchestrator and worker alike (#507). */
3021
+ "label-is-operator-owned",
3022
+ /** Adding the queue label was refused because the issue carries a clearly
3023
+ * delimited write-lane section heading that parsed no path-like files
3024
+ * (#825): promoting it would fail open beside overlapping work, so the
3025
+ * verb refuses with an actionable syntax error instead of echoing a
3026
+ * fail-open the heading contradicts. */
3027
+ "file-lane-unparseable",
2574
3028
  /** The release grant does not permit this shape for this caller. */
2575
3029
  "release-not-granted",
2576
3030
  /** The artefact or environment is not one this project declared (#129). */