omp-conductor 0.16.1 → 0.17.0

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/README.md CHANGED
@@ -114,6 +114,31 @@ For what the host needs before anything runs — `bun`, an authenticated `gh`,
114
114
  `omp-telegram` for the escalation channel — see the
115
115
  [Install prerequisites](https://github.com/TerrifiedBug/conductor/blob/main/omp/REFERENCE.md#install-prerequisites) in the reference.
116
116
 
117
+ ### Shell completions
118
+
119
+ `omp-conductor complete` generates completions for zsh, bash, fish, and
120
+ PowerShell. Load them for the current shell:
121
+
122
+ ```bash
123
+ source <(omp-conductor complete zsh)
124
+ # or
125
+ source <(omp-conductor complete bash)
126
+ ```
127
+
128
+ For a persistent zsh install:
129
+
130
+ ```bash
131
+ mkdir -p ~/.omp/conductor
132
+ omp-conductor complete zsh > ~/.omp/conductor/completions.zsh
133
+ echo '[ -f ~/.omp/conductor/completions.zsh ] && source ~/.omp/conductor/completions.zsh' >> ~/.zshrc
134
+ ```
135
+
136
+ Use `bash` and `~/.bashrc` for the equivalent bash install. In fish, run
137
+ `omp-conductor complete fish | source`; in PowerShell, run
138
+ `omp-conductor complete powershell | Out-String | Invoke-Expression`.
139
+ After a successful interactive setup, the wizard offers to install zsh or bash
140
+ completions this way and adds the rc source line only when it is absent.
141
+
117
142
  ## Quick start
118
143
 
119
144
  1. Install `omp-conductor` and `omp-telegram`. Pair the Telegram bot and enable its bridge.
@@ -129,6 +154,26 @@ For what the host needs before anything runs — `bun`, an authenticated `gh`,
129
154
 
130
155
  The wizard reads the tracker with the same routing code as the daemon. It shows every issue that the next tick can route.
131
156
 
157
+ On an interactive TTY, setup uses the styled Clack interface. Piped setup
158
+ keeps the stable line protocol for automation. Set
159
+ `OMP_CONDUCTOR_PLAIN_UI=1` to use the readline interface on a TTY and replace
160
+ Clack progress in other verbs with their stable plain messages.
161
+
162
+ To make an interactive run replayable, save every accepted answer by its
163
+ stable key:
164
+
165
+ ```bash
166
+ omp-conductor setup --save-answers ./setup-answers.json
167
+ omp-conductor setup --answers ./setup-answers.json
168
+ ```
169
+
170
+ An answers file is a JSON object whose values are strings or booleans.
171
+ Confirmations require booleans, text prompts require strings, and select
172
+ prompts require the exact displayed label. `--answers` never falls back to
173
+ an interactive prompt: a missing key or wrong value fails with the key and
174
+ question name. Piped line input remains supported, but an exhausted pipe
175
+ fails with the `--answers` alternative instead of waiting indefinitely.
176
+
132
177
  Nothing changes before the consent step. After consent, setup does these actions:
133
178
 
134
179
  - creates the required labels;
@@ -356,6 +401,19 @@ fleet, and both are covered above under
356
401
  paused-fleet failure hides. `tail`, `extend`, `unblock` and the `worker`
357
402
  controls are the day-to-day levers:
358
403
 
404
+ For automation, `status --json` emits the same project, layer, daemon,
405
+ Telegram, code-graph, run, cap, report, and sibling data as the text report.
406
+ The other read-only ledgers have stable JSON forms too:
407
+
408
+ ```bash
409
+ omp-conductor ledger --json
410
+ omp-conductor decision list --json
411
+ omp-conductor watch list --json
412
+ ```
413
+
414
+ JSON output is never styled. Empty ledgers return their normal top-level object
415
+ with an empty `entries`, `decisions`, or `watches` array.
416
+
359
417
  ### Outcomes and cost: `stats`
360
418
 
361
419
  `omp-conductor stats [--since 7d | 30d | YYYY-MM-DD] [--project NAME] [--json]`
@@ -520,13 +578,13 @@ reference — usage strings, flags and per-command behaviour — is in the
520
578
 
521
579
  | Verb | What it does |
522
580
  | --- | --- |
523
- | `setup [area]` | The wizard: interview + probes. `--no-ai`, per-area amend, `host`, `graph`. |
581
+ | `setup [area]` | The wizard: interview + probes. `--no-ai`, `--answers FILE`, `--save-answers FILE`, per-area amend, `host`, `graph`. |
524
582
  | `start` / `stop` / `restart` | Run the dispatch daemon. `restart` drains first; `stop --pane` also halts the pane. |
525
583
  | `upgrade [--to VERSION]` | Pin one published npm release across CLI, omp plugin, Herdr plugin and brief. |
526
- | `status` | Layered fleet report: dispatch, ticks, pane, recovery, telegram, daemon, then the project body. |
584
+ | `status [--json]` | Layered fleet report: dispatch, ticks, pane, recovery, telegram, daemon, then the project body. |
527
585
  | `stats [--since 7d|30d|YYYY-MM-DD] [--json]` | What the fleet accomplished and at what cost, from the local store only: merges, merge rate, lead time, attempts and metered cost per merged issue, failure classes, gh calls. |
528
586
  | `doctor [--json] [--probe-telegram]` | Read-only deployment health — gh auth, exact-case labels, systemd drift, config backup, sqlite integrity, spend telemetry, timezones, Telegram. Run after install and after every upgrade; exit 0 only when nothing failed. |
529
- | `ledger` | The action audit — every mediated verb, refusal and turn budget. |
587
+ | `ledger [--json]` | The action audit — every mediated verb, refusal and turn budget. |
530
588
  | `board` | Live terminal kanban from Queue to Settled. |
531
589
  | `dashboard [--port N] [--host ADDR]` | Browser UI plus bearer-authenticated `/api/projects`: every project's daemon state, port and healthz. Loopback by default; the token lives at `<state>/dashboard-token`. |
532
590
  | `hold [--keep-ticks]` | Pause claims and disarm ticks — the soft stop. |
@@ -538,7 +596,8 @@ reference — usage strings, flags and per-command behaviour — is in the
538
596
  | `verb <conductor_*>` | Run a mediated verb from the CLI (external orchestration). |
539
597
  | `friction <kind> --detail TEXT` | Record a bounded operator judgment — an escalation that belonged in a digest, or a report that was noise/surprising — feeding the learning loop. |
540
598
  | `event record` / `report` / `message` | Record and deliver reports and messages through the durable outbox. |
541
- | `decision open/resolve/withdraw/list` | Read and answer questions in the decision ledger. |
599
+ | `decision open/resolve/withdraw/list [--json]` | Read and answer questions in the decision ledger; JSON applies to `list`. |
600
+ | `watch add/list [--json]` | Record or list orchestrator-only conditions and carry notes; JSON applies to `list`. |
542
601
  | `intake "<text>"` / `intake list` / `intake dismiss <id>` | Capture a raw idea durably (it lives in the store and survives restarts), list what is still pending, dismiss what turned out to be nothing. |
543
602
  | `daemon [--once]` | Run the loop in the foreground — systemd's entry point. |
544
603
  | `resume` | Clear pause and any `stop --pane` recovery pin; never re-arms. |
package/REFERENCE.md CHANGED
@@ -265,6 +265,12 @@ Skip the reading half entirely with `--no-ai`:
265
265
  omp-conductor setup --no-ai
266
266
  ```
267
267
 
268
+ For unattended setup, pass `--answers FILE` with a JSON object keyed by the
269
+ stable prompt keys captured by `--save-answers`; the command exits `1` and
270
+ names the first missing key instead of prompting. Add `--save-answers FILE`
271
+ to an interactive run to capture only accepted answers for deterministic
272
+ replay. Unknown keys are ignored so one file can cover a larger interview.
273
+
268
274
  To fill in or revise just the brief later — the two `POLICY.md` sections above — run
269
275
  the `brief` area, which re-asks the judgment questions and re-runs the probes:
270
276
 
@@ -2202,7 +2208,7 @@ least of all on a fleet whose session lives somewhere else.
2202
2208
  ## CLI reference
2203
2209
 
2204
2210
  ```bash
2205
- omp-conductor setup [area] [--no-ai] [--project NAME]
2211
+ omp-conductor setup [area] [--no-ai] [--answers FILE] [--save-answers FILE] [--project NAME]
2206
2212
  omp-conductor setup host [NAME] [--project NAME]
2207
2213
  omp-conductor setup graph [--no-seed] [--print] [--project NAME]
2208
2214
  omp-conductor start [--port N] [--project NAME]
@@ -2212,10 +2218,10 @@ omp-conductor restart [--now] [--timeout SECONDS] [--port N] [--project NAME]
2212
2218
  omp-conductor upgrade [--to VERSION] [--project NAME]
2213
2219
  omp-conductor upgrade-install --to VERSION [--project NAME]
2214
2220
  omp-conductor upgrade-rollback
2215
- omp-conductor status [--project NAME]
2221
+ omp-conductor status [--project NAME] [--json]
2216
2222
  omp-conductor doctor [--project NAME] [--json] [--probe-telegram]
2217
- omp-conductor ledger [--issue N] [--limit N] [--project NAME]
2218
- omp-conductor board [--project NAME]
2223
+ omp-conductor ledger [--issue N] [--limit N] [--project NAME] [--json]
2224
+ omp-conductor board [--project NAME] [--json]
2219
2225
  omp-conductor hold [--keep-ticks] [--project NAME]
2220
2226
  omp-conductor stop [--pane] [--project NAME]
2221
2227
  omp-conductor arm [--project NAME]
@@ -2234,26 +2240,28 @@ omp-conductor message --text TEXT [--project NAME]
2234
2240
  omp-conductor decision open --question TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]
