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/README.md CHANGED
@@ -271,6 +271,40 @@ The Herdr half owns recovery, not dispatch or policy: it restores the exact
271
271
  session identity, requests an immediate heartbeat, or reports through Telegram
272
272
  and a Herdr notification that the fleet is down.
273
273
 
274
+ ### The worker identity and its harness binding
275
+
276
+ Worker sessions do not run as you. `setup host` creates a dedicated
277
+ unprivileged account, `omp-worker` (home `/var/lib/omp-worker`), and the daemon
278
+ launches every worker session under it through `setpriv`. The account is granted
279
+ search access to the paths a session needs and read access to your agent config
280
+ files; its writable world is the worktree and session directory dispatch hands
281
+ it per run, and nothing else.
282
+
283
+ That boundary means the account cannot list your home — which is also where
284
+ `omp-conductor` and its `@oh-my-pi/pi-coding-agent` peer are installed, and
285
+ module resolution needs to list a directory to find the `node_modules` inside
286
+ it. Left there, a worker silently resolved a *different* harness version out of
287
+ its own package cache. So `setup host` also installs one mount unit,
288
+ `var-lib-omp\x2dworker\x2dharness-node_modules.mount`, binding your install
289
+ read-only at `/var/lib/omp-worker-harness/node_modules`, and launches worker
290
+ children from that path. It is a bind, not a copy: upgrade the harness and every
291
+ worker picks it up with nothing to re-materialise. The mount's parent directory
292
+ is `root:omp-worker` `0750`, so the bound tree is reachable by root and the
293
+ worker account and by no other local account.
294
+
295
+ Three consequences worth knowing:
296
+
297
+ - `omp-conductor` must be installed, not run from a source checkout, for worker
298
+ dispatch to work — a checkout has no install root to bind.
299
+ - If the binding is missing or no longer resolves to your install, the daemon
300
+ refuses to launch workers and says so (`setup host` reports the same thing as
301
+ pending work). Re-run `omp-conductor setup host`; no attempt is charged to the
302
+ issue, because no session ever started.
303
+ - The check runs at every launch, not once at daemon startup. The mount unit is
304
+ ordered before the daemon, so a reboot brings them up in the right order — and
305
+ if the binding ever arrives late anyway, the next dispatch picks it up with no
306
+ restart needed.
307
+
274
308
  ### Stop the conductor (hold / stop)
275
309
 
276
310
  Two words, and one of them takes a flag:
package/REFERENCE.md CHANGED
@@ -763,6 +763,40 @@ or `/`) is `bug`, and `feat` otherwise. The slug is the issue title folded to
763
763
  issue alone, so a retried run recomputes the same branch and finds its own work
764
764
  instead of forking a second one.
765
765
 
