omp-conductor 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,20 +31,24 @@ needs a human. It never edits product code and never pushes a branch; whether it
31
31
  may merge or release is a setup answer (`authority`), and both default to no. Only
32
32
  tier 2 pages you directly.
33
33
 
34
- The package ships three deployables, plus two skills:
34
+ The package ships two deployables:
35
35
 
36
36
  | Deployable | Entry | What it is for |
37
37
  | --- | --- | --- |
38
- | omp plugin | `/conductor` slash command | Inspect and control the fleet from inside an omp session: status, hold/halt, arm/disarm, pause/resume, setup. |
39
- | Standalone daemon | `omp-conductor` binary | The dispatch loop, managed as a background process (`start` / `stop` / `restart`) with a `/healthz` endpoint for a supervisor. |
38
+ | Everything an operator does | `omp-conductor` binary | The only operator surface: setup, inspect, control, and the dispatch loop itself as a background process (`start` / `stop` / `restart`) with a `/healthz` endpoint for a supervisor. |
40
39
  | Orchestrator heartbeat | omp extension, activated by `.conductor-tick.json` | Prompts a 24/7 orchestrator session on a fixed interval so its standing loop actually runs, and marks the session stalled when its prompts stop being consumed. Inert in every other session — including a second session opened in the fleet's own directory. See [Orchestrator tick](#orchestrator-tick). |