2235
2241
  omp-conductor decision resolve <id> --answer TEXT [--project NAME]
2236
2242
  omp-conductor decision withdraw <id> [--reason TEXT] [--project NAME]
2237
- omp-conductor decision list [--project NAME]
2243
+ omp-conductor decision list [--project NAME] [--json]
2238
2244
  omp-conductor watch add --note TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]
2239
- omp-conductor watch list [--project NAME]
2245
+ omp-conductor watch list [--project NAME] [--json]
2240
2246
  omp-conductor daemon [--once] [--port N] [--project NAME]
2241
2247
  omp-conductor resume [--project NAME]
2242
2248
  omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
2249
+ omp-conductor complete <zsh|bash|fish|powershell>
2250
+ omp-conductor complete -- <args...>
2243
2251
  omp-conductor help
2244
2252
  ```
2245
2253
 
2246
2254
  | Command | Scope | Behaviour |
2247
2255
  | --- | --- | --- |
2248
- | `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. |
2256
+ | `setup [area] [--no-ai] [--answers FILE] [--save-answers FILE] [--project NAME]` | project | The deterministic interview, with styled Clack prompts on an interactive TTY and byte-stable plain output for pipes or `OMP_CONDUCTOR_PLAIN_UI=1`. `--answers` validates a JSON object of stable prompt keys before setup and replaces every prompt; a missing required key exits `1` naming the key and file instead of hanging. `--save-answers` records accepted interactive answers as replayable JSON after a successful run. Bare setup is a full first run, or — when the project already exists — a chooser of which area to amend. Naming an area positionally skips that chooser and amends only that area: `tracker`, `gates`, `caps`, `code-graph`, `authority`, `policy`, `escalation`, `reporting`, `brief`. `host` and `graph` are install subcommands rather than areas and are matched first; anything else exits `2` listing both vocabularies. Every prompt shows its current value as the default, and Enter accepts what you see; `Ctrl-C` at any prompt abandons the run and writes nothing. Setup also **reads your repos to propose answers**: the gates prompt is pre-filled from what CI actually runs, and the brief's `## Project context` and release procedure are drafted from every routing repo and shown for confirmation before anything is written. Each probe is a short session with **no shell, no editor and no verbs** in a throwaway shallow clone, and every answer is a proposal you edit or decline — a probe that cannot clone, cannot reach a model, or answers unusably costs you one warning and the shipped stub. `--no-ai` asks every question with the reading half removed. |
2249
2257
  | `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. |
2250
2258
  | `setup graph [--no-seed] [--print] [--project NAME]` | project | The code-graph install end to end, in one preview and one confirm: check the prerequisites read-only and stop before installing anything when `codebase-memory-mcp` is absent or no MCP entry mounts it (printing the entry to add); `git clone` each missing index-only checkout **as you, never through sudo**; install and enable `cbm-reindex.timer` as root; then seed one indexing run so the first fetch happens while you watch, and verify with the same probe `status` uses. A repo that does not verify is a failure with the remediation, not a success — staged-but-not-trusted is how you discover months later that no worker read an index. `--no-seed` enables the timer without the seeding run and says plainly the graph is unusable until it first fires; it never skips the prerequisite or clone steps. `--print` changes nothing. Exits `1` when no repo has [`graphProject`](#configuration). |
2251
2259
  | `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. |
