omp-conductor 0.15.11 → 0.15.13

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 (51) hide show
  1. package/REFERENCE.md +107 -60
  2. package/package.json +1 -1
  3. package/schema/config.schema.json +3 -0
  4. package/src/briefs/orchestrator.md +64 -11
  5. package/src/briefs/policy.md +19 -3
  6. package/src/briefs/worker.md +11 -8
  7. package/src/cli.ts +41 -21
  8. package/src/commands/context.ts +102 -1
  9. package/src/commands/doctor.ts +4 -2
  10. package/src/commands/intake.ts +26 -5
  11. package/src/commands/message.ts +80 -32
  12. package/src/commands/report.ts +38 -2
  13. package/src/commands/restart.ts +81 -54
  14. package/src/commands/setup.ts +61 -11
  15. package/src/commands/stop.ts +45 -22
  16. package/src/commands/upgrade-rollback.ts +9 -0
  17. package/src/config-schema.ts +9 -0
  18. package/src/config.ts +35 -1
  19. package/src/daemon.ts +588 -37
  20. package/src/dashboard/app.js +398 -59
  21. package/src/dashboard/index.html +27 -0
  22. package/src/dashboard/server.ts +219 -5
  23. package/src/dashboard/style.css +169 -1
  24. package/src/doctor.ts +419 -45
  25. package/src/escalate.ts +8 -0
  26. package/src/failure-class.ts +37 -0
  27. package/src/fleet.ts +49 -2
  28. package/src/gitops.ts +157 -0
  29. package/src/lifecycle.ts +113 -2
  30. package/src/model-fallback.ts +177 -0
  31. package/src/omp.ts +115 -13
  32. package/src/orchestrator-down.ts +231 -0
  33. package/src/orchestrator-tick.ts +108 -5
  34. package/src/orchestrator.ts +18 -4
  35. package/src/privileged.ts +10 -0
  36. package/src/release-policy.ts +373 -28
  37. package/src/session-host.ts +11 -5
  38. package/src/setup-host.ts +665 -70
  39. package/src/setup-install.ts +275 -28
  40. package/src/setup-wizard.ts +339 -126
  41. package/src/setup.ts +25 -0
  42. package/src/stop-provenance.ts +66 -0
  43. package/src/store.ts +194 -1
  44. package/src/tracker/github.ts +47 -0
  45. package/src/types.ts +182 -0
  46. package/src/upgrade.ts +110 -32
  47. package/src/verbs/protocol.ts +16 -3
  48. package/src/verbs/server.ts +27 -1
  49. package/src/wizard-ui.ts +261 -46
  50. package/src/worker.ts +24 -3
  51. package/systemd/omp-conductor.service.example +7 -3
package/REFERENCE.md CHANGED
@@ -172,6 +172,14 @@ spend telemetry reading $0.00 on every completed run, invalid reporting
172
172
  timezones, and Telegram delivery health. `--probe-telegram` is the one opt-in
173
173
  side effect: one self-identified test message through the report transport.
174
174
 
