omp-conductor 0.16.2 → 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 +38 -4
- package/REFERENCE.md +17 -11
- package/package.json +2 -1
- package/src/clack-ui.ts +83 -0
- package/src/command-manifest.ts +16 -7
- package/src/commands/arm.ts +7 -2
- package/src/commands/decision.ts +17 -7
- package/src/commands/doctor.ts +18 -1
- package/src/commands/hold.ts +9 -7
- package/src/commands/ledger.ts +25 -4
- package/src/commands/setup.ts +61 -10
- package/src/commands/stats.ts +9 -5
- package/src/commands/status.ts +32 -5
- package/src/commands/tail.ts +13 -1
- package/src/commands/watch.ts +16 -7
- package/src/fleet.ts +55 -15
- package/src/orchestrator-tick.ts +46 -7
- package/src/privileged.ts +3 -0
- package/src/setup-answers.ts +135 -0
- package/src/setup-install.ts +2 -0
- package/src/setup-probe.ts +1 -0
- package/src/setup-wizard.ts +120 -29
- package/src/ui/progress.ts +32 -0
- package/src/ui/style.ts +11 -0
- package/src/wizard-ui.ts +14 -5
package/README.md
CHANGED
|
@@ -154,6 +154,26 @@ completions this way and adds the rc source line only when it is absent.
|
|
|
154
154
|
|
|
155
155
|
The wizard reads the tracker with the same routing code as the daemon. It shows every issue that the next tick can route.
|
|
156
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
|
+
|
|
157
177
|
Nothing changes before the consent step. After consent, setup does these actions:
|
|
158
178
|
|
|
159
179
|
- creates the required labels;
|
|
@@ -381,6 +401,19 @@ fleet, and both are covered above under
|
|
|
381
401
|
paused-fleet failure hides. `tail`, `extend`, `unblock` and the `worker`
|
|
382
402
|
controls are the day-to-day levers:
|
|
383
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
|
+
|
|
384
417
|
### Outcomes and cost: `stats`
|
|
385
418
|
|
|
386
419
|
`omp-conductor stats [--since 7d | 30d | YYYY-MM-DD] [--project NAME] [--json]`
|
|
@@ -545,13 +578,13 @@ reference — usage strings, flags and per-command behaviour — is in the
|
|
|
545
578
|
|
|
546
579
|
| Verb | What it does |
|
|
547
580
|
| --- | --- |
|
|
548
|
-
| `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`. |
|
|
549
582
|
| `start` / `stop` / `restart` | Run the dispatch daemon. `restart` drains first; `stop --pane` also halts the pane. |
|
|
550
583
|
| `upgrade [--to VERSION]` | Pin one published npm release across CLI, omp plugin, Herdr plugin and brief. |
|
|
551
|
-
| `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. |
|
|
552
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. |
|
|
553
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. |
|
|
554
|
-
| `ledger` | The action audit — every mediated verb, refusal and turn budget. |
|
|
587
|
+
| `ledger [--json]` | The action audit — every mediated verb, refusal and turn budget. |
|
|
555
588
|
| `board` | Live terminal kanban from Queue to Settled. |
|
|
556
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`. |
|
|
557
590
|
| `hold [--keep-ticks]` | Pause claims and disarm ticks — the soft stop. |
|
|
@@ -563,7 +596,8 @@ reference — usage strings, flags and per-command behaviour — is in the
|
|
|
563
596
|
| `verb <conductor_*>` | Run a mediated verb from the CLI (external orchestration). |
|
|
564
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. |
|
|
565
598
|
| `event record` / `report` / `message` | Record and deliver reports and messages through the durable outbox. |
|
|
566
|
-
| `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`. |
|
|
567
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. |
|
|
568
602
|
| `daemon [--once]` | Run the loop in the foreground — systemd's entry point. |
|
|
569
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,9 +2240,9 @@ 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]
|
|
@@ -2247,15 +2253,15 @@ omp-conductor help
|
|
|
2247
2253
|
|
|
2248
2254
|
| Command | Scope | Behaviour |
|
|
2249
2255
|
| --- | --- | --- |
|
|
2250
|
-
| `setup [area] [--no-ai] [--project NAME]` | project | The deterministic interview,
|
|
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. |
|
|
2251
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. |
|
|
2252
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). |
|
|
2253
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. |
|
|
2254
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. |
|
|
2255
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). |
|
|
2256
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. |
|
|
2257
|
-
| `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. |
|
|
2258
|
-
| `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`. |
|
|
2259
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. |
|
|
2260
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). |
|
|
2261
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`. |
|
|
@@ -2272,9 +2278,9 @@ omp-conductor help
|
|
|
2272
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). |
|
|
2273
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. |
|
|
2274
2280
|
| `decision withdraw <id> [--reason TEXT]` | project | Close a question the session stopped needing, with why. Same guard as `resolve`. |
|
|
2275
|
-
| `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. |
|
|
2276
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. |
|
|
2277
|
-
| `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. |
|
|
2278
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. |
|
|
2279
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. |
|
|
2280
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. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.
|
|
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.",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@bomb.sh/tab": "0.0.22",
|
|
37
|
+
"@clack/prompts": "1.7.0",
|
|
37
38
|
"yaml": "^2.9.0",
|
|
38
39
|
"zod": "^4"
|
|
39
40
|
},
|
package/src/clack-ui.ts
ADDED
|
@@ -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
|
+
}
|
package/src/command-manifest.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
59
59
|
description: "interview, configure the fleet, or install host and graph surfaces",
|
|
60
60
|
scope: "project",
|
|
61
61
|
usage: [
|
|
62
|
-
"setup [area] [--no-ai] [--project NAME]",
|
|
62
|
+
"setup [area] [--no-ai] [--answers FILE] [--save-answers FILE] [--project NAME]",
|
|
63
63
|
"setup host [NAME] [--project NAME]",
|
|
64
64
|
"setup graph [--no-seed] [--print] [--project NAME]",
|
|
65
65
|
],
|
|
@@ -71,6 +71,8 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
71
71
|
flags: [
|
|
72
72
|
project(),
|
|
73
73
|
toggle("--no-ai", "ask every question without AI repository probes"),
|
|
74
|
+
value("--answers", "answer every prompt from a JSON file"),
|
|
75
|
+
value("--save-answers", "save successful prompt answers as JSON"),
|
|
74
76
|
toggle("--no-seed", "enable graph indexing without the initial seed"),
|
|
75
77
|
toggle("--print", "print the graph install plan without changing anything"),
|
|
76
78
|
],
|
|
@@ -151,8 +153,8 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
151
153
|
name: "status",
|
|
152
154
|
description: "print layered fleet and deployment status",
|
|
153
155
|
scope: "fleet",
|
|
154
|
-
usage: ["status [--project NAME]"],
|
|
155
|
-
flags: [project()],
|
|
156
|
+
usage: ["status [--project NAME] [--json]"],
|
|
157
|
+
flags: [project(), toggle("--json", "print the stable status JSON shape")],
|
|
156
158
|
},
|
|
157
159
|
{
|
|
158
160
|
name: "stats",
|
|
@@ -188,8 +190,13 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
188
190
|
name: "ledger",
|
|
189
191
|
description: "print conductor verb and daemon decision history",
|
|
190
192
|
scope: "project",
|
|
191
|
-
usage: ["ledger [--issue N] [--limit N] [--project NAME]"],
|
|
192
|
-
flags: [
|
|
193
|
+
usage: ["ledger [--issue N] [--limit N] [--project NAME] [--json]"],
|
|
194
|
+
flags: [
|
|
195
|
+
value("--issue", "filter by issue number"),
|
|
196
|
+
value("--limit", "maximum rows"),
|
|
197
|
+
project(),
|
|
198
|
+
toggle("--json", "print the stable ledger JSON shape"),
|
|
199
|
+
],
|
|
193
200
|
},
|
|
194
201
|
{
|
|
195
202
|
name: "hold",
|
|
@@ -362,7 +369,7 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
362
369
|
"decision open --question TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]",
|
|
363
370
|
"decision resolve <id> --answer TEXT [--project NAME]",
|
|
364
371
|
"decision withdraw <id> [--reason TEXT] [--project NAME]",
|
|
365
|
-
"decision list [--project NAME]",
|
|
372
|
+
"decision list [--project NAME] [--json]",
|
|
366
373
|
],
|
|
367
374
|
subcommands: [
|
|
368
375
|
{ name: "open", description: "record a question for the operator" },
|
|
@@ -376,6 +383,7 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
376
383
|
value("--resolves-when", "automatic resolution condition"),
|
|
377
384
|
value("--answer", "operator answer"),
|
|
378
385
|
value("--reason", "withdrawal reason"),
|
|
386
|
+
toggle("--json", "print decision list as stable JSON"),
|
|
379
387
|
project(),
|
|
380
388
|
],
|
|
381
389
|
positionals: [{ name: "action" }, { name: "id" }],
|
|
@@ -386,7 +394,7 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
386
394
|
scope: "project",
|
|
387
395
|
usage: [
|
|
388
396
|
"watch add --note TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]",
|
|
389
|
-
"watch list [--project NAME]",
|
|
397
|
+
"watch list [--project NAME] [--json]",
|
|
390
398
|
],
|
|
391
399
|
subcommands: [
|
|
392
400
|
{ name: "add", description: "record a watch" },
|
|
@@ -396,6 +404,7 @@ export const COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
|
|
|
396
404
|
value("--note", "note carried when the watch resolves"),
|
|
397
405
|
value("--blocks", "what the watch blocks"),
|
|
398
406
|
value("--resolves-when", "automatic resolution condition"),
|
|
407
|
+
toggle("--json", "print watch list as stable JSON"),
|
|
399
408
|
project(),
|
|
400
409
|
],
|
|
401
410
|
positionals: [{ name: "action" }],
|
package/src/commands/arm.ts
CHANGED
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
import type { CommandContext } from "./context.ts";
|
|
10
10
|
import { armTicks } from "../fleet.ts";
|
|
11
|
+
import { withProgress } from "../ui/progress.ts";
|
|
11
12
|
|
|
12
13
|
export async function armCommand(ctx: CommandContext): Promise<void> {
|
|
13
14
|
for (const project of ctx.targetProjects()) {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const r = await withProgress(
|
|
16
|
+
"arm: sending inbound Telegram challenge…",
|
|
17
|
+
"Inbound Telegram round-trip proved",
|
|
18
|
+
() => armTicks(project.name),
|
|
19
|
+
{ plainMessage: true },
|
|
20
|
+
);
|
|
16
21
|
process.stdout.write(
|
|
17
22
|
`ARMED — inbound round-trip proved with owner ${r.owner}; ticks are now live.\n` +
|
|
18
23
|
`marker ${r.path}${r.alreadyArmed ? " (replaced previous marker)" : ""}\n`,
|
package/src/commands/decision.ts
CHANGED
|
@@ -86,17 +86,27 @@ try {
|
|
|
86
86
|
// were the whole reason an operator read the orchestrator's own reminder as
|
|
87
87
|
// a question aimed at them (#459) — `watch list` shows those.
|
|
88
88
|
const open = store.openDecisions(project.name).filter((d) => d.kind !== "watch");
|
|
89
|
-
|
|
89
|
+
const json = ctx.argv.includes("--json");
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const decisions = open.map((d) => ({
|
|
92
|
+
id: d.id,
|
|
93
|
+
ageHours: Math.max(0, Math.round((now - d.askedAt) / 3_600_000)),
|
|
94
|
+
blocks: d.blocks ?? null,
|
|
95
|
+
condition: d.condition === undefined ? null : d.conditionMetAt === undefined ? "pending" : "met",
|
|
96
|
+
question: d.question,
|
|
97
|
+
}));
|
|
98
|
+
if (json) {
|
|
99
|
+
process.stdout.write(`${JSON.stringify({ project: project.name, decisions }, null, 2)}\n`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (decisions.length === 0) {
|
|
90
103
|
process.stdout.write("no open decisions\n");
|
|
91
104
|
return;
|
|
92
105
|
}
|
|
93
|
-
const
|
|
94
|
-
for (const d of open) {
|
|
95
|
-
const condition =
|
|
96
|
-
d.condition === undefined ? "-" : d.conditionMetAt === undefined ? "pending" : "met";
|
|
97
|
-
const hours = Math.max(0, Math.round((now - d.askedAt) / 3_600_000));
|
|
106
|
+
for (const decision of decisions) {
|
|
98
107
|
process.stdout.write(
|
|
99
|
-
`${
|
|
108
|
+
`${decision.id} ${decision.ageHours}h blocks:${decision.blocks ?? "-"} ` +
|
|
109
|
+
`condition:${decision.condition ?? "-"} ${decision.question}\n`,
|
|
100
110
|
);
|
|
101
111
|
}
|
|
102
112
|
return;
|
package/src/commands/doctor.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { CommandContext } from "./context.ts";
|
|
12
12
|
import { runDoctor, type DoctorReport, type FindingStatus } from "../doctor.ts";
|
|
13
|
+
import { dim, fail, heading, ok, warn } from "../ui/style.ts";
|
|
13
14
|
|
|
14
15
|
const DOCTOR_USAGE = `omp-conductor doctor — check the deployment faults that have already cost
|
|
15
16
|
debugging sessions, mechanically and read-only.
|
|
@@ -69,6 +70,22 @@ export function renderDoctorReport(report: DoctorReport): string {
|
|
|
69
70
|
return `${lines.join("\n")}\n`;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function styleDoctorReport(report: DoctorReport, text: string): string {
|
|
74
|
+
return text
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line, index) => {
|
|
77
|
+
if (index === 0) return heading(line);
|
|
78
|
+
if (index === 1)
|
|
79
|
+
return report.status === "fail" ? fail(line) : report.status === "warn" ? warn(line) : ok(line);
|
|
80
|
+
if (line.startsWith(" FAIL ")) return fail(line);
|
|
81
|
+
if (line.startsWith(" WARN ")) return warn(line);
|
|
82
|
+
if (line.startsWith(" PASS ")) return ok(line);
|
|
83
|
+
if (line.includes(" fix: ")) return dim(line);
|
|
84
|
+
return line;
|
|
85
|
+
})
|
|
86
|
+
.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
export async function doctorCommand(ctx: CommandContext): Promise<void> {
|
|
73
90
|
// Help first: parsing stops before any probe, config read, or side effect.
|
|
74
91
|
if (ctx.argv[1] === "--help" || ctx.argv[1] === "-h") {
|
|
@@ -96,7 +113,7 @@ export async function doctorCommand(ctx: CommandContext): Promise<void> {
|
|
|
96
113
|
probeTelegram: ctx.argv.includes("--probe-telegram"),
|
|
97
114
|
});
|
|
98
115
|
if (ctx.argv.includes("--json")) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
99
|
-
else process.stdout.write(renderDoctorReport(report));
|
|
116
|
+
else process.stdout.write(styleDoctorReport(report, renderDoctorReport(report)));
|
|
100
117
|
// The one contract CI reads: nonzero iff something failed. Warnings pass.
|
|
101
118
|
if (report.status === "fail") process.exitCode = 1;
|
|
102
119
|
}
|
package/src/commands/hold.ts
CHANGED
|
@@ -8,19 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
import type { CommandContext } from "./context.ts";
|
|
10
10
|
import { hold } from "../fleet.ts";
|
|
11
|
+
import { dim, ok } from "../ui/style.ts";
|
|
11
12
|
|
|
12
13
|
export async function holdCommand(ctx: CommandContext): Promise<void> {
|
|
13
14
|
const keepTicks = ctx.argv.includes("--keep-ticks");
|
|
14
15
|
for (const project of ctx.targetProjects()) {
|
|
15
16
|
const r = hold(project.name, "hold", keepTicks ? { keepTicks: true } : {});
|
|
16
17
|
process.stdout.write(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
ok(
|
|
19
|
+
`held — claiming paused` +
|
|
20
|
+
`${r.wasPaused ? " (already paused)" : ""}` +
|
|
21
|
+
(r.disarmed === undefined
|
|
22
|
+
? "; ticks left armed — resume restores the fleet with no new arm challenge"
|
|
23
|
+
: `; ticks disarmed at ${r.disarmed.path}` +
|
|
24
|
+
`${r.disarmed.wasArmed ? "" : " (was already disarmed)"}`),
|
|
25
|
+
) + `\n${dim("daemon and pane left running; stop to stop the daemon too")}\n`,
|
|
24
26
|
);
|
|
25
27
|
}
|
|
26
28
|
}
|
package/src/commands/ledger.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { CommandContext } from "./context.ts";
|
|
|
10
10
|
import { findProject, loadConfig } from "../config.ts";
|
|
11
11
|
import { dbPath, openStore } from "../store.ts";
|
|
12
12
|
import { formatVerbLedgerEntry } from "../verbs/ledger.ts";
|
|
13
|
+
import { dim, heading } from "../ui/style.ts";
|
|
13
14
|
|
|
14
15
|
export async function ledgerCommand(ctx: CommandContext): Promise<void> {
|
|
15
16
|
const cfg = loadConfig();
|
|
@@ -35,10 +36,28 @@ try {
|
|
|
35
36
|
...(issue === undefined ? {} : { issue }),
|
|
36
37
|
limit,
|
|
37
38
|
});
|
|
39
|
+
if (ctx.argv.includes("--json")) {
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
`${JSON.stringify(
|
|
42
|
+
{
|
|
43
|
+
project: p.name,
|
|
44
|
+
...(issue === undefined ? {} : { issue }),
|
|
45
|
+
entries,
|
|
46
|
+
refused: entries.filter((entry) => entry.decision === "refused").length,
|
|
47
|
+
turnOverrides: overrides,
|
|
48
|
+
},
|
|
49
|
+
null,
|
|
50
|
+
2,
|
|
51
|
+
)}\n`,
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
38
55
|
if (entries.length === 0 && overrides.length === 0) {
|
|
39
56
|
process.stdout.write(
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
dim(
|
|
58
|
+
`no conductor actions recorded for ${p.name}` +
|
|
59
|
+
`${issue === undefined ? "" : ` on #${String(issue)}`}`,
|
|
60
|
+
) + "\n",
|
|
42
61
|
);
|
|
43
62
|
return;
|
|
44
63
|
}
|
|
@@ -46,13 +65,15 @@ try {
|
|
|
46
65
|
if (entries.length > 0) {
|
|
47
66
|
const refused = entries.filter((e) => e.decision === "refused").length;
|
|
48
67
|
blocks.push(
|
|
49
|
-
`${p.name} — ${entries.length} verb call(s), ${refused} refused (newest first)
|
|
68
|
+
heading(`${p.name} — ${entries.length} verb call(s), ${refused} refused (newest first)`) +
|
|
69
|
+
"\n" +
|
|
50
70
|
entries.flatMap(formatVerbLedgerEntry).join("\n"),
|
|
51
71
|
);
|
|
52
72
|
}
|
|
53
73
|
if (overrides.length > 0) {
|
|
54
74
|
blocks.push(
|
|
55
|
-
`${p.name} — ${overrides.length} turn override(s) (newest first)
|
|
75
|
+
heading(`${p.name} — ${overrides.length} turn override(s) (newest first)`) +
|
|
76
|
+
"\n" +
|
|
56
77
|
overrides
|
|
57
78
|
.map(
|
|
58
79
|
(entry) =>
|