2252
2260
  | `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. |
2253
2261
  | `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). |
2254
2262
  | `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. |
2255
- | `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. |
2256
- | `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`. |
2263
+ | `status [--project NAME] [--json]` | 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. `--json` emits the stable structured snapshot directly, without ANSI or prose, for automation. The text 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. |
2264
+ | `ledger [--issue N] [--limit N] [--json]` | project | The action audit: every [mediated-verb](#the-mediated-verbs-126) mutation and every next-attempt turn budget. Verb entries include the arguments, decision, named refusal, and resulting SHA. Turn-budget entries remain after an override is replaced or consumed. Reads (`conductor_pr_status`) are absent so polling cannot bury the signal. `--issue` narrows both histories; `--limit` defaults to 50. `--json` emits one stable object with `project`, optional `issue`, `entries`, `refused`, and `turnOverrides`. Recent verb refusals and pending turn overrides also appear in `status`. |
2257
2265
  | `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. |
2258
2266
  | `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). |
2259
2267
  | `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`. |
@@ -2270,9 +2278,9 @@ omp-conductor help
2270
2278
  | `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). |
2271
2279
  | `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. |
2272
2280
  | `decision withdraw <id> [--reason TEXT]` | project | Close a question the session stopped needing, with why. Same guard as `resolve`. |