175
+ The unit-drift check compares the installed units against the staged render,
176
+ excluding the herdr pane-shell `SHELL=` pin: that value is host state resolved
177
+ at staging time, and `doctor`'s own process resolves it differently (or not at
178
+ all) in a non-login/tick context, so it is reported as not-comparable rather
179
+ than as drift (#511). It still flags genuine drift in `User=`, `ExecStart=`,
180
+ other `Environment=` lines, `Restart=` and `SuccessExitStatus=`, and the unit
181
+ itself keeps pinning `SHELL=` so panes do not fall back to dash (#463).
182
+
175
183
  ## Onboarding
176
184
 
177
185
  `omp-conductor setup` is the whole of it. One command, in a plain terminal, doing
@@ -1225,14 +1233,25 @@ bare `telegram_send` there has nothing to preserve. That turn addresses the
1225
1233
  operator from config instead:
1226
1234
 
1227
1235
  ```bash
1228
- omp-conductor message --text "<the message>" # this project's chat and topic
1229
- omp-conductor message --text "QUESTION: cut 0.16.0 tonight?" # carries the decision category
1236
+ omp-conductor message --text "<the message>" # this project's chat and topic
1237
+ omp-conductor message --category tier2 --text "cut 0.16.0 tonight?" # blocking question: declared category + open decision row
1238
+ omp-conductor message --text "QUESTION: do we release?" # marker form still maps to decision-needed
1230
1239
  ```
1231
1240
 
1241
+ A question — a declared `--category` that is not `material`, or text beginning
1242
+ `QUESTION:` — is an ask, not a send: before delivery the command records an
1243
+ open decision row for the question (parked on silence: "nobody answered" keeps
1244
+ the row open and pending, re-surfaced in every tick until answered or the
1245
+ seven-day expiry, never an approval) and prints the id to resolve it later
1246
+ with `omp-conductor decision resolve <id> --answer "…"`, so the orchestrator
1247
+ never has to remember a separate `decision open` (#520).
1248
+
1232
1249
  It is not a bypass of the interrupt policy: the same availability decision an
1233
- autonomous Telegram tool call gets is applied, so a message the policy defers is
1234
- durably held for the digest or the working-hours catch-up and the command prints
1235
- that held-notice id instead of claiming delivery. It is also not a report — it
1250
+ autonomous Telegram tool call gets is applied, so a message whose category the
1251
+ policy defers is durably held for the digest or the working-hours catch-up and
1252
+ the command prints that held-notice id instead of claiming delivery a
1253
+ *blocking* question must therefore declare a category the fleet interrupts on,
1254
+ not default to the marker. It is also not a report — it
1236
1255
  leaves no `reports` row, and nothing retries it.
1237
1256
 
1238
1257
  ### Delivery is at-least-once, and the docs will not pretend otherwise
@@ -2118,7 +2137,7 @@ least of all on a fleet whose session lives somewhere else.
2118
2137
 
2119
2138
  ```bash
2120
2139
  omp-conductor setup [area] [--no-ai] [--project NAME]
2121
- omp-conductor setup host [--project NAME]
2140
+ omp-conductor setup host [NAME] [--project NAME]
2122
2141
  omp-conductor setup graph [--no-seed] [--print] [--project NAME]
2123
2142
  omp-conductor start [--port N] [--project NAME]
2124
2143
  omp-conductor --version
@@ -2126,7 +2145,7 @@ omp-conductor stop
2126
2145
  omp-conductor restart [--now] [--timeout SECONDS] [--port N] [--project NAME]
2127
2146
  omp-conductor upgrade [--to VERSION] [--project NAME]
2128
2147
  omp-conductor upgrade-install --to VERSION [--project NAME]
2129
- omp-conductor upgrade-rollback [--project NAME]
2148
+ omp-conductor upgrade-rollback
2130
2149
  omp-conductor status [--project NAME]
2131
2150
  omp-conductor doctor [--project NAME] [--json] [--probe-telegram]
2132
2151
  omp-conductor ledger [--issue N] [--limit N] [--project NAME]
@@ -2156,47 +2175,47 @@ omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--pr
2156
2175
  omp-conductor help
2157
2176
  ```
2158
2177
 
2159
- | Command | Behaviour |
2160
- | --- | --- |
2161
- | `setup [area] [--no-ai] [--project NAME]` | The deterministic interview, in a plain terminal — the same prompts, the same one-writer apply sequence, and the same single consent gate as `omp-conductor setup`, which is now one dialog implementation of the shared surface rather than the only way in. Bare 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. |
2162
- | `setup host [--project NAME]` | 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. |
2163
- | `setup graph [--no-seed] [--print] [--project NAME]` | 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). |
2164
- | `start` | 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. |
2165
- | `stop` | 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. |
2166
- | `restart [--now] [--timeout SECONDS] [--port N] [--project NAME]` | 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). |
2167
- | `upgrade [--to VERSION] [--project NAME]` | Deterministically update the Bun-global CLI, omp plugin, Herdr recovery plugin, and managed brief as one release. Resolves the npm version and exact `gitHead`, pauses only new claims, drains active workers, installs all surfaces, reloads Herdr and the daemon, waits for pane recovery, verifies identities and fleet health twice, then restores the original dispatch state. Host-wide by default: one daemon serves every configured project, so a bare run drains all of them and refreshes every brief. `--project` is rejected when the live daemon serves several projects — draining one queue and restarting the shared daemon would kill another's workers. A no-op when already current. Failure leaves dispatch paused. Must run outside a Herdr-managed session. The same transaction runs detached, without a human, as the fleet-installs-itself path: the orchestrator calls the `conductor_install` verb under the granted `install` shape, the daemon validates the version against npm and starts a transient systemd unit (`upgrade-install`) outside the pane and the daemon, and the first tick after the restart verifies version, `/healthz`, ticks, pane and `doctor` against the durable upgrade journal before restoring dispatch — rolling back and paging tier-2 on any gap. |
2168
- | `status [--project NAME]` | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / `brief` / `decisions` / optional `failure classes` and `code graph` / `daemon`, then the project body. The project body includes the latest completed dispatch timestamp, ready/routed/admitted counts, bounded hold groups, and the GitHub API budget (`graphql` / `core` remaining and reset, in the caps block); API failures are marked `DEGRADED` so queue starvation cannot look idle. Active-run lines overlay cooperative worker `paused`/`pausing` from `/healthz` without changing SQLite `running` state or the live worker count. The next tick comes from the live heartbeat process, not a guess from log timestamps. Telegram health uses `getMe` to prove API authentication without sending a message and reports inbound bridge configuration separately. Configured graphs report prerequisites, indexed repos, timer state, and refresh freshness without blocking dispatch. A `reports` block lists everything the outbox has not delivered, with its age, and prints `pending` (nobody has it) differently from `SENDING` (outcome unknown, it may already have arrived) — see [Report delivery](#report-delivery-the-outbox). The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
2169
- | `ledger [--issue N] [--limit N]` | 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. Recent verb refusals and pending turn overrides also appear in `status`. |
2170
- | `board [--project NAME]` | 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. |
2171
- | `hold [--keep-ticks] [--project NAME]` | 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). |
2172
- | `stop [--pane] [--project NAME]` | 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`. |
2173
- | `arm [--project NAME]` | 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`. |
2174
- | `disarm [--project NAME]` | 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. |
2175
- | `tail <issue>` | Follow the newest run for that issue: the worker's assistant text as `assistant: …` and each tool it calls as `tool: <name>`, printed as they land. Workers are omp sessions inside the daemon rather than terminals, so this is the only way to watch one live — a herdr pane running it becomes an observation window. Starts from the top of the transcript, not the end, so attaching to a run that is already ten turns in shows those ten turns. Exits `1` with `no run recorded for #N` when the issue has never been dispatched, or `no transcript yet (state: …)` when the attempt has not opened one. Otherwise it runs until `Ctrl-C`, or until the run has finished and its transcript has been silent for five seconds, and prints `run ended: <state>`. |
2176
- | `extend <issue> --turns N [--project NAME]` | Raise a live worker's effective turn ceiling through its owning daemon without restarting its session. If the latest run is failed, killed, orphaned, or blocked and has no live controller, store a one-shot ceiling for that issue's next claimed attempt instead. A next-attempt value must exceed the project base, every extension must stay at or below `workerMaxTurnsCeiling`, and live extensions remain monotonic. The pending value appears in `status`, is recorded in `ledger`, and is consumed atomically by one claim. |
2177
- | `worker pause <issue>` / `worker resume <issue>` | 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. |
2178
- | `worker stop <issue> --reason TEXT [--project NAME]` | 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. |
2179
- | `unblock <issue> [--force] [--no-requeue]` | 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. |
2180
- | `verb <conductor_*> [--arg k=v ...]` | 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`. |
2181
- | `friction <kind> --detail TEXT [--issue N]` | 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. |
2182
- | `report --text TEXT [--kind material|digest|tier2|decision-needed|fleet-stopped|confirmed-failure]` | 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. |
2183
- | `decision open --question TEXT [--blocks TEXT] [--resolves-when COND]` | 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). |
2184
- | `decision resolve <id> --answer TEXT` | Record what you decided. Exits `1` naming the id when it is unknown or no longer open, so a second answer cannot overwrite the first. |
2185
- | `decision withdraw <id> [--reason TEXT]` | Close a question the session stopped needing, with why. Same guard as `resolve`. |
2186
- | `decision list` | Open questions, oldest first: id, age, what each blocks, whether its condition is met, and the question. Prints `no open decisions` when there are none. |
2187
- | `daemon` | Run the loop in the **foreground**, ticking every 5 minutes and serving `/healthz`. Admitted workers run in a tracked background pool, so settlement and capacity checks remain periodic while they work; shutdown drains the pool before closing the store. This is what `start` launches and what a systemd unit should call. |
2188
- | `daemon --once` | Run a single tick, wait for workers admitted by that tick, and exit. No HTTP server or pidfile — a drill must not register itself as the daemon, or the next reader believes it and the real daemon's in-flight runs get reconciled as orphans. |
2189
- | `--port N` | Accepted by `start`, `restart` and `daemon`. Both `--port 9000` and `--port=9000` work; missing or out of range exits `2` rather than falling back to the default, because probing the wrong endpoint is worse than a hard failure. |
2190
- | `--project NAME` | Selects a project for project-scoped commands and foreground `daemon`. On an installed shared service, `start --project NAME` still starts the host-wide unit and uses the name only to verify `/healthz`; a draining `restart --project NAME` is rejected when that daemon serves multiple projects. A project-only daemon is available only through an explicit foreground `daemon --project NAME` or standalone start on a host proven not to have the unit. |
2191
- | `pause [--reason TEXT]` | Stop new claims and work-starting mutations only. The running daemon notices on its next tick; runs already in flight finish. The orchestrator may still merge, update, or label runs admitted before the pause, and may release when the release policy's own preconditions hold. The orchestrator heartbeat keeps ticking if armed — its gate is the arm marker, not this flag. Per-worker pause is separate. Prefer `hold` to silence both. `--reason TEXT` is recorded in the pause sentinel, which `status` shows as the pause provenance. |
2192
- | `resume [--project NAME]` | Clear pause and any `stop --pane` recovery pin — does **not** re-arm. Run `arm` after an inbound Telegram proof to resume ticks. |
2193
- | `--version`, `-V`, `version` | Print the installed `omp-conductor` package version and exit `0`. Works from the global binary and npm/plugin install because it reads the package metadata beside the shipped CLI. |
2194
- | `brief-upgrade` | Inspect the package-floor + shared policy (when present) + `POLICY.md` overlay. Reports by default, naming which layers compose; see [Keeping a brief current](#keeping-a-brief-current). |
2195
- | `--migrate` | Only for `brief-upgrade`. Lift a bannered `ORCHESTRATOR.md` owned half into `POLICY.md` and recompose. Dry-run unless `--apply`. |
2196
- | `--retrofit` | Only for `brief-upgrade`. Propose (or with `--apply`, write) a `YOURS TO EDIT` banner before the first owned-topic heading on a hand-written brief. |
2197
- | `--apply` | Only for `brief-upgrade`. Confirms `--migrate` / `--retrofit`. On its own it exits `2`: the legacy single-file merge was removed in 0.4.3. |
2198
- | `--file PATH` | Only for `brief-upgrade`. Check a brief that is not where the wizard would have put it, on a host that may have no config at all. |
2199
- | `help`, `--help`, `-h` | Print usage. An unknown or missing verb prints it too, and exits `2`. |
2178
+ | Command | Scope | Behaviour |
2179
+ | --- | --- | --- |
2180
+ | `setup [area] [--no-ai] [--project NAME]` | project | The deterministic interview, in a plain terminal — the same prompts, the same one-writer apply sequence, and the same single consent gate as `omp-conductor setup`, which is now one dialog implementation of the shared surface rather than the only way in. Bare 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. |
2181
+ | `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. |
2182
+ | `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). |
2183
+ | `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. |
2184
+ | `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. |
2185
+ | `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). |
2186
+ | `upgrade [--to VERSION] [--project NAME]` | host | Deterministically update the Bun-global CLI, omp plugin, Herdr recovery plugin, and managed brief as one release. Resolves the npm version and exact `gitHead`, pauses only new claims, drains active workers, installs all surfaces, reloads Herdr and the daemon, waits for pane recovery, verifies identities and fleet health twice, then restores the original dispatch state. Host-wide by default: one daemon serves every configured project, so a bare run drains all of them and refreshes every brief. `--project` is rejected when the live daemon serves several projects — draining one queue and restarting the shared daemon would kill another's workers. A no-op when already current. Failure leaves dispatch paused. Must run outside a Herdr-managed session. The same transaction runs detached, without a human, as the fleet-installs-itself path: the orchestrator calls the `conductor_install` verb under the granted `install` shape, the daemon validates the version against npm and starts a transient systemd unit (`upgrade-install`) outside the pane and the daemon, and the first tick after the restart verifies version, `/healthz`, ticks, pane and `doctor` against the durable upgrade journal before restoring dispatch — rolling back and paging tier-2 on any gap. |
2187
+ | `status [--project NAME]` | fleet | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / `brief` / `decisions` / optional `failure classes` and `code graph` / `daemon`, then the project body. The project body includes the latest completed dispatch timestamp, ready/routed/admitted counts, bounded hold groups, and the GitHub API budget (`graphql` / `core` remaining and reset, in the caps block); API failures are marked `DEGRADED` so queue starvation cannot look idle. Active-run lines overlay cooperative worker `paused`/`pausing` from `/healthz` without changing SQLite `running` state or the live worker count. The next tick comes from the live heartbeat process, not a guess from log timestamps. Telegram health uses `getMe` to prove API authentication without sending a message and reports inbound bridge configuration separately. Configured graphs report prerequisites, indexed repos, timer state, and refresh freshness without blocking dispatch. A `reports` block lists everything the outbox has not delivered, with its age, and prints `pending` (nobody has it) differently from `SENDING` (outcome unknown, it may already have arrived) — see [Report delivery](#report-delivery-the-outbox). The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
2188
+ | `ledger [--issue N] [--limit N]` | 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. Recent verb refusals and pending turn overrides also appear in `status`. |
2189
+ | `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. |
2190
+ | `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). |
2191
+ | `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`. |
2192
+ | `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`. |
2193
+ | `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. |
2194
+ | `tail <issue>` | project | Follow the newest run for that issue: the worker's assistant text as `assistant: …` and each tool it calls as `tool: <name>`, printed as they land. Workers are omp sessions inside the daemon rather than terminals, so this is the only way to watch one live — a herdr pane running it becomes an observation window. Starts from the top of the transcript, not the end, so attaching to a run that is already ten turns in shows those ten turns. Exits `1` with `no run recorded for #N` when the issue has never been dispatched, or `no transcript yet (state: …)` when the attempt has not opened one. Otherwise it runs until `Ctrl-C`, or until the run has finished and its transcript has been silent for five seconds, and prints `run ended: <state>`. |
2195
+ | `extend <issue> --turns N [--project NAME]` | project | Raise a live worker's effective turn ceiling through its owning daemon without restarting its session. If the latest run is failed, killed, orphaned, or blocked and has no live controller, store a one-shot ceiling for that issue's next claimed attempt instead. A next-attempt value must exceed the project base, every extension must stay at or below `workerMaxTurnsCeiling`, and live extensions remain monotonic. The pending value appears in `status`, is recorded in `ledger`, and is consumed atomically by one claim. |
2196
+ | `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. |
2197
+ | `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. |
2198
+ | `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. |
2199
+ | `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`. |
2200
+ | `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. |
2201
+ | `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. |
2202
+ | `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). |
2203
+ | `decision resolve <id> --answer TEXT` | project | Record what you decided. Exits `1` naming the id when it is unknown or no longer open, so a second answer cannot overwrite the first. |
2204
+ | `decision withdraw <id> [--reason TEXT]` | project | Close a question the session stopped needing, with why. Same guard as `resolve`. |
2205
+ | `decision list` | project | Open questions, oldest first: id, age, what each blocks, whether its condition is met, and the question. Prints `no open decisions` when there are none. |
2206
+ | `daemon` | host | Run the loop in the **foreground**, ticking every 5 minutes and serving `/healthz`. Admitted workers run in a tracked background pool, so settlement and capacity checks remain periodic while they work; shutdown drains the pool before closing the store. This is what `start` launches and what a systemd unit should call. |
2207
+ | `daemon --once` | host | Run a single tick, wait for workers admitted by that tick, and exit. No HTTP server or pidfile — a drill must not register itself as the daemon, or the next reader believes it and the real daemon's in-flight runs get reconciled as orphans. |
2208
+ | `--port N` | — | Accepted by `start`, `restart` and `daemon`. Both `--port 9000` and `--port=9000` work; missing or out of range exits `2` rather than falling back to the default, because probing the wrong endpoint is worse than a hard failure. |
2209
+ | `--project NAME` | — | Selects a project for project-scoped commands and foreground `daemon`. On an installed shared service, `start --project NAME` still starts the host-wide unit and uses the name only to verify `/healthz`; a draining `restart --project NAME` is rejected when that daemon serves multiple projects. A project-only daemon is available only through an explicit foreground `daemon --project NAME` or standalone start on a host proven not to have the unit. On a multi-project host, `setup host` refuses without a name and says which part needs it: the units it installs are host-global, and only the per-project tail (tick config, brief link) is per-project — pass `--project NAME` or the positional `setup host NAME`. |
2210
+ | `pause [--reason TEXT]` | fleet | Stop new claims and work-starting mutations only. The running daemon notices on its next tick; runs already in flight finish. The orchestrator may still merge, update, or label runs admitted before the pause, and may release when the release policy's own preconditions hold. The orchestrator heartbeat keeps ticking if armed — its gate is the arm marker, not this flag. Per-worker pause is separate. Prefer `hold` to silence both. `--reason TEXT` is recorded in the pause sentinel, which `status` shows as the pause provenance. |
2211
+ | `resume [--project NAME]` | fleet | Clear pause and any `stop --pane` recovery pin — does **not** re-arm. Run `arm` after an inbound Telegram proof to resume ticks. |
2212
+ | `--version`, `-V`, `version` | none | Print the installed `omp-conductor` package version and exit `0`. Works from the global binary and npm/plugin install because it reads the package metadata beside the shipped CLI. |
2213
+ | `brief-upgrade` | project | Inspect the package-floor + shared policy (when present) + `POLICY.md` overlay. Reports by default, naming which layers compose; see [Keeping a brief current](#keeping-a-brief-current). |
2214
+ | `--migrate` | — | Only for `brief-upgrade`. Lift a bannered `ORCHESTRATOR.md` owned half into `POLICY.md` and recompose. Dry-run unless `--apply`. |
2215
+ | `--retrofit` | — | Only for `brief-upgrade`. Propose (or with `--apply`, write) a `YOURS TO EDIT` banner before the first owned-topic heading on a hand-written brief. |
2216
+ | `--apply` | — | Only for `brief-upgrade`. Confirms `--migrate` / `--retrofit`. On its own it exits `2`: the legacy single-file merge was removed in 0.4.3. |
2217
+ | `--file PATH` | — | Only for `brief-upgrade`. Check a brief that is not where the wizard would have put it, on a host that may have no config at all. |
2218
+ | `help`, `--help`, `-h` | none | Print usage. An unknown or missing verb prints it too, and exits `2`. |
2200
2219
 
2201
2220
  Pause is a sentinel under the state directory and survives a daemon restart.
2202
2221
  `hold --project NAME` writes `paused-<name>` for that project only; a bare
@@ -2206,10 +2225,26 @@ before the pause, and leaves `conductor_release` to its normal authority, grant,
2206
2225
  and precondition checks. Per-worker pause is independent. Hold also removes the
2207
2226
  arm marker the heartbeat reads, so both brains go quiet without killing processes.
2208
2227
 
2209
- Every one of these is a verb on the `omp-conductor` binary, each taking an optional
2210
- `--project NAME`. There is no in-session command: an omp session that wants any of
2211
- them shells out to the binary, which is what keeps one implementation and one ledger
2212
- entry per action.
2228
+ Every one of these is a verb on the `omp-conductor` binary, and the Scope column
2229
+ is its project classification, declared once in `src/commands/context.ts`
2230
+ (`COMMAND_SCOPES`) and enforced by the tests there (#514):
2231
+
2232
+ - **project** — acts on exactly one configured project; a multi-project host
2233
+ demands `--project NAME` (or the documented positional form, e.g.
2234
+ `setup host NAME`).
2235
+ - **host** — acts on the one shared host (units, daemon, package surfaces);
2236
+ never resolves a project, so a multi-project config cannot make it ambiguous.
2237
+ `--project` is accepted only for the documented per-project tail that row
2238
+ names, and refused whenever it cannot be honoured (upgrade/restart reject
2239
+ narrowing the shared daemon; `upgrade-rollback` rejects it outright).
2240
+ - **fleet** — acts on every configured project by default (or `--all`), with
2241
+ `--project NAME` narrowing to one.
2242
+ - **—** (em dash) — the row is a flag rather than a command, or the command
2243
+ takes no project at all.
2244
+
2245
+ There is no in-session command: an omp session that wants any of them shells out
2246
+ to the binary, which is what keeps one implementation and one ledger entry per
2247
+ action.
2213
2248
 
2214
2249
  ### Health endpoint
2215
2250
 
@@ -2587,14 +2622,26 @@ Known and deliberate in this version:
2587
2622
  daemon waits for its active worker pool before closing the store. The CLI
2588
2623
  escalates to `SIGKILL` after 10 seconds, so a worker that needs longer is
2589
2624
  orphaned and salvaged on restart. Use `pause`, wait for `workers 0 / N`, then
2590
- stop when a clean drain matters. A supervising unit should set
2591
- `SuccessExitStatus=0 143`, and operators should prefer `omp-conductor stop` /
2592
- `systemctl stop` over raw `kill`, so `Restart=on-failure` cannot misread a
2593
- deliberate stop as a crash.
2594
- - **A failed orchestrator degrades quietly.** The daemon logs a warning and keeps
2595
- running, but tier-1 escalations then land in issue comments which is exactly the
2596
- "nobody reads it until morning" path the orchestrator exists to avoid. The warning
2597
- is in `daemon.log`; nothing pages you about it.
2625
+ stop when a clean drain matters. The supervising unit keeps
2626
+ `SuccessExitStatus=0 143` and renders `Restart=always` (#546): systemd
2627
+ restarts the daemon on any exit clean or signalled — **except** an explicit
2628
+ `systemctl stop`, which it records as intentional and never undoes. So an
2629
+ unattended SIGTERM (a worker's `bun test`) costs a few seconds of downtime
2630
+ instead of an outage that waits for a human, while the operator's own stop
2631
+ still stops and stays stopped. A genuine crash loop still trips the
2632
+ start-limit burst and reaches the recovery unit through `OnFailure=`, so the
2633
+ restart policy stays a visible detector rather than a silent spin. Prefer
2634
+ `omp-conductor stop` / `systemctl stop` over raw `kill`: a raw kill is not an
2635
+ intentional stop, and the daemon comes straight back.
2636
+ - **A failed orchestrator doesn't stay quiet.** Start failure and an
2637
+ unexpected session death each open one durable orchestrator-down incident
2638
+ that pages tier 2 once ("down since &lt;t&gt;, tier-1 escalations diverting to
2639
+ issue comments") and is shown as a degrade row by `omp-conductor status` —
2640
+ mode, since-when, and how many tier-1 escalations were diverted while it was
2641
+ down. The incident survives a daemon restart while still down (it is
2642
+ re-derived, not forgotten), and one closing notice lands when the orchestrator
2643
+ recovers. What remains a separate watchdog is the `.conductor-stalled` wedge
2644
+ path, for a session that stays alive but stops draining its queue.
2598
2645
  - **Workers are not terminal panes, so you cannot watch them there.** Each
2599
2646
  worker is an omp session the daemon starts as a child process. The resident
2600
2647
  daemon tracks workers in a background pool so the five-minute loop keeps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.15.11",
3
+ "version": "0.15.13",
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.",
@@ -297,6 +297,8 @@
297
297
  "description": "Per-fleet or per-project hard limits"
298
298
  },
299
299
  "workerModel": {},
300
+ "modelFallbacks": {},
301
+ "modelFallbackThreshold": {},
300
302
  "escalation": {
301
303
  "type": "object",
302
304
  "properties": {
@@ -611,6 +613,7 @@
611
613
  },
612
614
  "additionalProperties": false
613
615
  },
616
+ "criticalBase": {},
614
617
  "workspaceRoot": {},
615
618
  "mirrorRoot": {}
616
619
  },
@@ -229,6 +229,48 @@ Keep the queue worth draining.
229
229
  - Examine widely, promote narrowly. Sixteen examined and four promoted is the
230
230
  shape to aim for — the cap is on what you **promote**, never on what you look at.
231
231
 
232
+ ### Grooming from intake
233
+
234
+ Ideas arrive cheap — `omp-conductor intake "<text>"` from anywhere, at any hour —
235
+ and every tick's prompt lists what is still pending (id and text, oldest
236
+ first) whenever any idea is waiting. Pending intake is a filing duty, not a
237
+ promotion duty:
238
+
239
+ For each pending item, file **exactly one** issue on **{{TRACKER_REPO}}**:
240
+
241
+ - the **title states the problem**, plainly;
242
+ - the **body carries the product rationale** (why this is worth building, from
243
+ the idea's own words) **and the acceptance criteria as a checklist** — the
244
+ same bar Duty 2 applies to any issue a worker will be handed;
245
+ - it carries the routing label (`repo:<repo>` — the prefix your setup renamed)
246
+ **and a priority**; what priority means here is project taste — see `POLICY.md`.
247
+
248
+ **Never add the `{{QUEUE_LABEL}}` queue label to an issue you groomed from
249
+ intake.** Filing is not promoting: the queue label is the claim gate and adding
250
+ it is a deliberate promotion decision under the ordinary rules above, not the
251
+ default for a filed idea. A groomed issue stays off the queue until someone
252
+ reads it and promotes it — adding `{{QUEUE_LABEL}}` to it afterwards dispatches
253
+ it normally.
254
+
255
+ Then close the loop, two commands, both required:
256
+
257
+ 1. **Mark provenance** so the idea is never filed twice:
258
+ `omp-conductor intake groomed <id> --issue <url>`. This is a no-op with a
259
+ message on an already-groomed id — ticks retry, and a repeat must never
260
+ re-file an idea it already filed.
261
+ 2. **Name the grooming in the digest ledger**, so the outcome is durable and
262
+ the next digest reports it: `omp-conductor event record --category intake
263
+ --summary "groomed intake <id> → #<n>" --evidence <url>`. This sends
264
+ nothing; it writes the row the digest reads, exactly like Duty 3's
265
+ `--category merge` example.
266
+
267
+ An item you cannot groom — malformed, empty, or not an idea at all — is
268
+ **dismissed, never filed**: `omp-conductor intake dismiss <id>`, with a note in
269
+ the digest rather than an issue. The point of intake is that capture is cheap
270
+ and grooming is asynchronous; do not stop the tick to interrogate a stray note.
271
+ Project-specific grooming taste — priority scales, label conventions, template
272
+ wording — belongs in `POLICY.md`; the floor above is the duty itself.
273
+
232
274
  ## Duty 3 — report
233
275
 
234
276
  See **Reporting** below. That section is yours, and it is the only thing that
@@ -287,16 +329,20 @@ queue with its state recorded. The ceiling applies even when you omit
287
329
  `telegram_ask` tool is refused on a locally injected tick precisely because it
288
330
  would wait for your operator as long as the answer takes: an unanswered
289
331
  question must never hold the loop. If the question must demonstrably reach the
290
- operator through Telegram, send it separately: `omp-conductor message --text
291
- "QUESTION: "` on a locally injected tick, or a `telegram_send` whose text
292
- begins `QUESTION:` while you are answering a live message in its own topic.
293
- Either way the marker is what makes the autonomous-tick gate apply the decision
294
- category. When the question is itself the escalation a condition that stops
332
+ operator through Telegram, send it separately: `omp-conductor message
333
+ --category <category> --text "<the question>"` on a locally injected tick
334
+ the escalation category is declared from the vocabulary, never a `QUESTION:`
335
+ text prefix, and the question is recorded as an open decision row (parked on
336
+ silence) before delivery or a `telegram_send` whose text begins `QUESTION:`
337
+ while you are answering a live message in its own topic. Either way the
338
+ declared or marker-carried category is what the autonomous-tick gate applies.
339
+ When the question is itself the escalation — a condition that stops
295
340
  the fleet, a tier-2 block the policy may page for — pass `category` on the ask
296
341
  (`"fleet-stopped"`, `"tier2"`, `"decision-needed"`, `"confirmed-failure"`) so
297
342
  the delivery is admitted under the configured scope; an untagged ask is treated
298
343
  as an ordinary decision-needed question. The decision row a `conductor_ask`
299
- seeds is what stops the question from being forgotten: trust the digest, never
344
+ seeds or the fallback command opens itself is what stops the question from
345
+ being forgotten: trust the digest, never
300
346
  your recollection of having asked. In both directions, the delivery
301
347
  contract is explicit: a message you did not explicitly send is a message that
302
348
  did not arrive.
@@ -323,9 +369,12 @@ active topic to keep. When such a turn must reach your operator directly rather
323
369
  than through a report, run
324
370
  `omp-conductor message --text "<the message>"`: it resolves this project's own
325
371
  chat and topic from config, applies the same availability policy an autonomous
326
- Telegram call gets, and prints either the delivery or the held-notice id. Begin
327
- the text `QUESTION:` when you are asking for something, so it carries the
328
- decision category. Reports still go through `omp-conductor report`.
372
+ Telegram call gets, and prints either the delivery or the held-notice id. To
373
+ ask for something, declare the escalation category: `omp-conductor message
374
+ --category <category> --text "<the question>"` the question is recorded as an
375
+ open decision row (parked on silence) before delivery, and the marker form
376
+ (text beginning `QUESTION:`) is still accepted and read as
377
+ `decision-needed`. Reports still go through `omp-conductor report`.
329
378
 
330
379
  If the answer needs a decision from the operator (a choice, a yes/no, or an
331
380
  approval) while you are answering a live message, ask it with `telegram_ask`:
@@ -552,8 +601,12 @@ The protocol, in order:
552
601
  auto-applied, which this protocol never does for POLICY.md. A returned answer proves an answer, not Telegram delivery.
553
602
  If the proposal must demonstrably reach the operator through Telegram, or
554
603
  `conductor_ask` is unavailable, send the compact yes/no question separately
555
- with `omp-conductor message --text "QUESTION: "` — on a tick that is the
556
- only path that reaches this project's own topic.
604
+ with `omp-conductor message --category decision-needed --text "<the question>"` —
605
+ on a tick that is the only path that reaches this project's own topic. The
606
+ command records the question as an open decision row before it delivers
607
+ (parked on silence: re-surfaced in every tick until answered or the
608
+ seven-day expiry), so an unanswered yes/no is a recorded "still pending",
609
+ never an approval.
557
610
  Wait for the operator's later reply, and never assume one. Telegram renders
558
611
  none of your markdown, so asterisks and backticks arrive as literal characters:
559
612
  - Lead with one plain sentence: what changes, and why, in your own words.
@@ -94,8 +94,21 @@ Hand every reportable event to the conductor's outbox:
94
94
  ```
95
95
  omp-conductor report --text "<the whole report>" # a material event
96
96
  omp-conductor report --text "<the whole digest>" --kind digest
97
+ omp-conductor report --text "<release published — install is yours>" --kind tier2
98
+ omp-conductor report --text "<fleet is up; no queue movement>" --kind fleet-stopped
99
+ omp-conductor report --text "<the event>" --kind confirmed-failure
97
100
  ```
98
101
 
102
+ `--kind` names the category from the policy vocabulary (`material`, `tier2`,
103
+ `decision-needed`, `fleet-stopped`, `confirmed-failure`, or `digest`); anything
104
+ else is refused by name. The policy decides interruption — a `tier2` handoff
105
+ still lands in the digest when `interruptOn` omits it or the availability
106
+ window is closed, and is held durably (never dropped). A plain `--text` with no
107
+ `--kind` is a `material` report, which is digest-only wherever the policy does
108
+ not page material. Only the operator or orchestrator may hand off an escalation
109
+ category; a worker session reporting `tier2`/`decision-needed`/`fleet-stopped`/
110
+ `confirmed-failure` is refused, since escalation tier is not a worker's to claim.
111
+
99
112
  The command persists the text *before* anything is sent and prints a durable
100
113
  handoff id. During quiet hours a material report becomes a held-notice id for
101
114
  the next digest or working-hours catch-up; otherwise it becomes a report id and
@@ -118,7 +131,9 @@ waiting — an answer to their message, or a question of your own. Answer in the
118
131
  topic the message arrived in: name neither `chat_id` nor `thread_id`, or name
119
132
  both; naming the chat alone drops a forum reply into the main chat and is
120
133
  refused. A locally injected tick has no such message to answer, so its direct
121
- delivery is `omp-conductor message --text "…"`, which resolves this project's
134
+ delivery is `omp-conductor message --text "…"` with `--category` and a
135
+ declared escalation category on a question, which records it as an open
136
+ decision row (parked on silence) before delivery — and resolves this project's
122
137
  own chat and topic. Neither is a report: they leave no record that anything went
123
138
  out. `telegram_ask` is the decision primitive for a live conversation — and on a
124
139
  locally injected tick it is refused as unbounded, so the ask surface there is
@@ -127,8 +142,9 @@ locally injected tick it is refused as unbounded, so the ask surface there is
127
142
  or `park` — leaves the row pending, re-surfaced in every tick), and a ceiling
128
143
  that applies even when the ask names none. A returned answer proves an
129
144
  answer, not Telegram delivery. A cancelled or errored ask is not an
130
- answer: re-deliver the question with text beginning
131
- `QUESTION:` so an autonomous tick applies the decision category, or report the
145
+ answer: re-deliver the question with `omp-conductor message --category <category> --text "<the question>"`
146
+ the category is declared from the escalation vocabulary, never a `QUESTION:`
147
+ text prefix, and the command opens the decision row itself — or report the
132
148
  channel as broken. A timed-out ask is "nobody answered yet" — it either
133
149
  auto-applied its recorded recommendation or is still pending; it is never
134
150
  "asked once, no reply, dropped".
@@ -24,17 +24,20 @@ cannot account for, and the checks that would have refused a bad push never ran.
24
24
  Reading with `gh` is fine; `git push`, `gh pr create` and `gh pr merge` are not
25
25
  yours to run.
26
26
 
27
- Read the issue first. **This brief is your copy of it**: the acceptance criteria
28
- below were rendered from the issue at dispatch, so you already have what you
29
- need. One `gh` read is still worth the call, for the discussion the brief did
30
- not copy down:
27
+ Read the issue first. **This brief is your copy of it**: the acceptance
28
+ criteria below were rendered from the issue at dispatch, and every comment on
29
+ it is rendered in the Discussion section. One `gh` read is still worth the
30
+ call, to compare the live issue against the copy in this brief:
31
31
 
32
32
  ```bash
33
- gh issue view {{ISSUE_NUMBER}} --repo {{TRACKER_REPO}} --comments
33
+ gh issue view {{ISSUE_NUMBER}} --repo {{TRACKER_REPO}} --json comments --jq '.comments[]|"@\(.author.login): \(.body)"'
34
34
  ```
35
35
 
36
- If that command fails, work from this brief and say so in your report rather
37
- than treating it as a blocker.
36
+ **If that command prints nothing, the read failed say so in your report.**
37
+ `gh issue view --comments` prints nothing on a non-TTY while still exiting 0,
38
+ so empty output is never proof of an empty discussion. The Discussion section
39
+ below is what was read at dispatch; a live read that prints nothing while it
40
+ is there is a broken read, not a silent queue.
38
41
 
39
42
  Then read the repo's own guidance before writing anything: `AGENTS.md`,
40
43
  `CLAUDE.md`, `CONTEXT.md`, and any `docs/adr/` entry the issue touches. Those
@@ -44,7 +47,7 @@ files are canonical; your priors are not.
44
47
 
45
48
  {{ACCEPTANCE_CRITERIA}}
46
49
 
47
- ## How to work
50
+ {{ISSUE_COMMENTS}}## How to work
48
51
 
49
52
  1. **Understand before editing — and ask the graph before you grep.** Your turns
50
53
  are mostly spent finding code, not writing it, and running out of turns