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.
- package/REFERENCE.md +107 -60
- package/package.json +1 -1
- package/schema/config.schema.json +3 -0
- package/src/briefs/orchestrator.md +64 -11
- package/src/briefs/policy.md +19 -3
- package/src/briefs/worker.md +11 -8
- package/src/cli.ts +41 -21
- package/src/commands/context.ts +102 -1
- package/src/commands/doctor.ts +4 -2
- package/src/commands/intake.ts +26 -5
- package/src/commands/message.ts +80 -32
- package/src/commands/report.ts +38 -2
- package/src/commands/restart.ts +81 -54
- package/src/commands/setup.ts +61 -11
- package/src/commands/stop.ts +45 -22
- package/src/commands/upgrade-rollback.ts +9 -0
- package/src/config-schema.ts +9 -0
- package/src/config.ts +35 -1
- package/src/daemon.ts +588 -37
- package/src/dashboard/app.js +398 -59
- package/src/dashboard/index.html +27 -0
- package/src/dashboard/server.ts +219 -5
- package/src/dashboard/style.css +169 -1
- package/src/doctor.ts +419 -45
- package/src/escalate.ts +8 -0
- package/src/failure-class.ts +37 -0
- package/src/fleet.ts +49 -2
- package/src/gitops.ts +157 -0
- package/src/lifecycle.ts +113 -2
- package/src/model-fallback.ts +177 -0
- package/src/omp.ts +115 -13
- package/src/orchestrator-down.ts +231 -0
- package/src/orchestrator-tick.ts +108 -5
- package/src/orchestrator.ts +18 -4
- package/src/privileged.ts +10 -0
- package/src/release-policy.ts +373 -28
- package/src/session-host.ts +11 -5
- package/src/setup-host.ts +665 -70
- package/src/setup-install.ts +275 -28
- package/src/setup-wizard.ts +339 -126
- package/src/setup.ts +25 -0
- package/src/stop-provenance.ts +66 -0
- package/src/store.ts +194 -1
- package/src/tracker/github.ts +47 -0
- package/src/types.ts +182 -0
- package/src/upgrade.ts +110 -32
- package/src/verbs/protocol.ts +16 -3
- package/src/verbs/server.ts +27 -1
- package/src/wizard-ui.ts +261 -46
- package/src/worker.ts +24 -3
- 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>"
|
|
1229
|
-
omp-conductor message --text "
|
|
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
|
|
1234
|
-
durably held for the digest or the working-hours catch-up and
|
|
1235
|
-
that held-notice id instead of claiming delivery
|
|
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
|
|
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,
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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.
|
|
2591
|
-
`SuccessExitStatus=0 143
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
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 <t>, 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.
|
|
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
|
|
291
|
-
"
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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.
|
|
327
|
-
|
|
328
|
-
|
|
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 "
|
|
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.
|
package/src/briefs/policy.md
CHANGED
|
@@ -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 "…"
|
|
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
|
|
131
|
-
|
|
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".
|
package/src/briefs/worker.md
CHANGED
|
@@ -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
|
|
28
|
-
below were rendered from the issue at dispatch,
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
37
|
-
|
|
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
|