2273
- | `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. Watches are not listed here — `watch list` shows those. |
2281
+ | `decision list [--json]` | project | Open questions, oldest first: id, age, what each blocks, whether its condition is met, and the question. `--json` emits `{ project, decisions }`; the empty state is an empty array. Prints `no open decisions` in text mode when there are none. Watches are not listed here — `watch list` shows those. |
2274
2282
  | `watch add --note TEXT [--blocks TEXT] [--resolves-when COND]` | project | Record a condition or carry note the orchestrator set for itself, with no human in the loop (#459). `--resolves-when` attaches a machine-checkable condition the daemon checks for you; a met watch wakes the next tick exactly as a met question does. Renders under its own "Watches" heading, is never counted in `decisions N open`, and has no seven-day expiry. |
2275
- | `watch list` | project | Open watches, oldest first: id, age, what each blocks, whether its condition is met, and the note. Prints `no watches` when there are none. |
2283
+ | `watch list [--json]` | project | Open watches, oldest first: id, age, what each blocks, whether its condition is met, and the note. `--json` emits `{ project, watches }`; the empty state is an empty array. Prints `no watches` in text mode when there are none. |
2276
2284
  | `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. |
2277
2285
  | `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. |
2278
2286
  | `--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. |
@@ -2285,6 +2293,7 @@ omp-conductor help
2285
2293
  | `--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. |
2286
2294
  | `--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. |