766
+ ### The file-lane declaration
767
+
768
+ Two workers writing the same file clobber each other, so admission holds a
769
+ candidate whose declared lane overlaps a live run's actual lane (`file-lane`
770
+ hold). The declaration is read from the issue body or a pre-dispatch comment —
771
+ the two surfaces the worker brief renders — through one grammar, so the lane the
772
+ gate enforces and the lane the promotion echo reports are always the same parse:
773
+
774
+ - **Inline line** (the brief form since #555): a line beginning with `File
775
+ lane:` or `File-lane=` (case-insensitive, tolerating heading/list markers)
776
+ whose rest carries the paths, backtick-delimited, with a bare
777
+ comma/space-separated fallback for tokens that look like relative paths:
778
+ `**File lane:** `omp/src/a.ts`, `omp/src/b.ts` are yours — nothing else holds
779
+ them.`
780
+ - **Write-lane section** (the package-floor decomposition form, #825): a
781
+ markdown heading `## Exact write lane` (or `## Write lane` /
782
+ `## write-lane`) whose immediately following bullet items carry one
783
+ backticked path each — `- `omp/src/types.ts` — why it changes`. Only the
784
+ contiguous bullet run under that heading is read: a following paragraph
785
+ (like a `Read only:` caveat) or the next section ends it, so read-only
786
+ entry points, proof commands and acceptance bullets elsewhere in the issue
787
+ are never captured as write files.
788
+
789
+ When both are present, the inline line wins — it has held since #555, and the
790
+ promotion echo shows exactly which declaration admission enforces. Across
791
+ surfaces, the latest declaration among the body and the whole comment thread
792
+ supersedes (a pre-dispatch correction comment replaces the body's lane). A body
793
+ or thread with no declaration admits exactly as before: `fail open`, and the
794
+ label response says `no lane declared (fail open)`. A clearly delimited
795
+ write-lane section that parsed nothing is **not** an absent declaration: adding
796
+ the queue label is refused with `file-lane-unparseable` and an actionable
797
+ message, because promoting beside overlapping work on a section that plainly
798
+ tried to declare is the exact gap the interlock exists to close.
799
+
766
800
  ## Routing
767
801
 
768
802
  An issue must carry **exactly one** `repo:<name>` label naming a repo in
@@ -1078,11 +1112,15 @@ omp-conductor setup graph --print # print the plan: clones, index commands,
1078
1112
  omp-conductor setup graph # run it: clone, install, enable, seed, verify
1079
1113
  ```
1080
1114
 
1081
- `setup graph --print` prints a `git clone` for every clone that does not exist yet, the
1082
- one-shot index command per repo, and a `cbm-reindex.service` + `cbm-reindex.timer`
1083
- pair built from the project's own repos and branches. `--write` stages all three
1084
- in the state directory and prints the two `sudo` lines that install and enable
1085
- them; it never runs `systemctl`.
1115
+ `setup graph --print` prints the plan: the host prerequisites, a `git clone` for
1116
+ every clone that does not exist yet, the one-shot index command per repo, and
1117
+ this project's `cbm-reindex-<project>.sh` script plus `cbm-reindex-<project>.service`
1118
+ + `cbm-reindex-<project>.timer` unit pair, named for the project so a second
1119
+ project on the same host gets its own refresh instead of silently replacing the
1120
+ first project's (#720). A run stages those three files in the state directory
1121
+ **only after its consent prompt** — declining leaves the staged tree
1122
+ byte-identical — then clones, installs and enables the timer as root, seeds one
1123
+ indexing run, and verifies; it never runs `systemctl` on its own authority.
1086
1124
 
1087
1125
  **Run it as the account the fleet runs as, never under `sudo`** — it refuses if
1088
1126
  you try. Everything it derives resolves per-account: the config it loads, the
@@ -1416,7 +1454,7 @@ conditions, each one something this package can check without asking you:
1416
1454
  | Condition | Met when |
1417
1455
  | --- | --- |
1418
1456
  | `pr-merged:<https url>` | `gh` reports that pull request merged. |
1419
- | `pr-checks-green:<https url>` | Every check on that pull request has a green verdict (a non-empty list, all `success`/`neutral`); a failing or still-pending check is not met. |
1457
+ | `pr-checks-green:<https url>` | Every check on that pull request has a green verdict (a non-empty list, all `success`/`neutral`); a failing or still-pending check is not met. The verdict is bound to the exact PR head it was observed at: if the head changes, the row returns to pending until the new head's own checks are green (#808). |
1420
1458
  | `pr-mergeable:<https url>` | The pull request is mergeable (`clean`, not `unknown` or conflicting). |
1421
1459
  | `issue-closed:<number>` | That issue is closed on the tracker. |
1422
1460
  | `npm-version:<pkg>@<version>` | `npm view <pkg>@<version> version` succeeds — the version is published. |
@@ -1430,10 +1468,11 @@ arm/channel/pending single-flight) instead of waiting a full interval. The poke
1430
1468
  reason and the digest flag `[CONDITION MET — act on this now]` both surface the
1431
1469
  wake so the session acts when the answer becomes actionable. Repeated sweeps
1432
1470
  while the condition stays true do nothing further — the store marks the
1433
- transition once. After a green-but-behind PR is updated through
1434
- `conductor_pr_update_branch`, open a fresh `pr-checks-green` watch on the new
1435
- head so the next green transition can wake merge review the same way; nothing
1436
- here merges on its own.
1471
+ transition once. A `pr-checks-green` watch survives a head change on its own:
1472
+ the binding is about the exact commit, so an updated branch drops the row back
1473
+ to pending and the new head's green transition wakes merge review the same way
1474
+ (`conductor_pr_update_branch` needs no fresh watch). Nothing here merges on its
1475
+ own.
1437
1476
 
1438
1477
  Anything else exits `2` and lists the six forms. An unparseable condition on an
1439
1478
  existing row is *listed and never treated as met*: a grammar a future release
@@ -1940,10 +1979,19 @@ injected tick the extension refuses it and mounts its own bounded surface,
1940
1979
 
1941
1980
  - The call carries `question`, `on-timeout` (`auto-proceed` or `park`, required),
1942
1981
  and optionally `timeoutSeconds`, `blocks`, `recommended`, `options` and
1943
- `category`.
1982
+ `category`. `recommended` is required whenever `on-timeout` is `auto-proceed`
1983
+ (the row must record what was auto-applied) and, when `options` are supplied,
1984
+ must be one of their labels — the label as delivered, never an index.
1944
1985
  - The tool records a decision row first (durable, seven-day expiry), then
1945
- delivers the question through the same path `omp-conductor message` uses
1946
- immediately when the reporting policy permits, durably held otherwise.
1986
+ delivers the question. When the Telegram surface can (a bot token, a
1987
+ configured escalation chat, a paired owner, and the reporting policy
1988
+ permitting an interrupt), it posts the options as the same selectable
1989
+ buttons `telegram_ask` posts and a tap resolves the decision row with the
1990
+ chosen option's label. When it cannot, the question goes out as plain text
1991
+ through the same path `omp-conductor message` uses — immediately when the
1992
+ reporting policy permits, durably held otherwise — and the decision row
1993
+ records the degraded delivery, so a prose reply is never treated as a
1994
+ selection (resolve or withdraw the row by hand if the operator answers).
1947
1995
  - It waits at most the ceiling: `timeoutSeconds` if the ask names one, else the
1948
1996
  tick config's `askTimeoutSeconds`, else 300 seconds — always capped at the
1949
1997
  turn budget, so the ask can never outlive the turn it runs in. An ask issued
@@ -2223,6 +2271,9 @@ omp-conductor doctor [--project NAME] [--json] [--probe-telegram]
2223
2271
  omp-conductor ledger [--issue N] [--limit N] [--project NAME] [--json]
2224
2272
  omp-conductor board [--project NAME] [--json]
2225
2273
  omp-conductor hold [--keep-ticks] [--project NAME]
2274
+ omp-conductor drain start --until ISO|DURATION [--reason TEXT] [--project NAME]
2275
+ omp-conductor drain status [--project NAME]
2276
+ omp-conductor drain cancel [--project NAME]
2226
2277
  omp-conductor stop [--pane] [--project NAME]
2227
2278
  omp-conductor arm [--project NAME]
2228
2279
  omp-conductor disarm [--project NAME]
@@ -2255,7 +2306,7 @@ omp-conductor help
2255
2306
  | --- | --- | --- |
2256
2307
  | `setup [area] [--no-ai] [--answers FILE] [--save-answers FILE] [--project NAME]` | project | The deterministic interview, with styled Clack prompts on an interactive TTY and byte-stable plain output for pipes or `OMP_CONDUCTOR_PLAIN_UI=1`. `--answers` validates a JSON object of stable prompt keys before setup and replaces every prompt; a missing required key exits `1` naming the key and file instead of hanging. `--save-answers` records accepted interactive answers as replayable JSON after a successful run. Bare setup is a full first run, or — when the project already exists — a chooser of which area to amend. Naming an area positionally skips that chooser and amends only that area: `tracker`, `gates`, `caps`, `code-graph`, `authority`, `policy`, `escalation`, `reporting`, `brief`. `host` and `graph` are install subcommands rather than areas and are matched first; anything else exits `2` listing both vocabularies. Every prompt shows its current value as the default, and Enter accepts what you see; `Ctrl-C` at any prompt abandons the run and writes nothing. Setup also **reads your repos to propose answers**: the gates prompt is pre-filled from what CI actually runs, and the brief's `## Project context` and release procedure are drafted from every routing repo and shown for confirmation before anything is written. Each probe is a short session with **no shell, no editor and no verbs** in a throwaway shallow clone, and every answer is a proposal you edit or decline — a probe that cannot clone, cannot reach a model, or answers unusably costs you one warning and the shipped stub. `--no-ai` asks every question with the reading half removed. |
2257
2308
  | `setup host [--project NAME]` | host | Re-render and stage the systemd unit, then **run** the install: `install -m 0644` into `/etc/systemd/system`, `daemon-reload`, `enable`, `restart`. Stages the fleet recovery oneshot (`omp-conductor-recover.service`) and its playbook (`/usr/local/sbin/omp-conductor-recover`) alongside, and installs them **before** the fleet units: both fleet units carry `OnFailure=` to the recovery unit, so a crash-looped daemon or herdr session now collects evidence durably, attempts one bounded recovery, and pages tier-2 instead of dying silently (#485). Every command is shown with its exact argv, one confirm covers the batch, and `sudo` asks for your password once before the first step — or is skipped entirely on a fleet that genuinely runs as root. The first failure stops the rest and prints the un-run remainder verbatim so you can finish by hand. Refuses an *escalated* invocation (`sudo`, or `sudo -i`/`su -` detected by the invoking account disagreeing with the fleet's) before writing anything, naming both accounts, because staging derives the unit's `User=`/`HOME=` from whoever ran it. On a non-Linux host the files are still staged and only the `systemctl` steps are refused. |
2258
- | `setup graph [--no-seed] [--print] [--project NAME]` | project | The code-graph install end to end, in one preview and one confirm: check the prerequisites read-only and stop before installing anything when `codebase-memory-mcp` is absent or no MCP entry mounts it (printing the entry to add); `git clone` each missing index-only checkout **as you, never through sudo**; install and enable `cbm-reindex.timer` as root; then seed one indexing run so the first fetch happens while you watch, and verify with the same probe `status` uses. A repo that does not verify is a failure with the remediation, not a success — staged-but-not-trusted is how you discover months later that no worker read an index. `--no-seed` enables the timer without the seeding run and says plainly the graph is unusable until it first fires; it never skips the prerequisite or clone steps. `--print` changes nothing. Exits `1` when no repo has [`graphProject`](#configuration). |
2309
+ | `setup graph [--no-seed] [--print] [--project NAME]` | project | The code-graph install end to end, in one preview and one confirm: check the prerequisites read-only and stop before installing anything when `codebase-memory-mcp` is absent or no MCP entry mounts it (printing the entry to add); stage this project's `cbm-reindex-<project>.{sh,service,timer}` **after** the confirm — never before, so a declined run leaves the staged tree byte-identical — refusing a stem that already belongs to another project or to a file it did not generate (#720); `git clone` each missing index-only checkout **as you, never through sudo**; install and enable `cbm-reindex-<project>.timer` as root; then seed one indexing run so the first fetch happens while you watch, and verify with the same probe `status` uses. A repo that does not verify is a failure with the remediation, not a success — staged-but-not-trusted is how you discover months later that no worker read an index. `--no-seed` enables the timer without the seeding run and says plainly the graph is unusable until it first fires; it never skips the prerequisite or clone steps. `--print` changes nothing. Exits `1` when no repo has [`graphProject`](#configuration). |
2259
2310
  | `start` | host | Start `herdr-fleet.service` when that optional unit is installed, clearing a previous pane-recovery pin, then start the dispatch daemon and wait until it answers `GET /healthz`. When `omp-conductor.service` is installed, systemd is the only start path: even `start --project NAME` restores the shared unit and uses the name only to verify that `/healthz` serves the requested project. A detached daemon is allowed only when the unit is proven absent. It never clears pause or arms ticks. Refuses if a daemon is already live, naming its pid; manager refusal or unprovable ownership is an error rather than a detached fallback. |
2260
2311
  | `stop` | fleet | Prefer `systemctl stop omp-conductor.service` when that unit's MainPID is the live daemon — systemd then owns the stop and will not schedule a restart for the exit it just requested. Otherwise `SIGTERM`, then `SIGKILL` after a 10-second grace period. Prints `not running` when there is nothing to stop, and tags the confirmation with `(via systemctl)` when the unit path was used. |
2261
2312
  | `restart [--now] [--timeout SECONDS] [--port N] [--project NAME]` | host | Drains the fleet by default: pause new claims, wait until live workers reach `0 / N` (bounded by `--timeout SECONDS`, default 1800 = 30 min), restart, then restore the prior dispatch state. A daemon serving multiple configured projects makes restart host-wide: `--project` is rejected because draining one queue and restarting the shared process would kill another project's workers. Prefer `systemctl restart` when the unit owns the live pid so the replacement stays supervised; only a host proven not to have the installed unit may fall back to the standalone stop/start path. `--now` skips the drain and restarts immediately, orphaning any live runs (old behaviour). A drain that hits `--timeout` restarts nothing and leaves dispatch paused — `omp-conductor resume` lifts it, or re-run `restart` to keep waiting. The new process **salvages dirty live worktrees before orphaning** those rows — see [Deploying a new package onto a busy fleet](#deploying-a-new-package-onto-a-busy-fleet). |
@@ -2264,6 +2315,7 @@ omp-conductor help
2264
2315
  | `ledger [--issue N] [--limit N] [--json]` | project | The action audit: every [mediated-verb](#the-mediated-verbs-126) mutation and every next-attempt turn budget. Verb entries include the arguments, decision, named refusal, and resulting SHA. Turn-budget entries remain after an override is replaced or consumed. Reads (`conductor_pr_status`) are absent so polling cannot bury the signal. `--issue` narrows both histories; `--limit` defaults to 50. `--json` emits one stable object with `project`, optional `issue`, `entries`, `refused`, and `turnOverrides`. Recent verb refusals and pending turn overrides also appear in `status`. |
2265
2316
  | `board [--project NAME]` | fleet | Live keyboard-driven kanban over the same SQLite and `/healthz` truth as `status`, plus the tracker's current labels: Queue, Claimed, Running, Green, Blocked, Failed, Orphaned, the last 24 hours of Merged and Settled, and Parked (an issue the tracker has not confirmed closed — still open, or a label read that failed — so nothing dispatches it until a human labels it). Columns are mutually exclusive and describe current state, not the newest run row, so a requeued issue is queued rather than failed and a closed issue is neither. Refreshes run/spend/turn values every second, and health plus the label read every ten seconds. `Enter` follows the selected transcript in place; `u` invokes the existing unblock workflow on a Blocked, Failed, or Orphaned card; `i` / `p` open the issue / PR; `r` refreshes health; `?` shows all keys. Requires an interactive terminal of at least 50×20. |
2266
2317
  | `hold [--keep-ticks] [--project NAME]` | fleet | Soft stop: pause claiming **and** disarm ticks. Daemon and pane stay up. Prefer this when the intent is "stop the conductor" without killing processes. `--keep-ticks` pauses claiming but leaves the arm marker, so the heartbeat keeps reporting and `resume` alone restores the fleet — no fresh arm challenge. See [Stop the conductor](README.md#stop-the-conductor-hold--stop). |
2318
+ | `drain start --until ISO\|DURATION [--reason TEXT]` / `drain status` / `drain cancel` | project | Start, inspect, or cancel the project's self-expiring admission fence (#484): a durable, bounded alternative to queue-label churn before a release. `start` writes the project's drain record through the landed `createDrain` — new claims pause while active runs settle, and admission resumes automatically at the absolute deadline even if the orchestrator dies. A successful mediated `conductor_release` also clears the drain once the terminal release act completes — version-bump preparation stays latched until the tag is actually cut — so the release window ends with the release itself rather than latching until the deadline (#791). `--until` takes an ISO instant or a relative duration (`90s`, `45m`, `2h`, `1d`) that must be bounded and in the future; a missing, unparseable, unbounded, or past expiry exits `2` before any state changes. `--reason` (1–500 characters) is persisted on the record. `status` reports inactive, or the active drain's creation time, absolute expiry, reason, and remaining active runs from the structured status snapshot. `cancel` removes only the named project's drain and is idempotent. The drain never touches the pause sentinel, the arm marker, or any queue label — it is a file record, not a hold. |
2267
2319
  | `stop [--pane] [--project NAME]` | fleet | Stop the conductor: pause claiming, disarm ticks, then stop the dispatch daemon (systemctl-aware). Pane stays up unless `--pane` is passed. `stop --pane` also pins herdr-conductor recovery off for the conductor agent only — it does **not** stop `herdr-fleet.service` or any other herdr session. Fail-closed: exits nonzero unless the agent is proven gone. To bounce the daemon without stopping the fleet, use `restart`. |
2268
2320
  | `arm [--project NAME]` | fleet | Proof-gated: send a Telegram challenge and write this project's arm marker only after your reply appears as a user turn in the orchestrator transcript. The challenge names the project, so a host running two fleets is not ambiguous. Never auto-armed by `resume` / `hold`. |
2269
2321
  | `disarm [--project NAME]` | fleet | Remove this project's arm marker so its ticks skip; another project's ticks keep running. Also clears a pre-per-project shared `armed` marker while that marker is still what holds this fleet's gate open — otherwise the disarm would not disarm. Processes untouched. |
@@ -2272,7 +2324,7 @@ omp-conductor help
2272
2324
  | `worker pause <issue>` / `worker resume <issue>` | project | Cooperatively park one live worker without changing its run state or lane. Pause aborts the active turn to harness idle and freezes the remaining wall-clock budget; resume continues the same session with a prompt to re-check its last action before repeating it. This is separate from fleet-level `hold`, which refuses new claims and work-starting mutations while allowing pre-pause completion work and releases. |
2273
2325
  | `worker stop <issue> --reason TEXT [--project NAME]` | project | Terminally end a running or cooperatively paused worker. The reason is required (1–500 characters) and persisted on the run. The command waits for settlement, records the distinct `stopped` state, salvages and publishes dirty work, removes `agent:in-progress` through the durable label outbox, and consumes neither failed-attempt nor continuation budget. If salvage fails, the tree holding the only copy stays in place and the command names it. Repeating stop is idempotent and reports the run's already-terminal state. |
2274
2326
  | `unblock <issue> [--force] [--no-requeue]` | project | Remove that issue's `blocked` and `failed` labels so an answered escalation can be claimed again, and restore the project queue label by default so the dispatcher actually sees it. `agent:in-progress` comes off too, but only when the newest recorded run is terminal — that row is the proof no worker still owns the issue, so a live run keeps the label (and the queue label stays off until that run settles), and so does an issue with no run row at all. Run history remains intact: blocks consume the independent continuation budget, not failed implementation attempts. The output reports both budgets and warns when either will make the next tick escalate instead of dispatch. The label changes go through the [label projection outbox](#how-one-tick-works): they are applied inline before the command returns, but **a tracker that refuses them (403, rate limit) no longer fails the verb** — it exits `0`, the intended label state is durable and the daemon retries it, and the output says `label sync queued (N pending) — the daemon retries` instead of claiming the labels were restored. Safety is preserved, but the issue is only claimable once the queue label itself lands: the queue read asks GitHub for issues carrying that label, so a refused queue-label add keeps the issue out of dispatch until projection succeeds. `--no-requeue` clears the state labels only, leaving the queue label untouched — the case where you are about to close the issue. **Refuses, clearing nothing and exiting `3`, when the newest attempt's work could not be committed and its worktree is the only copy** — re-claiming removes that tree. `--force` records the operator's acceptance on the run row and then clears; the salvage failure stays in history. Exits `2` when the issue number is missing or malformed. |
2275
- | `verb <conductor_*> [--arg k=v ...]` | project | Run one [mediated verb](#the-mediated-verbs-126) as the orchestrator, from the CLI — the external-orchestrator half of the verb surface. Every argument goes in as a `--arg k=v` string; an orchestrator can merge (`conductor_pr_merge`), label (`conductor_label`), release (`conductor_release`), update a branch (`conductor_pr_update_branch`) or title/body (`conductor_pr_update`), or read PR state (`conductor_pr_status`). The daemon applies the same checks and writes the same ledger rows a session's call would; a missing `--arg` is refused exactly as a missing tool argument is, worker-only verbs (`conductor_push`, `conductor_pr_create`) are refused with `role-not-allowed`, and a refusal exits `3`. An unknown verb exits `2`. |
2327
+ | `verb <conductor_*> [--arg k=v ...]` | project | Run one [mediated verb](#the-mediated-verbs-126) as the orchestrator, from the CLI — the external-orchestrator half of the verb surface. Every argument goes in as a `--arg k=v` string; an orchestrator can merge (`conductor_pr_merge`), label (`conductor_label`), release (`conductor_release`), update a branch (`conductor_pr_update_branch`) or title/body (`conductor_pr_update`), recover a settled run's missing PR (`conductor_pr_recover`), or read PR state (`conductor_pr_status`). The daemon applies the same checks and writes the same ledger rows a session's call would; a missing `--arg` is refused exactly as a missing tool argument is, worker-only verbs (`conductor_push`, `conductor_pr_create`) are refused with `role-not-allowed`, and a refusal exits `3`. An unknown verb
2276
2328
  | `friction <kind> --detail TEXT [--issue N]` | project | Record one bounded judgment the daemon cannot infer: an escalation belonged in a digest, or a tick report was noise/surprising. The detail is limited to 160 characters. One event never changes policy; three observations inside seven days make the aggregate eligible for one Learning-loop prompt, followed by a seven-day cooldown. |
2277
2329
  | `report --text TEXT [--kind material|digest|tier2|decision-needed|fleet-stopped|confirmed-failure]` | project | Hand a rendered report to the daemon's durable outbox. The command persists the text **before** anything can send and prints a durable handoff id. A material report submitted during quiet hours becomes a held notice until the window opens; otherwise it becomes a report whose delivery the daemon owns, retries with bounded backoff, and records. Delivery is [at-least-once](#report-delivery-the-outbox), so a crash mid-send is retried as a possible repeat and `delivered` never proves exactly one message. `--kind digest` is accepted at most once per local day, decided from the ledger; an unknown `--kind` exits `2`. The remaining kinds declare the report's interrupt category — the escalation handoff: the reporting policy decides between immediate delivery and a durable hold exactly as for a daemon escalation of that category, A repeated identical call exits `2` only while the earlier handoff is still queued undelivered; once it lands, the same text is admitted again (the handoff state decides, not a permanent ledger). Anything still owed appears in `status` with its age. |
2278
2330
  | `decision open --question TEXT [--blocks TEXT] [--resolves-when COND]` | project | Record a question the orchestrator has put to you, and print its id. A question that lives only in a session's context is lost at the next compaction — after which it is either asked twice or dropped silently. `--resolves-when` attaches a machine-checkable condition: `pr-merged:<https url>`, `pr-checks-green:<https url>`, `pr-mergeable:<https url>`, `issue-closed:<n>`, `npm-version:<pkg>@<version>`, or `rate-limit-reset:github`; anything else exits `2` listing the six forms. See [The decision ledger](#the-decision-ledger-136). |
@@ -2355,7 +2407,7 @@ curl -s localhost:8787/healthz
2355
2407
  "configured": true,
2356
2408
  "status": "degraded",
2357
2409
  "checkedAt": "2026-08-08T13:00:00.000Z",
2358
- "prerequisites": { "indexer": "present", "mcpMount": "missing" },
2410
+ "prerequisites": { "indexer": "present", "mcpMount": "unconfigured" },
2359
2411
  "repos": [
2360
2412
  {
2361
2413
  "name": "api",
@@ -2507,10 +2559,12 @@ daemon, across a process boundary, not in a prompt the model can rewrite.
2507
2559
  | --- | --- | --- |
2508
2560
  | `conductor_push` | the worker owning the run | The ref is exactly `refs/heads/<that run's branch>`. Fast-forward only; there is no force argument to reject because none is declared. |
2509
2561
  | `conductor_pr_create` | the worker owning the run | The run has no open PR (the same guard admission uses); head is the run branch; base is the repo's configured `defaultBranch`. |
2562
+ | `conductor_pr_review` | **orchestrator only** | The PR is one a run of this project opened; the run is in a revisable settled state (`pushed-green`, or `failed`/`killed` after pushing green); the live head still equals the reviewed head and the checks at it are green; no review revision is already in flight; the review-round ceiling has not been reached. |
2563
+ | `conductor_pr_recover` | **orchestrator only** | The target issue has a recorded terminal run whose branch still exists at its exact recorded 40-hex head; the run is not live and not merged; the issue is open; no open PR already closes the issue at that head, and a recorded PR is open (returned), closed/merged (refused), definitively missing (replaced) or unreadable (retryable). Creates the one missing PR from that branch to the configured `defaultBranch` and records it on the run. |
2510
2564
  | `conductor_pr_status` | worker or orchestrator | Read-only — nothing to gate. A worker reads only its own run's PR; an orchestrator may name any syntactically valid PR URL, open, merged, or closed, and gets its live state and head (checks are reported when available; a merged or closed PR reports its state instead of an `expected OPEN` refusal). |
2511
2565
  | `conductor_pr_update_branch` | orchestrator, or the worker owning the run | The PR belongs to this project and is open. A worker may only name its own run's PR. |
2512
2566
  | `conductor_pr_merge` | **orchestrator only** | Ordinarily, `authority.merge` equals the caller. A hand-edited `recoveryMerges` entry may instead authorize one exact unrecorded PR/head/reason while held. In both paths, `headSha` equals the live head *at execution time*; checks are green at that same SHA; the project route and migration chain are valid; the project's single merge slot is free. |
2513
- | `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. |
2567
+ | `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. Adding the queue label echoes the parsed [file lane](#the-file-lane-declaration), or refuses with `file-lane-unparseable` when a clearly delimited write-lane section parsed nothing — it never claims fail-open beside a declaration that was actually attempted. |
2514
2568
  | `conductor_release` | **orchestrator only** | `authority.release` equals the caller; the per-shape grant permits it; the artefact or environment was declared; the release preconditions hold; the `reason` is in the closed enum. `version-bump-pr` creates or re-validates one deterministic version-only PR and, on a later call, merges only its exact green head through the project's single merge slot. |
2515
2569
  | `conductor_install` | **orchestrator only** | Gated like a release act: the `install` shape defaults to `human` and a grant is what moves it. The daemon refuses a version npm does not expose with a full `gitHead`, refuses while another install is still in flight, and otherwise starts a detached transient unit that pauses, drains, installs the CLI/omp plugin/Herdr plugin and reloads — outside this session and the daemon. The unit never declares its own success; the first tick after the restart verifies and reports through the durable outbox. |
2516
2570
 
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: to-spec
3
+ description: Groom exactly ONE backlog candidate into a strict, source-verified to-spec verdict. Read-only: reads the candidate and the authoritative source, returns one fenced JSON block matching the to-spec schema. NEVER use for implementation or edits.
4
+ tools: read, grep, glob, web_search
5
+ read-summarize: false
6
+ ---
7
+
8
+ # to-spec — groom one backlog candidate into a verified, to-spec result
9
+
10
+ You are grooming exactly ONE backlog candidate for a conductor fleet: you read
11
+ candidate and source, and you return one strict JSON verdict. You never change
12
+ anything. You have reading tools only — no shell, no editor, no GitHub verbs,
13
+ no task spawning, no label changes. The whole of your work is the structured
14
+ result below, and the fleet treats anything else as a failed grooming pass.
15
+
16
+ ## The candidate
17
+
18
+ Your item's task text names the candidate: the tracker issue number and title,
19
+ the issue body, and the authoritative source repository and ref. Read the
20
+ issue's premise against the code *in that source, at that ref* — never from
21
+ memory, never from another checkout, never from the issue alone. If you cannot
22
+ reach a source you trust, say so through the verdict (`BLOCKED` names what
23
+ failed) — never an invented fallback source.
24
+
25
+ ## The verdict
26
+
27
+ Exactly one of these five strings, nothing else:
28
+
29
+ - `ALREADY DONE` — the work already exists in the source (a later epic retired
30
+ the issue's premise counts as done; prove it with the symbol/file, never the
31
+ title).
32
+ - `PROMOTABLE` — well-specified, fits one worker budget, and the acceptance
33
+ criteria are checkable; carries the proposed brief.
34
+ - `NEEDS DECOMPOSITION` — the plan is real but too big for one budget; say what
35
+ slices it splits into and why each is a separate slice.
36
+ - `BLOCKED` — a named open prerequisite, lane, or credential gap stands in the
37
+ way.
38
+ - `NEEDS PRODUCT DECISION` — the issue cannot proceed until a human decides
39
+ product shape, slice order, or scope; state the one question that unblocks it.
40
+
41
+ ## The return contract
42
+
43
+ Answer in **one fenced JSON block, nothing else after it**. Every field is
44
+ required and no extra keys are accepted:
45
+
46
+ - `verdict` — one of the five strings above.
47
+ - `routing` — exactly one `owner/repo`, or `"MULTI"`.
48
+ - `routingSplit` — required iff `routing` is `"MULTI"`: what each slice goes to.
49
+ - `source` — `{ name, ref, freshAt }`: the authoritative source you read, the
50
+ exact ref, and `freshAt` = epoch milliseconds when you actually observed it.
51
+ Conductor refuses results whose source is older than 24 hours or missing
52
+ name/ref/freshAt — an unsourced verdict is not grooming, it is prose.
53
+ - `laterWorkInvalidates` — boolean: did later work (an epic committed after
54
+ this candidate was filed) retire its premise?
55
+ - `laterWorkNote` — what you searched for that check and what you found. Even
56
+ when false this must name the search, so "false" cannot be written without
57
+ looking.
58
+ - `entryPoints` — 3–6 files to change or read first, the discovery a worker's
59
+ budget dies on when absent.
60
+ - `existingTests` — tests that already exercise the behaviour, by path; `[]`
61
+ when you found none.
62
+ - `likelySilentFake` — the one thing most likely to be silently faked while
63
+ implementing, and how to prove it is not.
64
+ - `proofCommands` — the focused commands that prove the work, each with its
65
+ `cwd` when it matters.
66
+ - `fileLane` — the files and directories this slice writes; `dependencies` —
67
+ open prerequisites, `[]` when none.
68
+ - `proposedBrief` — required iff `verdict` is `PROMOTABLE`: the brief a worker
69
+ would be dispatched with, including the silent fake and the proof commands.
70
+ - `reasonNotToPromote` — required for every other verdict: why this must not
71
+ be promoted.
72
+
73
+ ## Three traps, each of which produces a confidently wrong verdict
74
+
75
+ - **Prose is not evidence.** A verdict without the source-backed contract is
76
+ refused as malformed: every field above is required, and `source` must name
77
+ the ref you read and when.
78
+ - **Stale source reads like good source.** Judge the candidate against the
79
+ stated ref as it is now; a verdict drawn from memory of a different clone is
80
+ stale and will be refused.
81
+ - **A later epic retires the premise.** On anything old, check whether later
82
+ open work invalidated the candidate before concluding anything else. That
83
+ check is mechanical, read-only, and exactly what you are cheap at —
84
+ `laterWorkNote` must name what you searched.
85
+
86
+ ## Answer
87
+
88
+ One fenced JSON block, nothing else after it. Anything unparseable or
89
+ off-schema is persisted as `blocked(malformed)` and the candidate counts as
90
+ not groomed — a refusal is a failed grooming, not a free pass to skip it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
@@ -21,6 +21,7 @@
21
21
  "files": [
22
22
  "src",
23
23
  "!src/**/*.test.ts",
24
+ "agents",
24
25
  "systemd",
25
26
  "schema",
26
27
  "README.md",
@@ -99,6 +99,35 @@
99
99
  "type": "string",
100
100
  "minLength": 1
101
101
  },
102
+ "host": {
103
+ "description": "Host facts every worker brief renders; absent renders no section",
104
+ "type": "object",
105
+ "properties": {
106
+ "description": {
107
+ "type": "string",
108
+ "minLength": 1,
109
+ "description": "What this host is and what else it runs"
110
+ },
111
+ "path": {
112
+ "type": "string",
113
+ "minLength": 1,
114
+ "description": "The non-interactive PATH a script or `ssh host \"<cmd>\"` invocation must export"
115
+ },
116
+ "conventions": {
117
+ "type": "object",
118
+ "propertyNames": {
119
+ "type": "string",
120
+ "minLength": 1
121
+ },
122
+ "additionalProperties": {
123
+ "type": "string",
124
+ "minLength": 1
125
+ },
126
+ "description": "Per-repo command conventions, keyed by the brief's `owner/repo` slug"
127
+ }
128
+ },
129
+ "additionalProperties": false
130
+ },
102
131
  "projects": {
103
132
  "minItems": 1,
104
133
  "type": "array",
@@ -136,7 +165,8 @@
136
165
  "properties": {
137
166
  "inProgress": {},
138
167
  "blocked": {},
139
- "failed": {}
168
+ "failed": {},
169
+ "backlog": {}
140
170
  },
141
171
  "additionalProperties": false
142
172
  },
@@ -340,6 +370,28 @@
340
370
  "additionalProperties": false,
341
371
  "description": "How `arm` proves a human just approved arming"
342
372
  },
373
+ "review": {
374
+ "type": "object",
375
+ "properties": {
376
+ "strictness": {
377
+ "default": "medium",
378
+ "type": "string",
379
+ "enum": [
380
+ "low",
381
+ "medium",
382
+ "high"
383
+ ]
384
+ },
385
+ "maxRounds": {
386
+ "default": 3,
387
+ "type": "integer",
388
+ "minimum": 1,
389
+ "maximum": 6
390
+ }
391
+ },
392
+ "additionalProperties": false,
393
+ "description": "Review strictness and round ceiling for green PRs"
394
+ },
343
395
  "authority": {
344
396
  "type": "object",
345
397
  "properties": {