41
- | Onboarding skill | `skill://conductor-onboarding` | Directs an omp session to interview you, read your repos for real CI gates, and tailor `ORCHESTRATOR.md` — then finish through the wizard. Discovered automatically once the plugin is installed. See [Onboarding](#onboarding). |
42
- | Update skill | `skill://conductor-update` | Thin natural-language wrapper over the deterministic `omp-conductor upgrade` command. See [Updating](#updating). |
43
40
 
44
- The first two are thin wrappers over the same `daemon.ts`, so the plugin and the
45
- CLI cannot disagree about what a cap means or where the state lives. Claiming is
46
- gated by the pause flag; tick sends are gated by the arm marker they are not
47
- the same switch. Prefer `hold` when you want both quiet.
41
+ **There is no slash command and no skill.** Earlier releases shipped a
42
+ `/conductor` command and a `skill://conductor-onboarding`; both are gone. The
43
+ interview they wrapped now lives in the binary as `omp-conductor setup`, which is
44
+ the documented path for every operator and agent see [Onboarding](#onboarding).
45
+ An omp chat session that wants conductor state or wants to change it **shells out
46
+ to `omp-conductor`**, which is why the verb ledger records who asked: a session
47
+ that ran a verb is indistinguishable from an operator who ran the same verb, and
48
+ that is the point.
49
+
50
+ Claiming is gated by the pause flag; tick sends are gated by the arm marker — they
51
+ are not the same switch. Prefer `hold` when you want both quiet.
48
52
 
49
53
  ## Your workflow vs. the package
50
54
 
@@ -60,7 +64,7 @@ once, on request, and then never reads it back, never rewrites it and never
60
64
  enforces a word of it. What you write there binds your orchestrator session and
61
65
  nothing else in this package.
62
66
 
63
- `/conductor setup` offers to render the shipped template
67
+ `omp-conductor setup` offers to render the shipped template
64
68
  (`src/briefs/orchestrator.md`) to `<workspaceRoot>/ORCHESTRATOR.md` with your
65
69
  project's coordinates filled in, and never replaces an existing file without a
66
70
  second, explicit confirmation. It is a starting point rather than a contract:
@@ -88,7 +92,7 @@ with the prompt instead of only in a brief the session read hours ago. Explicit
88
92
  policies name their actual interrupt categories and digest cadence. The policy is
89
93
  re-read from `~/.omp/conductor/config.json` on **every** tick, and escalation,
90
94
  direct Telegram, and durable report paths apply it mechanically. Turning the
91
- volume up or down — `/conductor setup` again, or an edit to the file — therefore
95
+ volume up or down — `omp-conductor setup` again, or an edit to the file — therefore
92
96
  binds the next tick without restarting the session.
93
97
 
94
98
  No config, an unreadable or invalid config, or several unnamed projects fall
@@ -114,7 +118,7 @@ backend would swap in.
114
118
 
115
119
  You tell the conductor where to look with three keys, all in
116
120
  `~/.omp/conductor/config.json` (the [Configuration](#configuration) section has
117
- the full annotated example, and `/conductor setup` will interview you for these
121
+ the full annotated example, and `omp-conductor setup` will interview you for these
118
122
  and create any missing labels):
119
123
 
120
124
  | Key | Meaning |
@@ -132,6 +136,10 @@ number of code repos, and both label names are yours to configure.
132
136
  omp plugin install omp-conductor
133
137
  ```
134
138
 
139
+ That installs the `omp-conductor` binary and the orchestrator heartbeat extension.
140
+ It registers **no slash command and no skills** — everything an operator does is a
141
+ verb on the binary, starting with `omp-conductor setup`.
142
+
135
143
  From a checkout of the monorepo, `./setup.sh` checks both plugins. It preserves
136
144
  an existing npm-managed `omp-conductor` and links only the Herdr half, so running
137
145
  setup on a release-based fleet cannot silently switch omp to mutable source.
@@ -261,60 +269,89 @@ The command never publishes npm, edits an install root, or delegates lifecycle
261
269
  steps to an AI session. It refuses to run inside a Herdr-managed session because
262
270
  an updater that restarts itself cannot verify the result.
263
271
 
264
- The bundled `skill://conductor-update` remains available as a natural-language
265
- entry point, but it only runs this command; the lifecycle is implemented and
266
- verified by the CLI.
267
-
268
272
  ## Onboarding
269
273
 
270
- Onboarding this package has two layers, and installing it gives you both.
274
+ `omp-conductor setup` is the whole of it. One command, in a plain terminal, doing
275
+ the two jobs onboarding has always had:
271
276
 
272
- | Layer | What it is | What it owns |
273
- | --- | --- | --- |
274
- | **`/conductor setup`** | The deterministic wizard. It shows the label, runtime, and dry-run plans before one consent step. A configured project can amend one area. | **Mechanical setup.** It writes `config.json`, labels, the brief, an external heartbeat config, and a staged systemd unit. It also completes the paused smoke and arm gates. |
275
- | **`skill://conductor-onboarding`** | A skill bundled in this package (`skills/conductor-onboarding/SKILL.md`). Any omp session can discover it after plugin installation. | **Brief authoring.** It supplies the judgment that fixed prompts cannot. |
277
+ | Half | What it does |
278
+ | --- | --- |
279
+ | **The interview** | Asks the judgment no amount of repo reading produces, then writes it into `POLICY.md` as prose you own and can edit. |
280
+ | **The probes** | Reads your repos and *proposes* the rest real CI gates, the project context, the release procedure each a default you edit or a draft you confirm. |
276
281
 
277
- The split exists because the two halves fail differently. A wrong config value is
282
+ The split matters because the two halves fail differently. A wrong config value is
278
283
  a run that errors on the next tick; a wrong release boundary is a fleet that
279
- publishes something at 03:00. The first is worth a text prompt with validation.
280
- The second is worth an interview.
281
-
282
- So the skill does the part a dialog cannot:
283
-
284
- - **Interviews you** on release policy humans release (the default), the agent
285
- releases to a named boundary, or the agent releases fully pressing on the one
286
- question that makes a delegated release safe: *where does the agent's leg end?*
287
- The answer becomes the [`authority`](#configuration) pair `/conductor setup`
288
- records, and the brief is worded from it. Plus escalation taste, and which of
289
- the three [`reporting.scope`](#your-workflow-vs-the-package)
290
- values your answer actually maps to.
291
- - **Reads your repos instead of asking about them.** It opens each routing repo's
292
- CI workflows, `package.json` scripts and `Makefile`/`justfile`, then *proposes*
293
- the exact pre-push [gates](#configuration) with the `cwd` each runs from, so the
294
- gates match what CI runs.
295
- - **Tailors `ORCHESTRATOR.md`** from the shipped template. The template is the
296
- floor: it rewrites the Releases and Reporting sections from your answers, adds
297
- the hard boundaries only you know about (infra directories, off-limits repos),
298
- leaves the fixed sections alone, and shows you the diff before writing.
299
- - **Verifies the worker brief's assumptions** against reality: default branch per
300
- repo, whether the branch names the conductor cuts survive your branch
301
- protection, whether each proposed gate exists and exits 0 on a clean checkout,
302
- and whether `pull_request` actually fires a workflow that never triggers on a
303
- PR gives a worker no checks to watch and no verdict to reach.
304
- - **Then finishes through the wizard**, so the dry run and the consent gate still
305
- do the writing.
306
-
307
- From an omp session with the plugin installed, just say what you want: "help me
308
- set up conductor", "onboard me", "configure the fleet" all reach it, because that
309
- is what the skill's description matches on. With `skills.enableSkillCommands`
310
- turned on you can also invoke it directly:
284
+ publishes something at 03:00. The first is worth a validated prompt. The second is
285
+ worth being asked properly, which is why it is asked and never guessed.
286
+
287
+ ### What only you can answer
288
+
289
+ Always asked: **where the roadmap lives and what the current priority is.** A
290
+ tracker shows what is *open*, never what *matters*, and an orchestrator that cannot
291
+ rank work grooms by recency which is how a stale issue outranks the thing you are
292
+ shipping this month.
293
+
294
+ Asked only when you grant the orchestrator a release shape, because a
295
+ humans-release fleet has no boundary to draw:
296
+
297
+ - **Where the orchestrator's leg ENDS**, in one sentence. If it cannot be said in
298
+ one sentence it is not a boundary, and a vague release mandate is what eventually
299
+ publishes something at 03:00.
300
+ - **What** may be released and from which branch; **when** batched how, after
301
+ which *named* checks; **what proof** must be held first, results actually read
302
+ rather than an impression; **what must still be asked** every time; and **what
303
+ stays permanently forbidden**.
304
+ - **What makes a release worth cutting** a sprint, an epic's children all closed,
305
+ N merged issues. Without it the orchestrator either releases per merge, a stream
306
+ of meaningless versions burning shared runners, or never releases at all.
307
+ - **Who owns the rollback.** Name a person and setup says so plainly: that person
308
+ already owns the release, so the honest configuration ends the agent's leg
309
+ *before* the irreversible step. It offers to move the boundary there; declining
310
+ is a choice, not a mistake.
311
+
312
+ Grant every release shape and setup pushes back once credentials sitting in the
313
+ environment of a session that runs unattended for weeks, and a 03:00 rollback being
314
+ a judgement call under time pressure with partial information — then records what
315
+ you decide. It is your fleet.
316
+
317
+ ### What setup reads for you
318
+
319
+ Rather than asking about your repos, setup reads them. Each probe is a short,
320
+ confined session in a throwaway shallow clone, and **every answer is a proposal**:
321
+
322
+ - **Gates.** Reads each routing repo's CI workflows, `package.json` scripts and
323
+ `Makefile`/`justfile`, then pre-fills the [gates](#configuration) prompt with the
324
+ exact commands and the `cwd` each runs from, so your gates match what CI runs. It
325
+ reports the evidence it used, and an honest "this repo has no cheap local check"
326
+ is a real answer rather than an invented `npm test`.
327
+ - **Project context** and **the release procedure.** Drafted across *every* routing
328
+ repo — which repo owns which concern, which ship together, where the release
329
+ machinery actually lives — then shown to you in full and kept **only if you
330
+ confirm**. `POLICY.md` is re-read on every tick, so a paragraph you never read
331
+ would become an instruction the orchestrator follows all week.
332
+
333
+ A probe has **no shell, no editor and no verbs**: it reads files and answers, and a
334
+ tool it was not given is refused rather than allowed. It is not a sandbox — it runs
335
+ as your own user and reads what you can read — which is why it is pointed only at
336
+ repos you configured yourself.
337
+
338
+ **Setup never fails because a probe did.** No omp peer on the host, a repo it cannot
339
+ clone, a cap reached, prose where JSON was asked for: each costs one warning and
340
+ leaves the shipped stub, which says what belongs there and how to fill it in. Every
341
+ question is still asked.
342
+
343
+ Skip the reading half entirely with `--no-ai`:
311
344
 
312
- ```text
313
- /skill:conductor-onboarding
345
+ ```bash
346
+ omp-conductor setup --no-ai
314
347
  ```
315
348
 
316
- Nothing about the wizard changes: `/conductor setup` on its own remains a
317
- complete, supported path, and the brief it renders is safe unedited.
349
+ To fill in or revise just the brief later the two `POLICY.md` sections above run
350
+ the `brief` area, which re-asks the judgment questions and re-runs the probes:
351
+
352
+ ```bash
353
+ omp-conductor setup brief
354
+ ```
318
355
 
319
356
  ### Changing one setting
320
357
 
@@ -329,6 +366,8 @@ opens with one question:
329
366
  asks one area's questions; every other answer is carried through from the saved config
330
367
  Walk every question again
331
368
  the full interview, every prompt pre-filled with what is configured now
369
+ Add another project
370
+ full interview for a new project; existing projects stay as they are
332
371
  ```
333
372
 
334
373
  Amending is the default. Pick it and the eight areas are listed with what each one
@@ -362,13 +401,45 @@ amending code graph — project platform
362
401
 
363
402
  A first run, or a project name this config has never seen, never sees either
364
403
  question: there is nothing to amend, so it is the full interview exactly as
365
- before. Choosing *Walk every question again* is also unchanged every prompt
404
+ before. Choosing *Walk every question again* asks once to confirm the replace
405
+ (so a silent overwrite cannot happen from muscle-memory Enter) — every prompt
366
406
  pre-filled with what is configured, Enter to keep it — with one wrinkle worth
367
407
  knowing: the two authority confirms and the orchestrator-session confirm cannot
368
408
  start on "yes", so Entering through the full interview **revokes** a delegation
369
409
  rather than renewing it. Amending the `authority` area names the current grant in
370
410
  the question, which is the safer way to leave one alone.
371
411
 
412
+ ### Adding another project
413
+
414
+ One daemon serves every configured project. To put a second (or third) fleet on
415
+ the same host without touching the first:
416
+
417
+ ```bash
418
+ omp-conductor setup --project second
419
+ # or pick "Add another project" from the re-run chooser
420
+ ```
421
+
422
+ That is a full interview for the new name only. Defaults land under
423
+ `~/.omp/conductor/projects/<name>/{worktrees,mirrors}` so two fleets never share
424
+ a cwd; the first project's existing flat `worktrees`/`mirrors` paths are never
425
+ migrated. A `workspaceRoot` that collides with another project is refused with
426
+ both names in the error. Re-using an existing name asks amend-or-replace before
427
+ anything is written.
428
+
429
+ After apply, setup provisions labels, brief, tick config (with `project` +
430
+ `agentName`), topic binding, smoke, and arm for the new project only, then prints:
431
+
432
+ - `omp-conductor restart --now` — the running daemon picks up the new project
433
+ only after reload (printed, not auto-run when workers are live)
434
+ - a copy-pasteable **herdr handoff**: `herdr --session conductor workspace create
435
+ --cwd <workspaceRoot> --label <project> --no-focus`, then `herdr --session
436
+ conductor agent start <project> --kind omp --pane <pane-id>` into that empty
437
+ pane (never into a live orchestrator), plus the `FLEET_CWDS` list for
438
+ multi-fleet recovery
439
+
440
+ `omp-conductor setup gates --project second` (and every other area) still amends
441
+ only that project.
442
+
372
443
  ### Keeping a brief current
373
444
 
374
445
  The standing prompt is two layers:
@@ -379,7 +450,6 @@ The standing prompt is two layers:
379
450
  | Fleet policy | `POLICY.md` | Yours. Setup writes the scaffold once; the Learning loop edits only this file. |
380
451
  | Composed view | `ORCHESTRATOR.md` | Regenerated from floor + `POLICY.md` on each tick (and at setup). Do not hand-amend it for durable policy. |
381
452
  | Worker brief | `src/briefs/worker.md` | Read per run from the package. |
382
- | Onboarding skill | `skills/conductor-onboarding/SKILL.md` | Read from the installed package. |
383
453
 
384
454
  ```bash
385
455
  omp-conductor brief-upgrade # report overlay / legacy state
@@ -436,7 +506,7 @@ time Telegram approval, `POLICY.md`-only edit, Hard-boundary prohibition, and
436
506
  3. Run the wizard:
437
507
 
438
508
  ```text
439
- /conductor setup
509
+ omp-conductor setup
440
510
  ```
441
511
 
442
512
  The wizard reads the tracker with the same routing code as the daemon. It shows every issue that the next tick can route.
@@ -506,18 +576,19 @@ The package also ships a generic unit at
506
576
  [`systemd/omp-conductor.service.example`](systemd/omp-conductor.service.example).
507
577
 
508
578
 
509
- ### Stop the conductor (hold / halt)
579
+ ### Stop the conductor (hold / stop)
510
580
 
511
- Four control planes used to answer "stop" differently. The package verbs:
581
+ Two words, and one of them takes a flag:
512
582
 
513
583
  | Verb | Claiming | Tick sends | Dispatch daemon | Conductor pane |
514
584
  | --- | --- | --- | --- | --- |
515
585
  | `hold` | paused | disarmed | left running | left running |
516
- | `halt` | paused | disarmed | stopped (systemctl-aware) | left running |
517
- | `halt --pane` | paused | disarmed | stopped | stopped + recovery pinned off |
518
- | `pause` | paused | **still armed** | left running | left running |
586
+ | `stop` | paused | disarmed | stopped (systemctl-aware) | left running |
587
+ | `stop --pane` | paused | disarmed | stopped | stopped + recovery pinned off |
588
+
589
+ `resume` clears pause **and** any `stop --pane` recovery pin, and **never re-arms**. `arm` is proof-gated: it sends a Telegram challenge and writes the arm marker only after your reply appears as a *user* turn in the orchestrator transcript. `stop --pane` targets the configured conductor agent only — it does **not** run `systemctl stop herdr-fleet`. To bounce the daemon without stopping the fleet, use `restart`.
519
590
 
520
- `resume` clears pause only and **never re-arms**. `arm` is proof-gated: it sends a Telegram challenge and writes the arm marker only after your reply appears as a *user* turn in the orchestrator transcript. `halt --pane` targets the configured conductor agent only it does **not** run `systemctl stop herdr-fleet`.
591
+ **Removed in 0.15.0**, each exiting `2` with a pointer: `halt` (now `stop`), `pause` (use `hold`), `release-pane` (now part of `resume`), and `graph-setup` (now [`setup graph`](#code-graph-discovery)). Dropping `pause` means the CLI no longer offers "stop claiming but keep ticking" — the state still exists internally (`upgrade` passes through it while draining), but as an operator verb the model is now three words instead of five.
521
592
 
522
593
  `status` prints a layered header (`dispatch` / `ticks` / next tick time / `pane` / `recovery` / `herdr` / `telegram` / `daemon`) so a paused fleet cannot hide an armed orchestrator still spending turns. The Telegram line calls the official `getMe` endpoint to prove the token and API are usable without sending a message, then separately reports whether the inbound bridge is configured.
523
594
 
@@ -580,7 +651,7 @@ message on stderr) on every uncertainty:
580
651
  - no tick config exists at all — `recover.sh` reads only
581
652
  `$FLEET_CWD/.conductor-pane-halted`, and without that file the pane's own
582
653
  directory is unknown, so the pin would land somewhere recovery never looks and
583
- the agent would be respawned seconds later. `release-pane` refuses for the
654
+ the agent would be respawned seconds later. `resume` refuses for the
584
655
  same reason, and `status` shows `recovery unpinnable` rather than `clear`
585
656
  - the tick config does not parse — the agent name would be a guess
586
657
  - `herdr agent list` is unreachable, prints nothing, or prints output with no
@@ -600,7 +671,7 @@ The pin is written to the pane's own directory (the one holding
600
671
  `.conductor-tick.json`, which is `FLEET_CWD` — the only place `recover.sh` looks),
601
672
  including when that tick config is the thing that failed to parse.
602
673
 
603
- Clear the pin with `omp-conductor release-pane` when you want recovery again.
674
+ Clear the pin with `omp-conductor resume` when you want recovery again.
604
675
 
605
676
 
606
677
  ## How one tick works
@@ -619,7 +690,7 @@ Per tick, for the daemon's project:
619
690
  [what settles a green PR](#what-settles-a-green-pr).
620
691
  2. **Paused?** If the pause sentinel exists, the tick claims nothing and returns.
621
692
  Settlement has already run, but no queue or admission work occurs. This makes
622
- `omp-conductor pause` take effect without signalling the process.
693
+ `omp-conductor hold` take effect without signalling the process.
623
694
  3. **List the queue.** Open issues in `tracker.repo` labelled `queueLabel`.
624
695
  4. **Filter and route.** An issue is eligible only if it carries the queue label
625
696
  and none of the three state labels (`inProgress`, `blocked`, `failed`). Eligible
@@ -977,6 +1048,11 @@ inside the service's cgroup, so systemd's Memory peak for
977
1048
  MCP stdio children those sessions mount. `MemoryMax=` governs that whole total,
978
1049
  not one process.
979
1050
 
1051
+ The generated unit starts `omp-conductor daemon --port 8787` without a
1052
+ `--project` filter, so one daemon serves every configured project. Its automatic
1053
+ `MemoryMax=` tier uses the sum of resolved `maxConcurrentWorkers` values across
1054
+ all projects: `3G` for a total of one worker, otherwise `5G`.
1055
+
980
1056
  On the reference deploy that produced [issue #51](https://github.com/TerrifiedBug/conductor/issues/51):
981
1057
 
982
1058
  | Shape | Observed |
@@ -995,12 +1071,13 @@ measured in the hundreds of thousands of characters of tool output.
995
1071
  - Prefer **≥16 GiB RAM** for the default `maxConcurrentWorkers: 2`, and do **not**
996
1072
  co-locate ClickHouse / other multi-GB services beside that fleet on an ≤8 GiB
997
1073
  box.
998
- - On hosts under ~16 GiB, set `maxConcurrentWorkers` to **1**. `/conductor setup`
999
- does this automatically when it can read host RAM.
1000
- - Supervise the daemon with a unit that sets `SuccessExitStatus=0 143` and a
1001
- `MemoryMax=` just above your expected peak. A ready-to-edit example ships as
1002
- [`systemd/omp-conductor.service.example`](systemd/omp-conductor.service.example)
1003
- (`MemoryMax=5G` for the two-worker shape).
1074
+ - On hosts under ~16 GiB, keep the **sum** of every project's
1075
+ `maxConcurrentWorkers` at **1**. `omp-conductor setup` chooses that default
1076
+ for a new project when it can read host RAM and warns before applying a
1077
+ configuration whose combined capacity exceeds the host recommendation.
1078
+ - Supervise the daemon with a unit that sets `SuccessExitStatus=0 143`; setup
1079
+ renders `MemoryMax=3G` for one configured worker and `MemoryMax=5G` for two
1080
+ or more.
1004
1081
  - `omp-conductor status` prints daemon `rss` from `/healthz` when the process is
1005
1082
  up, so you can see pressure without scraping journald.
1006
1083
 
@@ -1028,7 +1105,7 @@ Set `dailySpendUsd` to `null` (wizard: blank) for no money gate — turns and wa
1028
1105
  limit simply defers work to a later tick. The spend cap **pauses the daemon and
1029
1106
  pages at Tier 2**: a loop that is burning money has to halt itself, because
1030
1107
  waiting for someone to notice tomorrow is how a runaway becomes expensive.
1031
- Work resumes only after `omp-conductor resume` (or `/conductor resume`).
1108
+ Work resumes only after `omp-conductor resume`.
1032
1109
 
1033
1110
  `workerMaxTurns` and `workerWallClockMs` are enforced inside the session driver.
1034
1111
  The daemon reads a live run's effective turn ceiling at every turn boundary. Use
@@ -1048,7 +1125,7 @@ escalation.
1048
1125
  Pause one live worker cooperatively with
1049
1126
  `omp-conductor worker pause <issue> [--project NAME]`. The daemon aborts the
1050
1127
  active turn to an idle harness state, freezes the remaining wall-clock budget,
1051
- and keeps the run in the Running lane. `omp-conductor worker resume <issue>`
1128
+ and keeps the run in the Running lane. `status` overlays `paused`/`pausing` from `/healthz` on that active-run line while the SQLite row stays `running`. `omp-conductor worker resume <issue>`
1052
1129
  continues the same session with a prompt to re-check its last action before
1053
1130
  proceeding. To end that run instead, use
1054
1131
  `omp-conductor worker stop <issue> --reason TEXT [--project NAME]`. Stop works
@@ -1173,21 +1250,21 @@ defined" in one call instead of twenty greps.
1173
1250
  `omp-conductor` never installs, starts, imports, or depends on the indexer for
1174
1251
  dispatch. With `graphProject` unset, nothing about dispatch, caps, escalation, or
1175
1252
  status changes. A fresh host needs both of these before an index is worth
1176
- anything, and `graph-setup` reports them as step 0:
1253
+ anything, and `setup graph` reports them as step 0:
1177
1254
 
1178
1255
  1. **`codebase-memory-mcp` on PATH** — a separate project,
1179
1256
  [DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp).
1180
1257
  2. **Mounted as an MCP server** in `~/.omp/agent/mcp.json`, on the account the
1181
1258
  daemon runs as. Miss this and the failure is silent: every index builds
1182
1259
  correctly, no worker session can read any of them, so workers fall back to
1183
- grepping and the feature looks like a no-op. `graph-setup` prints the entry.
1260
+ grepping and the feature looks like a no-op. `setup graph` prints the entry.
1184
1261
 
1185
1262
  Say yes and the wizard asks for one root, then derives one clone per routed repo
1186
1263
  underneath it (default `~/.cache/conductor-graph/<org>/<repo>`) and writes it to
1187
1264
  each repo's [`graphProject`](#configuration). The only automatic interaction is
1188
1265
  a bounded, read-only health query; this package never clones, fetches, builds an
1189
1266
  index, or changes systemd. Dispatch, caps and escalation do not depend on graph
1190
- health. On a fleet configured before this key existed, `/conductor setup` and
1267
+ health. On a fleet configured before this key existed, `omp-conductor setup` and
1191
1268
  the `code graph` area add it in two prompts — see
1192
1269
  [Changing one setting](#changing-one-setting).
1193
1270
 
@@ -1212,11 +1289,11 @@ edits — orient with it, then read the real file before changing it.
1212
1289
  ### Creating and refreshing them
1213
1290
 
1214
1291
  ```bash
1215
- omp-conductor graph-setup # print the plan: clones, index commands, units
1216
- omp-conductor graph-setup --write # stage the script and the two units (no root)
1292
+ omp-conductor setup graph --print # print the plan: clones, index commands, units
1293
+ omp-conductor setup graph # run it: clone, install, enable, seed, verify
1217
1294
  ```
1218
1295
 
1219
- `graph-setup` prints a `git clone` for every clone that does not exist yet, the
1296
+ `setup graph --print` prints a `git clone` for every clone that does not exist yet, the
1220
1297
  one-shot index command per repo, and a `cbm-reindex.service` + `cbm-reindex.timer`
1221
1298
  pair built from the project's own repos and branches. `--write` stages all three
1222
1299
  in the state directory and prints the two `sudo` lines that install and enable
@@ -1244,7 +1321,7 @@ Two properties of the generated unit are deliberate:
1244
1321
  The unit spells out `HOME` and an explicit `PATH`, because systemd supplies
1245
1322
  neither usefully: the indexer resolves its store from `HOME`, systemd's default
1246
1323
  `PATH` has no `~/.local/bin`, and the indexer shells out to `git`. Both are the
1247
- user that ran `graph-setup`; the unit sets no `User=`, so check them if that is
1324
+ user that ran `setup graph`; the unit sets no `User=`, so check them if that is
1248
1325
  not the account the timer runs as.
1249
1326
 
1250
1327
  ### Seeing whether the graph is usable
@@ -1520,9 +1597,17 @@ conditions, each one something this package can check without asking you:
1520
1597
  | `rate-limit-reset:github` | GraphQL quota on `github` has any remaining capacity again. |
1521
1598
 
1522
1599
  The daemon evaluates them beside each tick, fire-and-forget: a hanging registry
1523
- costs one unevaluated condition, never the tick. A met row is flagged in the next
1524
- digest as `[CONDITION MET — act on this now]`, so the session acts on the answer
1525
- at the moment it becomes actionable rather than when it happens to remember.
1600
+ costs one unevaluated condition, never the tick. A row that transitions
1601
+ false→true also writes the same `.conductor-tick-requested` poke recover uses,
1602
+ so the orchestrator heartbeat fires promptly (mid-interval poll, still gated by
1603
+ arm/channel/pending single-flight) instead of waiting a full interval. The poke
1604
+ reason and the digest flag `[CONDITION MET — act on this now]` both surface the
1605
+ wake so the session acts when the answer becomes actionable. Repeated sweeps
1606
+ while the condition stays true do nothing further — the store marks the
1607
+ transition once. After a green-but-behind PR is updated through
1608
+ `conductor_pr_update_branch`, open a fresh `pr-checks-green` watch on the new
1609
+ head so the next green transition can wake merge review the same way; nothing
1610
+ here merges on its own.
1526
1611
 
1527
1612
  Anything else exits `2` and lists the six forms. An unparseable condition on an
1528
1613
  existing row is *listed and never treated as met*: a grammar a future release
@@ -1647,7 +1732,22 @@ The file is validated on every read. A malformed config produces one readable er
1647
1732
  listing every fault, and the daemon refuses to start rather than running with half
1648
1733
  a project.
1649
1734
 
1650
- `/conductor setup` is the only thing here that writes this file, and on a project
1735
+ The same vocabulary the loader enforces ships as a JSON Schema at
1736
+ `schema/config.schema.json` in the installed package (draft 2020-12). Anything
1737
+ `saveConfig` writes carries a top-level `"$schema"` reference to that installed
1738
+ copy (resolved from the package's own location, so it points at a real file),
1739
+ which lets an editor that understands JSON Schema validate a hand-edited config
1740
+ as you type; a config without the key is just as valid. Regenerate the shipped
1741
+ schema from `ConfigSchema` (`src/config-schema.ts`) with:
1742
+
1743
+ ```sh
1744
+ bun run schema
1745
+ ```
1746
+
1747
+ and commit the resulting `schema/config.schema.json`. CI's `bun test` fails if the
1748
+ checked-in schema drifts from what the code renders, so you cannot forget the step.
1749
+
1750
+ `omp-conductor setup` is the only thing here that writes this file, and on a project
1651
1751
  it already knows it can rewrite one area of it without re-asking the rest — see
1652
1752
  [Changing one setting](#changing-one-setting).
1653
1753
 
@@ -1713,6 +1813,7 @@ A complete, valid config for one project with two target repos:
1713
1813
  "workerModel": "smol",
1714
1814
  "escalation": {
1715
1815
  "telegramChatId": "123456789",
1816
+ "telegramTopicId": 8713,
1716
1817
  "fallbackToIssueComment": true,
1717
1818
  "orchestrator": "embedded"
1718
1819
  },
@@ -1881,7 +1982,7 @@ policy instead of restating it — no threshold lives in two places.
1881
1982
  | `environments` | any names | `[]` | Deploy targets. **Empty denies** every environment. |
1882
1983
 
1883
1984
  A project with no `policy` block loads as the whole default above, which is the
1884
- strictest reading of the prose it replaced. `/conductor setup` asks for all of
1985
+ strictest reading of the prose it replaced. `omp-conductor setup` asks for all of
1885
1986
  it under the **merge & release preconditions** area, so changing one condition
1886
1987
  costs eight prompts rather than a hand-edit — see
1887
1988
  [Changing one setting](#changing-one-setting).
@@ -1917,13 +2018,14 @@ it never gets prompted, so it never runs anything. Installing
1917
2018
  `omp plugin install omp-conductor` also installs a heartbeat that prompts it.
1918
2019
 
1919
2020
  The heartbeat is **inert unless the session cwd contains
1920
- `.conductor-tick.json`**, so an ordinary session has no timer. `/conductor setup`
2021
+ `.conductor-tick.json`**, so an ordinary session has no timer. `omp-conductor setup`
1921
2022
  writes this file for external orchestration. A manual configuration has this form:
1922
2023
 
1923
2024
  ```json
1924
2025
  {
1925
2026
  "intervalSeconds": 900,
1926
- "armedFile": "state/armed",
2027
+ "project": "fleet",
2028
+ "armedFile": "/home/fleet/.omp/conductor/armed-fleet",
1927
2029
  "accessFile": "/home/fleet/.omp/agent/telegram/access.json",
1928
2030
  "message": "Run your standing loop from ORCHESTRATOR.md now."
1929
2031
  }
@@ -1932,11 +2034,65 @@ writes this file for external orchestration. A manual configuration has this for
1932
2034
  | Key | Required | Default | Notes |
1933
2035
  | --- | --- | --- | --- |
1934
2036
  | `intervalSeconds` | yes | — | Whole seconds between ticks, minimum `60`. A tick costs a full turn of a frontier model, so a sub-minute period is refused rather than obeyed. |
2037
+ | `project` | no | the only configured project | Which conductor project this fleet session ticks for. `setup host` stamps it, one tick config per fleet cwd, and it is what lets a host with several configured projects resolve *this* fleet's brief, reporting policy, digest ledger and release grants. Omitting it is the pre-multi-project spelling: correct on a single-project host, and on a host with two or more it degrades every tick to the default reporting scope with no release grants — `status` and the tick log then name the one fix (`re-run omp-conductor setup host`). A name no configured project has degrades the same way. |
1935
2038
  | `budgetSeconds` | no | `600` | Seconds a turn may run before the tick guard refuses its remaining tool calls (#189), and before a queued operator message preempts them. An integer ≥ 60; anything else degrades to the default. |
1936
- | `armedFile` | no | none — the gate passes | Path to the arm marker. A tick does nothing while the file is missing. Relative paths resolve against the session cwd, so `state/armed` means `<cwd>/state/armed`. |
2039
+ | `armedFile` | no | none — the gate passes | Path to the arm marker. A tick does nothing while the file is missing. Relative paths resolve against the session cwd, so `state/armed` means `<cwd>/state/armed`. `setup host` writes `<state dir>/armed-<project>`, one marker per project, so `arm --project A` cannot arm B. A value it did not generate is left alone as your own choice. |
1937
2040
  | `accessFile` | no | none — the gate passes | Path to the Telegram bridge's `access.json`. Every tick re-reads it and requires `enabled: true` with exactly one entry in `allowFrom`. Relative paths resolve against the session cwd. **Configure this on any fleet deploy** — see below. |
1938
2041
  | `message` | no | `Tick <ISO timestamp>: re-read <workspaceRoot>/ORCHESTRATOR.md from disk, then run your standing loop from it.`, then the reporting-policy line, delivery rule, and mechanical availability state | When set, this text replaces the ordinary reporting-policy line and delivery rule, but the runtime-owned availability state is still appended: a custom prompt cannot infer whether the operator may be interrupted. Re-read from disk on **every** tick, so rewording it binds the next heartbeat instead of waiting for a session restart; a re-read that fails — caught mid-edit, removed, or invalid — keeps the value read at session start rather than stopping the heartbeat. `intervalSeconds` is *not* re-read: rescheduling a live timer still needs a restart. The default *orders* the session to re-read its brief, naming the path resolved from the project's `workspaceRoot`, because a standing prompt drifts out of a long-lived session's context while the file on disk does not. |
1939
- | `agentName` | no | `fleet` | The herdr agent name the orchestrator's pane is registered under. Under herdr this is the whole of the identity check below, and the default matches `AGENT_NAME=${AGENT_NAME:-fleet}` in the recovery plugin's `recover.sh`, so both halves key on one name. Rename the agent and set this to match. |
2042
+ | `agentName` | no | the project name, else `fleet` | The herdr agent name the orchestrator's pane is registered under. Under herdr this is the whole of the identity check below. `setup host` writes the project name, so two fleets in one herdr session are distinguishable; when no tick config names one, the fallback matches `AGENT_NAME=${AGENT_NAME:-fleet}` in the recovery plugin's `recover.sh`, so both halves key on one name. Rename the agent and set this to match. |
2043
+
2044
+ #### Upgrading from one shared arm marker
2045
+
2046
+ Before per-project markers every project was given the same `<state dir>/armed`,
2047
+ so arming one fleet armed all of them. `setup host` rewrites that value — and
2048
+ only that value — to `armed-<project>`. The old bare marker is honoured for one
2049
+ more cycle on a **single-project** host, so the upgrade never silently disarms a
2050
+ live fleet, and the next `arm` or `disarm` retires it. On a host with **two or
2051
+ more** projects it arms nothing: `status` reports `legacy global arm marker —
2052
+ re-run setup host, then arm per project`, and every tick stays disarmed until each
2053
+ project is armed on its own marker.
2054
+
2055
+ The same restamp renames the identity this pane ticks under: an `agentName` of
2056
+ `fleet` — the value every project used to be given — becomes the project name.
2057
+ **Under herdr that is an operator step, not a no-op.** Ownership is proved against
2058
+ the pane's registered herdr agent, so after re-running `setup host` the live fleet
2059
+ pane needs the new name.
2060
+
2061
+ **Rename the agent herdr already detects — do not `agent start`.** `herdr agent
2062
+ start` submits omp *into* the pane's existing shell and requires a pane sitting at
2063
+ a shell prompt with no agent on it; the live orchestrator pane is neither, so it
2064
+ is refused at best and starts a second omp in that pane at worst. `rename` touches
2065
+ no process and keeps the session as it is:
2066
+
2067
+ ```sh
2068
+ herdr --session <session> agent list # find the fleet's pane_id
2069
+ herdr --session <session> agent rename <pane-id> <project>
2070
+ ```
2071
+
2072
+ If the name cannot be reassigned in place, stop and resume rather than starting a
2073
+ second orchestrator — the same shape `recover.sh` uses, so the omp session is
2074
+ preserved rather than replaced:
2075
+
2076
+ ```sh
2077
+ herdr --session <session> agent get <pane-id> # note agent_session.value — the session ref
2078
+ # exit omp in that pane (/exit) so the pane is back at a shell prompt, then:
2079
+ herdr --session <session> agent start <project> --kind omp --pane <pane-id> -- --resume=<ref>
2080
+ ```
2081
+
2082
+ Until the pane carries the new name it declines to tick and logs which agent it
2083
+ actually is versus the one the tick config names, with the `rename` command in the
2084
+ line — the heartbeat fails closed and says so rather than letting two fleets both
2085
+ answer to `fleet`. Set `agentName` explicitly if you would rather keep the old
2086
+ name; a value that is not the shared default is never rewritten.
2087
+
2088
+ Recovery is fail-closed across that window. A restamped `agentName` moves the
2089
+ recovery plugin's own state files to per-agent paths that do not exist yet, and
2090
+ the live pane is still saved under `fleet`, so the snapshot offers no candidate
2091
+ for the new name. `herdr-conductor` treats the pre-rename identity and bootstrap
2092
+ marker as proof a fleet has already lived on this host whatever it is called now:
2093
+ it pages `no fleet identity to recover for agent <project>` instead of
2094
+ provisioning a second workspace beside the live orchestrator. Finish the rename
2095
+ and the next pass recovers normally.
1940
2096
 
1941
2097
  A default tick sends one message (`customType` `omp-conductor.tick`, attributed
1942
2098
  to the user): the standing-loop prompt, the reporting-policy constraint re-read
@@ -1975,9 +2131,11 @@ registered agent name: the heartbeat asks `herdr agent list` for the entry whose
1975
2131
  is the *session* — every pane in it shares `HERDR_SESSION` and the cwd — and
1976
2132
  herdr's `agent` field is the *runtime*, `omp` for the orchestrator and for the
1977
2133
  shell beside it, so neither can tell them apart. The registered name can, it is
1978
- what `herdr agent start fleet --kind omp --pane <id>` sets, and it is the same
1979
- identity the recovery plugin keys on. A pane with a different name, or no name at
1980
- all, stays inert.
2134
+ what `herdr agent start fleet --kind omp --pane <id>` sets when the recovery plugin
2135
+ starts a fleet into an empty pane and what `herdr agent rename <pane-id> fleet`
2136
+ sets on a pane whose agent herdr already detects, which is the only safe spelling
2137
+ while omp is running in it. It is the same identity the recovery plugin keys on. A
2138
+ pane with a different name, or no name at all, stays inert.
1981
2139
 
1982
2140
  **Without herdr**, the session claims the directory in a sibling
1983
2141
  `.conductor-tick-owner.json` (pid, session file, claim time) and ticks only while
@@ -2039,8 +2197,8 @@ that happens to have a `.conductor-tick.json` has no bridge to check. It is not
2039
2197
  off switch for the check: **a fleet deploy always sets it.**
2040
2198
 
2041
2199
  Every tick — sent or skipped — is logged with its reason (`not armed`,
2042
- `escalation channel down`, `tick already pending`) to the omp log. `/conductor
2043
- pause` is deliberately **not** one of the gates: pause stops the *dispatcher*
2200
+ `escalation channel down`, `tick already pending`) to the omp log. `omp-conductor
2201
+ hold` is deliberately **not** one of the gates: hold stops the *dispatcher*
2044
2202
  claiming work, and the tick drives a different session — one whose duties
2045
2203
  (grooming the queue, draining escalations, reporting) are exactly what stays
2046
2204
  useful while dispatch is stopped. Its own off switch is the arm marker. Skips
@@ -2126,6 +2284,9 @@ least of all on a fleet whose session lives somewhere else.
2126
2284
  ## CLI reference
2127
2285
 
2128
2286
  ```bash
2287
+ omp-conductor setup [area] [--no-ai] [--project NAME]
2288
+ omp-conductor setup host [--project NAME]
2289
+ omp-conductor setup graph [--no-seed] [--print] [--project NAME]
2129
2290
  omp-conductor start [--port N] [--project NAME]
2130
2291
  omp-conductor --version
2131
2292
  omp-conductor stop
@@ -2135,10 +2296,9 @@ omp-conductor status [--project NAME]
2135
2296
  omp-conductor ledger [--issue N] [--limit N] [--project NAME]
2136
2297
  omp-conductor board [--project NAME]
2137
2298
  omp-conductor hold [--project NAME]
2138
- omp-conductor halt [--pane] [--project NAME]
2299
+ omp-conductor stop [--pane] [--project NAME]
2139
2300
  omp-conductor arm [--project NAME]
2140
2301
  omp-conductor disarm [--project NAME]
2141
- omp-conductor release-pane [--project NAME]
2142
2302
  omp-conductor tail <issue> [--project NAME]
2143
2303
  omp-conductor extend <issue> --turns N [--project NAME]
2144
2304
  omp-conductor worker pause <issue> [--project NAME]
@@ -2154,30 +2314,30 @@ omp-conductor decision resolve <id> --answer TEXT [--project NAME]
2154
2314
  omp-conductor decision withdraw <id> [--reason TEXT] [--project NAME]
2155
2315
  omp-conductor decision list [--project NAME]
2156
2316
  omp-conductor daemon [--once] [--port N] [--project NAME]
2157
- omp-conductor pause [--reason TEXT]
2158
- omp-conductor resume
2159
- omp-conductor graph-setup [--project NAME] [--write]
2317
+ omp-conductor resume [--project NAME]
2160
2318
  omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
2161
2319
  omp-conductor help
2162
2320
  ```
2163
2321
 
2164
2322
  | Command | Behaviour |
2165
2323
  | --- | --- |
2324
+ | `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. |
2325
+ | `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`. 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. |
2326
+ | `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). |
2166
2327
  | `start` | Start `herdr-fleet.service` when that optional unit is installed, clearing a previous pane-recovery pin, then spawn the dispatch loop in the background and wait until it answers `GET /healthz` on `:8787`. Without systemd or that unit it keeps the standalone daemon behaviour. It never clears pause or arms ticks. Refuses if a daemon is already live, naming its pid; if the process dies or never serves, it cleans up and quotes the tail of `daemon.log`. |
2167
2328
  | `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. |
2168
2329
  | `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. Prefer `systemctl restart` when the unit owns the live pid so the replacement stays supervised; otherwise `stop` then `start`, inheriting the running daemon's port and project unless a flag overrides them. `--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). |
2169
2330
  | `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. A no-op when already current. Failure leaves dispatch paused. Must run outside a Herdr-managed session. |
2170
- | `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. 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. |
2331
+ | `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. |
2171
2332
  | `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`. |
2172
2333
  | `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. |
2173
- | `hold [--project NAME]` | Soft stop: pause claiming **and** disarm ticks. Daemon and pane stay up. Prefer this over `pause` when the intent is "stop the conductor" without killing processes. See [Stop the conductor](#stop-the-conductor-hold--halt). |
2174
- | `halt [--pane] [--project NAME]` | `hold`, then stop the dispatch daemon (systemctl-aware). Pane stays up unless `--pane` is passed. `halt --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. |
2175
- | `arm [--project NAME]` | Proof-gated: send a Telegram challenge and write the arm marker only after your reply appears as a user turn in the orchestrator transcript. Never auto-armed by `resume` / `hold`. |
2176
- | `disarm [--project NAME]` | Remove the arm marker so ticks skip. Processes untouched. |
2177
- | `release-pane [--project NAME]` | Clear the `halt --pane` recovery pin so herdr-conductor may resume the fleet agent again. |
2334
+ | `hold [--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. See [Stop the conductor](#stop-the-conductor-hold--stop). |
2335
+ | `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`. |
2336
+ | `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`. |
2337
+ | `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. |
2178
2338
  | `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>`. |
2179
2339
  | `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. |
2180
- | `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 `pause`, which refuses new claims and work-starting mutations while allowing pre-pause completion work and releases. |
2340
+ | `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. |
2181
2341
  | `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. |
2182
2342
  | `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](#the-tick): 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. |
2183
2343
  | `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`. |
@@ -2190,12 +2350,10 @@ omp-conductor help
2190
2350
  | `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. |
2191
2351
  | `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. |
2192
2352
  | `--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. |
2193
- | `--project NAME` | Pick the project to service. One daemon process serves exactly one project; with several configured projects the name is required. |
2353
+ | `--project NAME` | Optional filter for `start`, `restart`, and `daemon`. Omit it to serve every configured project from one process (the unit path). Pass it only when you deliberately want a single-project daemon. |
2194
2354
  | `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. |
2195
- | `resume` | Clear pause only — does **not** re-arm. Run `arm` after an inbound Telegram proof to resume ticks. |
2355
+ | `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. |
2196
2356
  | `--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. |
2197
- | `graph-setup` | Print how to set up the code-graph indexes workers query instead of grepping: a `git clone` for every index-only clone that does not exist yet, the one-shot index command per repo, and a `cbm-reindex.service` + `cbm-reindex.timer` pair generated from the project's own repos and branches. Reads only, so it is safe on a host where you are not root. Exits `1` when no repo in the project has [`graphProject`](#configuration) set, because the fix is a wizard answer rather than a flag. See [Code-graph discovery](#code-graph-discovery). |
2198
- | `--write` | Only for `graph-setup`. Writes the refresh script into the state directory and the two units into `/etc/systemd/system`, then prints the exact `systemctl daemon-reload && systemctl enable --now cbm-reindex.timer` to run. It never runs `systemctl` itself and never enables anything: that needs root, and a package that enables system timers behind your back is one you cannot audit by reading its output. |
2199
2357
  | `brief-upgrade` | Inspect the package-floor + `POLICY.md` overlay. Reports by default; see [Keeping a brief current](#keeping-a-brief-current). |
2200
2358
  | `--migrate` | Only for `brief-upgrade`. Lift a bannered `ORCHESTRATOR.md` owned half into `POLICY.md` and recompose. Dry-run unless `--apply`. |
2201
2359
  | `--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. |
@@ -2203,18 +2361,18 @@ omp-conductor help
2203
2361
  | `--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. |
2204
2362
  | `help`, `--help`, `-h` | Print usage. An unknown or missing verb prints it too, and exits `2`. |
2205
2363
 
2206
- Pause is a flag file under the state directory, so it applies to every project and
2207
- survives a daemon restart. It refuses new claims and work-starting mutations,
2208
- allows completion verbs only for runs admitted before the pause, and leaves
2209
- `conductor_release` to its normal authority, grant, and precondition checks.
2210
- Per-worker pause is independent. Hold also removes the arm marker the heartbeat
2211
- reads, so both brains go quiet without killing processes.
2364
+ Pause is a sentinel under the state directory and survives a daemon restart.
2365
+ `hold --project NAME` writes `paused-<name>` for that project only; a bare
2366
+ `paused` file (legacy / all-projects) pauses every project. It refuses new claims
2367
+ and work-starting mutations, allows completion verbs only for runs admitted
2368
+ before the pause, and leaves `conductor_release` to its normal authority, grant,
2369
+ and precondition checks. Per-worker pause is independent. Hold also removes the
2370
+ arm marker the heartbeat reads, so both brains go quiet without killing processes.
2212
2371
 
2213
- These are available in-session as `/conductor setup`, `/conductor status`,
2214
- `/conductor hold`, `/conductor halt [--pane]`, `/conductor arm`, `/conductor disarm`,
2215
- `/conductor release-pane`, `/conductor pause` and `/conductor resume`, each taking
2216
- an optional project name. Background-process management (`start` / `stop` /
2217
- `restart`) is CLI-only: the plugin does not start, stop or restart the daemon.
2372
+ Every one of these is a verb on the `omp-conductor` binary, each taking an optional
2373
+ `--project NAME`. There is no in-session command: an omp session that wants any of
2374
+ them shells out to the binary, which is what keeps one implementation and one ledger
2375
+ entry per action.
2218
2376
 
2219
2377
  ### Health endpoint
2220
2378
 
@@ -2225,50 +2383,58 @@ curl -s localhost:8787/healthz
2225
2383
  ```json
2226
2384
  {
2227
2385
  "ok": true,
2228
- "paused": false,
2229
- "activeRuns": 1,
2230
- "project": "demo",
2231
2386
  "rssBytes": 123456789,
2232
- "dispatch": {
2233
- "completedAt": 1786185678000,
2234
- "ready": 8,
2235
- "routed": 8,
2236
- "admitted": 0,
2237
- "degraded": true,
2238
- "holds": [
2239
- { "reason": "parent-lookup-error", "count": 8, "issues": [321, 320, 318] }
2240
- ]
2241
- },
2242
- "codeGraph": {
2243
- "configured": true,
2244
- "status": "degraded",
2245
- "checkedAt": "2026-08-08T13:00:00.000Z",
2246
- "prerequisites": { "indexer": "present", "mcpMount": "missing" },
2247
- "repos": [
2248
- {
2249
- "name": "api",
2250
- "path": "/home/fleet/.cache/conductor-graph/acme/api",
2251
- "clone": "present",
2252
- "index": "present"
2387
+ "projects": [
2388
+ {
2389
+ "ok": true,
2390
+ "paused": false,
2391
+ "activeRuns": 1,
2392
+ "project": "demo",
2393
+ "dispatch": {
2394
+ "completedAt": 1786185678000,
2395
+ "ready": 8,
2396
+ "routed": 8,
2397
+ "admitted": 0,
2398
+ "degraded": true,
2399
+ "holds": [
2400
+ { "reason": "parent-lookup-error", "count": 8, "issues": [321, 320, 318] }
2401
+ ]
2402
+ },
2403
+ "codeGraph": {
2404
+ "configured": true,
2405
+ "status": "degraded",
2406
+ "checkedAt": "2026-08-08T13:00:00.000Z",
2407
+ "prerequisites": { "indexer": "present", "mcpMount": "missing" },
2408
+ "repos": [
2409
+ {
2410
+ "name": "api",
2411
+ "path": "/home/fleet/.cache/conductor-graph/acme/api",
2412
+ "clone": "present",
2413
+ "index": "present"
2414
+ }
2415
+ ],
2416
+ "timer": { "enabled": "enabled", "active": "active" },
2417
+ "refresh": {
2418
+ "result": "success",
2419
+ "fresh": true,
2420
+ "lastSuccessAt": "2026-08-08T12:50:00.000Z",
2421
+ "ageMs": 600000
2422
+ },
2423
+ "reasons": ["worker MCP configuration does not mount the indexer"]
2253
2424
  }
2254
- ],
2255
- "timer": { "enabled": "enabled", "active": "active" },
2256
- "refresh": {
2257
- "result": "success",
2258
- "fresh": true,
2259
- "lastSuccessAt": "2026-08-08T12:50:00.000Z",
2260
- "ageMs": 600000
2261
- },
2262
- "reasons": ["worker MCP configuration does not mount the indexer"]
2263
- }
2425
+ }
2426
+ ]
2264
2427
  }
2265
2428
  ```
2266
2429
 
2267
- Any other path or method returns `404`. `ok` reports process liveness only.
2268
- Nonfatal admission errors and graph degradation keep it `true` so a supervisor
2269
- does not restart-loop. Inspect `dispatch.degraded` and its bounded reason groups
2270
- for queue starvation; inspect `codeGraph` for configured graph health.
2271
- `activeRuns` counts occupied issues live workers plus green PRs awaiting merge.
2430
+ Any other path or method returns `404`. Top-level `ok` is process liveness across
2431
+ every served project; top-level `rssBytes` is the daemon's resident set.
2432
+ Per-project blocks keep `paused`, `activeRuns`, `dispatch`, `codeGraph`, and
2433
+ `workers`. Nonfatal admission errors and graph degradation keep `ok` `true` so a
2434
+ supervisor does not restart-loop. Inspect `dispatch.degraded` and its bounded
2435
+ reason groups for queue starvation; inspect `codeGraph` for configured graph
2436
+ health. `activeRuns` counts occupied issues — live workers plus green PRs
2437
+ awaiting merge.
2272
2438
 
2273
2439
  ## What a worker may and may not do
2274
2440
 
@@ -2390,8 +2556,8 @@ daemon, across a process boundary, not in a prompt the model can rewrite.
2390
2556
  | --- | --- | --- |
2391
2557
  | `conductor_push` | the worker owning the run | The ref is exactly `refs/heads/<that run's branch>`. Fast-forward only; there is no force argument to reject because none is declared. |
2392
2558
  | `conductor_pr_create` | the worker owning the run | The run has no open PR (the same guard admission uses); head is the run branch; base is the repo's configured `defaultBranch`. |
2393
- | `conductor_pr_status` | worker or orchestrator | Read-only. A worker reads only its own run's PR. |
2394
- | `conductor_pr_update_branch` | orchestrator, or the worker owning the run | The PR belongs to this run and project, and is open. |
2559
+ | `conductor_pr_status` | worker or orchestrator | Read-only. A worker reads only its own run's PR; an orchestrator may name any open PR in a routed project repo. |
2560
+ | `conductor_pr_update_branch` | orchestrator, or the worker owning the run | The PR belongs to this project and is open. A worker may only name its own run's PR. |
2395
2561
  | `conductor_pr_merge` | **orchestrator only** | `authority.merge` equals the caller; `headSha` equals the live head *at execution time*; checks green at that same SHA; the project's single merge slot is free. |
2396
2562
  | `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. |
2397
2563
  | `conductor_release` | **orchestrator only** | `authority.release` equals the caller; the per-shape grant permits it; the artefact or environment was declared; the release preconditions hold; the `reason` is in the closed enum. |
@@ -2540,8 +2706,9 @@ Known and deliberate in this version:
2540
2706
  was spent.
2541
2707
  - **GitHub is the only tracker.** The internal `Tracker` port is deliberately
2542
2708
  provider-neutral, but `tracker.kind` accepts only `"github"` today.
2543
- - **One project per daemon process.** Several projects means several processes,
2544
- each with `--project` and its own `--port`.
2709
+ - **One daemon serves every configured project by default.** `setup host` writes a
2710
+ unit without `--project`. Pass `--project NAME` only to filter a foreground or
2711
+ temporary daemon down to one project.
2545
2712
  - **Labels are matched exactly and case-sensitively.** `Ready-For-Agent` is not
2546
2713
  `ready-for-agent`, and the mismatch is silent: the issue is simply never picked
2547
2714
  up.