2287
2295
  | `--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. |
2296
+ | `complete <zsh\|bash\|fish\|powershell>` | none | Print a shell completion script. Source it directly for a session or save it and source the file from the shell rc. Interactive setup offers the persistent zsh/bash install; fish and PowerShell remain available manually. `complete -- <args...>` is the fast shell callback protocol: static verbs, subcommands, flags and setup areas come from the command manifest, while project names load from config and fail empty on any read error. |
2288
2297
  | `help`, `--help`, `-h` | none | Print usage. An unknown or missing verb prints it too, and exits `2`. |
2289
2298
 
2290
2299
  Pause is a sentinel under the state directory and survives a daemon restart.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
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.",
@@ -33,6 +33,8 @@
33
33
  "schema": "bun run src/generate-schema.ts"
34
34
  },
35
35
  "dependencies": {
36
+ "@bomb.sh/tab": "0.0.22",
37
+ "@clack/prompts": "1.7.0",
36
38
  "yaml": "^2.9.0",
37
39
  "zod": "^4"
38
40
  },
@@ -381,12 +381,27 @@ did not arrive.
381
381
 
382
382
  ## Human messages
383
383
 
384
- A human writing to you between ticks is not a tick. Answer with a **single
385
- `telegram_send` call** one message, the answer only, from evidence you already
386
- hold or go and fetch. Never answer as plain end-of-turn text: on this session,
387
- text you merely write reaches nobody — if you do not call `telegram_send`, the
388
- person gets silence. While handling any turn, produce no visible commentary
389
- between tool calls reasoning stays in thinking, actions stay in tools.
384
+ A human writing to you between ticks is not a tick, and how a reply reaches
385
+ them depends on which of three shapes this turn is a shape the brief states,
386
+ never something a session infers from a tool's error:
387
+
388
+ - **A locally injected tick** has no inbound message and no topic to keep:
389
+ hand anything reportable to the outbox with `omp-conductor report`, and reach
390
+ the operator directly with `omp-conductor message --text "<the message>"`,
391
+ declaring the escalation category when you are asking. Your tick prompt names
392
+ this shape — it carries the delivery rule that says when a tick was injected
393
+ locally.
394
+ - **A turn that began as an inbound Telegram message** is answered with a
395
+ **single `telegram_send` call** — one message, the answer only, from evidence
396
+ you already hold or go and fetch — omitting **both** `chat_id` and `thread_id`
397
+ so the reply keeps the topic the message came from.
398
+ - **An interactive terminal session**, started by hand, is one the operator is
399
+ reading live: there, end-of-turn text **is** the delivery, and Telegram is
400
+ used only when it must demonstrably arrive — `telegram_send` resolves
401
+ `chat_id` from the last inbound message and refuses without one.
402
+
403
+ While handling any turn, produce no visible commentary between tool calls —
404
+ reasoning stays in thinking, actions stay in tools.
390
405
 
391
406
  **Reply where the message arrived.** `thread_id` defaults to the active topic
392
407
  *only while `chat_id` is omitted*, so a fleet whose Telegram is a forum topic
@@ -85,11 +85,19 @@ authoritative. All four scopes, spelled out:
85
85
  interrupt when operator availability permits; everything else waits for one
86
86
  daily rollup.
87
87
 
88
- **Delivery.** Never rely on end-of-turn text reaching anyone. The provable
89
- delivery paths are `omp-conductor report` (reports — persisted and retried by
90
- the daemon) and `telegram_send` (direct messages). `telegram_ask` is the decision
91
- primitive, not Telegram delivery evidence. Everything else is noise or silence.
92
- Hand every reportable event to the conductor's outbox:
88
+ **Delivery.** A reply reaches the operator by the shape of the turn it answers.
89
+ A **locally injected tick** delivers through the outbox — `omp-conductor report`
90
+ for anything reportable, `omp-conductor message` to reach the operator directly
91
+ because a report written as end-of-turn text on a tick reaches nobody. A turn
92
+ that **began as an inbound Telegram message** answers with `telegram_send`,
93
+ omitting `chat_id` and `thread_id` so the reply keeps its topic. An **interactive
94
+ terminal session** is read live by the operator, so end-of-turn text *is* the
95
+ delivery there; `telegram_send` resolves `chat_id` from the last inbound message
96
+ and refuses without one, so it is used only when the message must demonstrably
97
+ arrive. The provable delivery paths are `omp-conductor report` (persisted and
98
+ retried by the daemon) and `telegram_send` (direct messages); `telegram_ask` is
99
+ the decision primitive, not delivery evidence. Hand every reportable event to
100
+ the conductor's outbox:
93
101
 
94
102
  ```
95
103
  omp-conductor report --text "<the whole report>" # a material event
@@ -0,0 +1,83 @@
1
+ // @clack/prompts is ESM-only and currently imports node:process in a shape
2
+ // that blocks a future `bun build`; this CLI intentionally executes TS in Bun.
3
+ import * as p from "@clack/prompts";
4
+ import { stdin, stdout } from "node:process";
5
+ import type { Readable, Writable } from "node:stream";
6
+
7
+ import type { TerminalUi } from "./wizard-ui.ts";
8
+
9
+ interface ClackIo {
10
+ input?: Readable;
11
+ output?: Writable;
12
+ }
13
+
14
+ /** Interactive WizardUi driver. Callers gate this on TTY input and output. */
15
+ export function clackUi(io: ClackIo = {}): TerminalUi {
16
+ const input = io.input ?? stdin;
17
+ const output = io.output ?? stdout;
18
+ const promptIo = { input, output };
19
+ let closed = false;
20
+ let started = false;
21
+ const start = (): void => {
22
+ if (started) return;
23
+ started = true;
24
+ p.intro("omp-conductor setup", { output });
25
+ };
26
+
27
+ return {
28
+ close: (message) => {
29
+ if (closed) return;
30
+ closed = true;
31
+ if (started) p.outro(message ?? "Setup finished.", { output });
32
+ },
33
+ notify: (message, type = "info") => {
34
+ start();
35
+ if (message.includes("\n")) {
36
+ const title = type === "warning" ? "Warning" : type === "error" ? "Error" : undefined;
37
+ p.note(message, title, { output });
38
+ return;
39
+ }
40
+ if (type === "warning") p.log.warning(message, { output });
41
+ else if (type === "error") p.log.error(message, { output });
42
+ else p.log.info(message, { output });
43
+ },
44
+ confirm: async (title, message) => {
45
+ start();
46
+ const result = await p.confirm({
47
+ ...promptIo,
48
+ message: `${title}\n${message}`,
49
+ initialValue: false,
50
+ });
51
+ return p.isCancel(result) ? undefined : result;
52
+ },
53
+ input: async (title, placeholder) => {
54
+ start();
55
+ const result = await p.text({
56
+ ...promptIo,
57
+ message: title,
58
+ placeholder,
59
+ defaultValue: placeholder,
60
+ });
61
+ return p.isCancel(result) ? undefined : result;
62
+ },
63
+ select: async (title, options, dialogOptions) => {
64
+ start();
65
+ const index = Math.max(
66
+ 0,
67
+ Math.min(dialogOptions?.initialIndex ?? 0, options.length - 1),
68
+ );
69
+ const initialValue = options[index]?.label;
70
+ const result = await p.select({
71
+ ...promptIo,
72
+ message: title,
73
+ options: options.map((option) => ({
74
+ value: option.label,
75
+ label: option.label,
76
+ hint: option.description,
77
+ })),
78
+ initialValue,
79
+ });
80
+ return p.isCancel(result) ? undefined : result;
81
+ },
82
+ };
83
+ }