omp-conductor 0.3.13 → 0.3.16

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
@@ -7,17 +7,16 @@ tiers: first to an orchestrator session that can re-brief the worker, then to yo
7
7
  ## What it is
8
8
 
9
9
  You label an issue. Within one tick the conductor claims it on the tracker, cuts a
10
- worktree, hands one omp session a self-contained brief, watches it to a green PR —
11
- and then stops. Merging is a human act; the conductor never performs it.
10
+ worktree, hands one omp worker a self-contained brief, and watches it to a green
11
+ PR. The worker then stops: it never merges, tags, publishes or deploys.
12
12
 
13
13
  ### Scope
14
14
 
15
- One issue, one green PR. That is the whole remit.
16
-
17
- Merging is a human act, and so is releasing. Releases are **batched**: cut from a
18
- coherent group of merged work by a human-supervised decision, never one per PR. So
19
- nothing in this package tags, pins, deploys or publishes, and no worker is ever
20
- asked to. A worker whose change needs releasing reports that and stops.
15
+ One issue, one green PR. That is the worker's whole remit. A change that needs a
16
+ merge or release is reported to the orchestrator, whose `merge` and `release`
17
+ authority are chosen during setup and default to `human`. Granting either action
18
+ to the orchestrator never grants it to a worker or the dispatch daemon. Releases
19
+ remain batched from coherent groups of merged work, never cut one per worker PR.
21
20
 
22
21
  Code counts every limit that decides whether work starts: concurrency, dollars per
23
22
  day, turns and wall clock per worker, attempts per issue. None of it is left to the
@@ -29,21 +28,23 @@ When a run does get stuck, the first responder is not you. A tier-1 escalation i
29
28
  injected into a long-lived **orchestrator session** that can read the issue and the
30
29
  run's transcript and then either re-brief the worker or decide the problem genuinely
31
30
  needs a human. It never edits product code and never pushes a branch; whether it
32
- may *merge* is a setup answer (`authority`), and it defaults to no. Only tier 2
33
- pages you directly.
31
+ may merge or release is a setup answer (`authority`), and both default to no. Only
32
+ tier 2 pages you directly.
34
33
 
35
- The package ships three deployables, plus one skill:
34
+ The package ships three deployables, plus two skills:
36
35
 
37
36
  | Deployable | Entry | What it is for |
38
37
  | --- | --- | --- |
39
- | omp plugin | `/conductor` slash command | Inspect and arm the conductor from inside an omp session: dry-run the queue, read status, pause, resume. |
38
+ | omp plugin | `/conductor` slash command | Inspect and control the fleet from inside an omp session: status, hold/halt, arm/disarm, pause/resume, setup. |
40
39
  | Standalone daemon | `omp-conductor` binary | The dispatch loop, managed as a background process (`start` / `stop` / `restart`) with a `/healthz` endpoint for a supervisor. |
41
40
  | 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). |
42
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` | Treats the npm and Herdr plugins as one maintenance operation: drain, halt, replace both halves, restart, re-arm through Telegram proof, and verify twice. See [Updating](#updating). |
43
43
 
44
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. The heartbeat
46
- reads the same pause flag both of them write.
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.
47
48
 
48
49
  ## Your workflow vs. the package
49
50
 
@@ -108,9 +109,10 @@ session is carrying two versions of your policy.
108
109
  exactly one value, `"github"`, and every tracker operation shells out to your
109
110
  already-authenticated `gh` CLI — the conductor never stores a token of its own.
110
111
  Gitea, Jira, and file-based trackers are not supported yet; the seam for them is
111
- `src/tracker/github.ts`, which implements the whole six-method `Tracker`
112
+ `src/tracker/github.ts`, which implements the whole nine-method `Tracker`
112
113
  interface in `src/types.ts` (`listReady`, `addLabel`, `removeLabel`, `comment`,
113
- `close`, `linkParent`) that a future backend would swap in.
114
+ `close`, `linkParent`, `parentOf`, `openCloserFor`, `prState`) that a future
115
+ backend would swap in.
114
116
 
115
117
  You tell the conductor where to look with three keys, all in
116
118
  `~/.omp/conductor/config.json` (the [Configuration](#configuration) section has
@@ -164,6 +166,23 @@ Also required on the host:
164
166
  With neither, tier 2 degrades to a comment on the issue. Nothing is broken in
165
167
  that configuration: it is supported, just slower to reach you.
166
168
 
169
+ ## Updating
170
+
171
+ Say “update conductor” from an operator shell or maintenance omp session outside
172
+ the target `herdr-fleet.service`. The bundled `skill://conductor-update` discovers
173
+ the installed and registry versions, drains active work, pins exact-pane recovery,
174
+ replaces both independently installed plugins, restarts through
175
+ `omp-conductor start`, re-arms through the existing Telegram proof, and verifies
176
+ the layered status twice.
177
+
178
+ The skill deliberately does not publish npm or edit an install root. It also
179
+ refuses to run from the fleet pane it must replace: an updater that kills itself
180
+ cannot verify the result. With skill commands enabled, invoke it directly with:
181
+
182
+ ```text
183
+ /skill:conductor-update
184
+ ```
185
+
167
186
  ## Onboarding
168
187
 
169
188
  Onboarding this package has two layers, and installing it gives you both.
@@ -223,7 +242,7 @@ the file behind instead. So a re-run against a project that is already configure
223
242
  opens with one question:
224
243
 
225
244
  ```text
226
- "veltro" is already configured — what would you like to do?
245
+ "platform" is already configured — what would you like to do?
227
246
  > Change one area
228
247
  asks one area's questions; every other answer is carried through from the saved config
229
248
  Walk every question again
@@ -235,14 +254,14 @@ says right now, so the row you want is the row you can see:
235
254
 
236
255
  ```text
237
256
  Which area? Each row shows what it says now
238
- tracker & repos — veltrosecurity/veltro, queue "ready-for-agent", "repo:" → veltro, chad, warden, vectorflow
239
- gates — veltro: none; chad: ruff check . @ backend, pnpm lint @ frontend; warden: ruff check . @ backen
257
+ tracker & repos — acme/platform, queue "ready-for-agent", "repo:" → platform, api, web, worker
258
+ gates — platform: bun run check; api: ruff check . @ backend; web: pnpm lint
240
259
  caps & worker model — 2 workers, 120 turns, 90m, $25/day, 2 attempts (all defaults) — harness default model
241
260
  code graph — not configured — workers grep
242
261
  authority — merge=orchestrator, release=orchestrator
243
- escalation & triage — tier 2 pages Telegram 8236653927, comments too, triage external
262
+ escalation & triage — tier 2 pages Telegram 123456789, comments too, triage external
244
263
  reporting scope — material — escalations, plus green PRs, second failures, and anything that stops the fleet
245
- orchestrator brief — none at /root/.omp/conductor/worktrees/ORCHESTRATOR.md
264
+ orchestrator brief — none at ~/.omp/conductor/worktrees/ORCHESTRATOR.md
246
265
  ```
247
266
 
248
267
  Only that area's questions are asked. Every other answer is read back out of
@@ -252,11 +271,11 @@ writes a config, and it still writes nothing before you agree. The consent scree
252
271
  leads with the delta and then shows the whole project as it would be written:
253
272
 
254
273
  ```text
255
- amending code graph — project veltro
274
+ amending code graph — project platform
256
275
  was not configured — workers grep
257
- now /root/.cache/conductor-graph/veltrosecurity — 4 clone(s): veltro, chad, warden, vectorflow
276
+ now ~/.cache/conductor-graph/acme — 4 clone(s): platform, api, web, worker
258
277
  carried over tracker & repos, gates, caps & worker model, authority, escalation & triage, reporting scope, orchestrator brief
259
- read back from /root/.omp/conductor/config.json and rewritten unchanged
278
+ read back from ~/.omp/conductor/config.json and rewritten unchanged
260
279
  ```
261
280
 
262
281
  A first run, or a project name this config has never seen, never sees either
@@ -274,7 +293,7 @@ The standing prompt is two layers:
274
293
 
275
294
  | Layer | File | Updates how? |
276
295
  | --- | --- | --- |
277
- | Package floor | `src/briefs/orchestrator.md` | Every tick recomposes it into `ORCHESTRATOR.md` from the installed package. `npm install` + restart is enough. |
296
+ | Package floor | `src/briefs/orchestrator.md` | Every tick recomposes it into `ORCHESTRATOR.md` from the installed package. Upgrade the package in this host's existing install root + restart is enough. |
278
297
  | Fleet policy | `POLICY.md` | Yours. Setup writes the scaffold once; the Learning loop edits only this file. |
279
298
  | Composed view | `ORCHESTRATOR.md` | Regenerated from floor + `POLICY.md` on each tick (and at setup). Do not hand-amend it for durable policy. |
280
299
  | Worker brief | `src/briefs/worker.md` | Read per run from the package. |
@@ -329,12 +348,22 @@ The **Learning loop** proposes diffs against `POLICY.md` for you to approve over
329
348
  omp-conductor start
330
349
  ```
331
350
 
332
- `start` does not report success until the daemon actually answers
333
- `GET /healthz`. Spawning is not starting: a daemon whose config is broken, whose
334
- port is taken or whose database is locked exits within a second, and a `start`
335
- that printed "started" for it would hand you a lie you discover only when work
336
- silently fails to be picked up. On failure the error quotes the tail of
337
- `daemon.log`. It refuses to start a second daemon, naming the pid of the live one.
351
+ `start` first starts `herdr-fleet.service` when that optional unit is installed,
352
+ clearing a previous `halt --pane` recovery pin so Herdr can resume the exact
353
+ conductor pane. Hosts without systemd or without that unit keep the standalone
354
+ daemon behaviour. It then waits until the daemon actually answers
355
+ `GET /healthz`; spawning is not starting. A daemon whose config is broken,
356
+ whose port is taken or whose database is locked exits within a second, and the
357
+ command fails with the tail of `daemon.log` instead of printing a false
358
+ success. It refuses to start a second daemon, naming the live pid. Starting
359
+ processes does not clear `pause` or arm ticks; those remain explicit operator
360
+ decisions.
361
+
362
+ Pane recovery spans two separately installed plugins: npm ships the omp
363
+ heartbeat/status half, while `herdr-conductor` supplies `recover.sh`. After an
364
+ npm upgrade, refresh the Herdr plugin from `TerrifiedBug/conductor/herdr` as
365
+ well; publishing or installing npm alone cannot add the recovery-side tick
366
+ request.
338
367
 
339
368
  For a first run, take a single tick in the foreground and watch it:
340
369
 
@@ -346,7 +375,56 @@ The loop ticks every 5 minutes. `omp-conductor stop` shuts the loop down after
346
375
  the current tick rather than mid-run. When the live process is the MainPID of
347
376
  `omp-conductor.service`, stop goes through `systemctl stop` so a unit with
348
377
  `Restart=on-failure` cannot bring it straight back; otherwise it is a raw
349
- `SIGTERM` (then `SIGKILL` after 10 seconds).
378
+ `SIGTERM` (then `SIGKILL` after 10 seconds). An example unit (with
379
+ `SuccessExitStatus=0 143` and `MemoryMax=5G`) ships as
380
+ [`systemd/omp-conductor.service.example`](systemd/omp-conductor.service.example).
381
+
382
+
383
+ ### Stop the conductor (hold / halt)
384
+
385
+ Four control planes used to answer "stop" differently. The package verbs:
386
+
387
+ | Verb | Claiming | Tick sends | Dispatch daemon | Conductor pane |
388
+ | --- | --- | --- | --- | --- |
389
+ | `hold` | paused | disarmed | left running | left running |
390
+ | `halt` | paused | disarmed | stopped (systemctl-aware) | left running |
391
+ | `halt --pane` | paused | disarmed | stopped | stopped + recovery pinned off |
392
+ | `pause` | paused | **still armed** | left running | left running |
393
+
394
+ `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`.
395
+
396
+ `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.
397
+
398
+ `halt --pane` is **fail-closed**: it exits `0` only when the conductor agent is
399
+ *proven* gone. It writes the recovery pin first, so a failed stop still cannot be
400
+ undone by `herdr-conductor` respawning the agent, and then refuses (nonzero exit,
401
+ message on stderr) on every uncertainty:
402
+
403
+ - no tick config exists at all — `recover.sh` reads only
404
+ `$FLEET_CWD/.conductor-pane-halted`, and without that file the pane's own
405
+ directory is unknown, so the pin would land somewhere recovery never looks and
406
+ the agent would be respawned seconds later. `release-pane` refuses for the
407
+ same reason, and `status` shows `recovery unpinnable` rather than `clear`
408
+ - the tick config does not parse — the agent name would be a guess
409
+ - `herdr agent list` is unreachable, prints nothing, or prints output with no
410
+ explicit `agents` array; only a real `agents: []` means "no agents"
411
+ - an agent row is unreadable — a missing `name`/`pane_id`, or an `agent` field
412
+ present with a non-string value. An *absent* or `null` `agent` is the sticky
413
+ claim herdr reports after the agent exits, and stays a normal answer
414
+ - the configured agent name is not unique, or the claimed pane runs some other agent
415
+ - `pane process-info` fails, or the claim is live `omp` but names no recognizable
416
+ omp foreground PID — "cannot see it" is never reported as "it is stopped"
417
+ - a signal cannot be delivered, or liveness cannot be probed — only `ESRCH`
418
+ ("no such process") proves death, so `EPERM` reads as "exists, not ours",
419
+ never as "stopped"
420
+ - the process is still alive after `SIGTERM` then `SIGKILL`
421
+
422
+ The pin is written to the pane's own directory (the one holding
423
+ `.conductor-tick.json`, which is `FLEET_CWD` — the only place `recover.sh` looks),
424
+ including when that tick config is the thing that failed to parse.
425
+
426
+ Clear the pin with `omp-conductor release-pane` when you want recovery again.
427
+
350
428
 
351
429
  ## How one tick works
352
430
 
@@ -560,6 +638,40 @@ multi-repo request is a human decision about contracts; it is not something to
560
638
  infer from a label. Sending the issue back costs a label edit; guessing costs a
561
639
  bad merge.
562
640
 
641
+ ## Host sizing and memory
642
+
643
+ Workers are **in-process** omp sessions inside the daemon's single PID (plus one
644
+ long-lived orchestrator session). systemd's Memory peak for `omp-conductor.service`
645
+ is therefore daemon + every live worker + the orchestrator + any MCP stdio
646
+ children those sessions mount — not a separate worker process list.
647
+
648
+ On the reference deploy that produced [issue #51](https://github.com/TerrifiedBug/conductor/issues/51):
649
+
650
+ | Shape | Observed |
651
+ | --- | --- |
652
+ | Idle / workers restarting | ~430 MB RSS for the daemon alone |
653
+ | Two workers + orchestrator, busy | **3.2–4.2 GB** Memory peak for the unit; up to ~800 MB swap |
654
+
655
+ That peak is **expected for concurrent SDK sessions**, not evidence of a
656
+ conductor-side leak: the SQLite store is disk-backed, admission state is
657
+ per-tick, and worker sessions are disposed when a run ends. What grows is the
658
+ session heap (conversation + tool output); a single graph-assisted run has been
659
+ measured in the hundreds of thousands of characters of tool output.
660
+
661
+ **Practical guidance**
662
+
663
+ - Prefer **≥16 GiB RAM** for the default `maxConcurrentWorkers: 2`, and do **not**
664
+ co-locate ClickHouse / other multi-GB services beside that fleet on an ≤8 GiB
665
+ box.
666
+ - On hosts under ~16 GiB, set `maxConcurrentWorkers` to **1**. `/conductor setup`
667
+ does this automatically when it can read host RAM.
668
+ - Supervise the daemon with a unit that sets `SuccessExitStatus=0 143` and a
669
+ `MemoryMax=` just above your expected peak. A ready-to-edit example ships as
670
+ [`systemd/omp-conductor.service.example`](systemd/omp-conductor.service.example)
671
+ (`MemoryMax=5G` for the two-worker shape).
672
+ - `omp-conductor status` prints daemon `rss` from `/healthz` when the process is
673
+ up, so you can see pressure without scraping journald.
674
+
563
675
  ## Caps
564
676
 
565
677
  Caps resolve per project: the global `defaults` block, then the project's own
@@ -568,7 +680,7 @@ rest. `0` is a real value (a hard stop), not "unset".
568
680
 
569
681
  | Cap | Default | What it protects |
570
682
  | --- | --- | --- |
571
- | `maxConcurrentWorkers` | `2` | Parallel omp sessions. Two, because **CI runner slots, not model tokens, are the usual throughput ceiling** — a third worker would starve its own PR checks on a small self-hosted runner pool. Raise it only if you actually have the runners. |
683
+ | `maxConcurrentWorkers` | `2` (setup may write `1` on <16 GiB hosts) | Parallel in-process omp sessions inside the daemon PID. Two, because **CI runner slots, not model tokens, are the usual throughput ceiling** — a third worker would starve its own PR checks on a small self-hosted runner pool. On hosts under ~16 GiB RAM, prefer `1` so the unit stays out of swap ([host sizing](#host-sizing-and-memory)). Raise it only if you actually have the runners *and* the RAM. |
572
684
  | `dailySpendUsd` | `25` | Rolling-day spend ceiling in USD, or `null` for no spend gate. `0` is a hard stop. Metered from assistant `usage.cost.total`. |
573
685
  | `workerMaxTurns` | `120` | Turn ceiling for one worker. Catches a session looping without converging. |
574
686
  | `workerWallClockMs` | `5400000` (90 minutes) | Wall-clock ceiling for one worker. A session that is merely stuck spends no turns, so turns alone cannot detect it. |
@@ -958,7 +1070,7 @@ indistinguishable from the first:
958
1070
  ```text
959
1071
  [omp-conductor] orchestrator tick inactive: pane w1:p1 (agent "fleet") owns the fleet tick here — this session will not tick
960
1072
  [omp-conductor] orchestrator tick inactive: this pane is agent "scratch", not the fleet agent "fleet" — this session will not tick
961
- [omp-conductor] orchestrator tick inactive: pid 4147344 (claimed 2026-08-07T07:55:36.001Z, session …/fleet.jsonl) owns the fleet tick in /root/.omp/conductor — this session will not tick
1073
+ [omp-conductor] orchestrator tick inactive: pid 12345 (claimed 2026-01-02T03:04:05.000Z, session …/fleet.jsonl) owns the fleet tick in /home/conductor/.omp/conductor — this session will not tick
962
1074
  ```
963
1075
 
964
1076
  A `herdr agent list` that does not answer also declines, for the same reason the
@@ -1079,9 +1191,15 @@ least of all on a fleet whose session lives somewhere else.
1079
1191
 
1080
1192
  ```bash
1081
1193
  omp-conductor start [--port N] [--project NAME]
1194
+ omp-conductor --version
1082
1195
  omp-conductor stop
1083
1196
  omp-conductor restart [--port N] [--project NAME]
1084
1197
  omp-conductor status [--project NAME]
1198
+ omp-conductor hold [--project NAME]
1199
+ omp-conductor halt [--pane] [--project NAME]
1200
+ omp-conductor arm [--project NAME]
1201
+ omp-conductor disarm [--project NAME]
1202
+ omp-conductor release-pane [--project NAME]
1085
1203
  omp-conductor tail <issue> [--project NAME]
1086
1204
  omp-conductor unblock <issue> [--project NAME]
1087
1205
  omp-conductor daemon [--once] [--port N] [--project NAME]
@@ -1094,18 +1212,24 @@ omp-conductor help
1094
1212
 
1095
1213
  | Command | Behaviour |
1096
1214
  | --- | --- |
1097
- | `start` | Spawn the loop in the background, detached, and wait until it answers `GET /healthz` on `:8787`. Refuses if one is already live, naming its pid. If the process dies or never serves, `start` cleans up after it and quotes the tail of `daemon.log`. |
1215
+ | `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`. |
1098
1216
  | `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. |
1099
1217
  | `restart` | 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. 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). |
1100
- | `status [--project NAME]` | Pause state, config and state paths, resolved caps, active runs and today's usage, plus a `daemon` block: pid, uptime, port, project, `/healthz` result and log path. While live workers > 0, prints a `deploy` line naming the count so a busy restart is visible before you take it. A `.conductor-stalled` marker in the state directory adds an `orchestrator STALLED since …` line see [the stall marker](#a-wedged-session-and-the-marker-that-notices). Reads while a daemon in another process writes. |
1218
+ | `status [--project NAME]` | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / `daemon`, then the project body. The next time 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. The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
1219
+ | `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). |
1220
+ | `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. |
1221
+ | `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`. |
1222
+ | `disarm [--project NAME]` | Remove the arm marker so ticks skip. Processes untouched. |
1223
+ | `release-pane [--project NAME]` | Clear the `halt --pane` recovery pin so herdr-conductor may resume the fleet agent again. |
1101
1224
  | `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>`. |
1102
1225
  | `unblock <issue>` | Remove that issue's `blocked` and `failed` state labels through the tracker, so the next tick can claim it again. This is the supported way back for an escalation you answered: eligibility disqualifies any issue carrying a state label, so an answered issue that keeps one is never re-claimed and the answer is inert. Removing a label the issue does not carry is a no-op, so both are always cleared and neither has to be looked up first. `agent:in-progress` is deliberately not touched — it means a worker process exists, which is not something an answer changes. The run history is left exactly as it is: an answered block still spent a worker, so it still counts toward `maxAttemptsPerIssue`, and the output says how many attempts remain — or warns that the next tick will escalate instead of dispatching, when none do. Exits `2` with `unblock needs an issue number` on a missing or malformed positional. |
1103
1226
  | `daemon` | Run the loop in the **foreground**, ticking every 5 minutes and serving `/healthz`. This is what `start` launches, and what a systemd unit should call. Writes the pidfile itself, and refuses with `another daemon is alive (pid N); stop it first` rather than becoming a second dispatcher. |
1104
1227
  | `daemon --once` | Run a single tick and exit. No HTTP server, and no 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. |
1105
1228
  | `--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. |
1106
1229
  | `--project NAME` | Pick the project to service. One daemon process serves exactly one project; with several configured projects the name is required. |
1107
- | `pause` | Stop claiming new work. The running daemon notices on its next tick; runs already in flight finish. The orchestrator heartbeat keeps ticking — its gate is the arm marker, not this flag. |
1108
- | `resume` | Allow claiming again. |
1230
+ | `pause` | Stop claiming new work only. The running daemon notices on its next tick; runs already in flight finish. The orchestrator heartbeat keeps ticking if armed — its gate is the arm marker, not this flag. Prefer `hold` to silence both. |
1231
+ | `resume` | Clear pause only — does **not** re-arm. Run `arm` after an inbound Telegram proof to resume ticks. |
1232
+ | `--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. |
1109
1233
  | `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). |
1110
1234
  | `--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. |
1111
1235
  | `brief-upgrade` | Inspect the package-floor + `POLICY.md` overlay. Reports by default; see [Keeping a brief current](#keeping-a-brief-current). |
@@ -1116,12 +1240,14 @@ omp-conductor help
1116
1240
  | `help`, `--help`, `-h` | Print usage. An unknown or missing verb prints it too, and exits `2`. |
1117
1241
 
1118
1242
  Pause is a flag file under the state directory, so it applies to every project and
1119
- survives a daemon restart.
1243
+ survives a daemon restart. Hold also removes the arm marker the heartbeat reads,
1244
+ so both brains go quiet without killing processes.
1120
1245
 
1121
- Four of these are available in-session as `/conductor setup`, `/conductor status`,
1122
- `/conductor pause` and `/conductor resume`, each taking an optional project name as
1123
- a second word. Background-process management is CLI-only: the plugin does not
1124
- start, stop or restart the daemon.
1246
+ These are available in-session as `/conductor setup`, `/conductor status`,
1247
+ `/conductor hold`, `/conductor halt [--pane]`, `/conductor arm`, `/conductor disarm`,
1248
+ `/conductor release-pane`, `/conductor pause` and `/conductor resume`, each taking
1249
+ an optional project name. Background-process management (`start` / `stop` /
1250
+ `restart`) is CLI-only: the plugin does not start, stop or restart the daemon.
1125
1251
 
1126
1252
  ### Health endpoint
1127
1253
 
@@ -1162,23 +1288,30 @@ The worker ends with a six-line evidence report (issue, pr, state, gates, change
1162
1288
  next). `pushed-green` means it watched the checks go green rather than expecting
1163
1289
  them to.
1164
1290
 
1165
- ### The boundary is prompt text — and one tripwire
1291
+ ### Worker confinement and the integrity tripwire
1166
1292
 
1167
- A worker session gets a `cwd` and nothing else: the harness applies no filesystem
1168
- or network restriction, so "touch any path outside its worktree" is a rule the
1169
- session is asked to keep, not one the process cannot break. Target selection *is*
1170
- mechanical only a repo in `routing.repos` is ever checked out and so are the
1171
- caps, but between them they bound where work starts and how much of it happens,
1172
- not how far a wandering or prompt-injected session can reach.
1293
+ A worker session is rooted at its worktree `cwd`. **Structured file tools are
1294
+ gated mechanically:** `runWorker` asks `createSession({ confineToCwd: true })`,
1295
+ which installs an inline harness extension that blocks `write` / `edit` /
1296
+ `read` / `grep` / `glob` when the tool's path resolves outside that worktree
1297
+ (symlink-aware). Target selection was already mechanical only a repo in
1298
+ `routing.repos` is ever checked out and the caps still bound *how much* work
1299
+ happens.
1173
1300
 
1174
- The one path that is checked mechanically is the conductor itself. At startup the
1175
- daemon sha256s every `.ts` and `.md` file of its own installed `src/` the
1176
- dispatcher and the briefs both, since rewriting a brief buys more than rewriting
1177
- the loop — and re-walks that tree on every tick (about 0.6 ms). Any difference at
1178
- all, changed or added or removed, is read as the package having been modified
1179
- underneath a running daemon: the tick claims nothing, the fleet is paused, and a
1180
- tier-2 escalation naming the first few differing paths pages you **once**, not
1181
- every five minutes.
1301
+ **`bash` is not gated.** Its argument is an opaque shell string; pretending to
1302
+ parse it would be a false sense of security. The brief still forbids escaping
1303
+ via the shell, and the deploy-level answer is a least-privilege worker uid
1304
+ (below).
1305
+
1306
+ #### Integrity tripwire (package self-hash)
1307
+
1308
+ Separately, the conductor watches *itself*. At startup the daemon sha256s every
1309
+ `.ts` and `.md` file of its own installed `src/` — the dispatcher and the briefs
1310
+ both, since rewriting a brief buys more than rewriting the loop — and re-walks
1311
+ that tree on every tick (about 0.6 ms). Any difference at all, changed or added
1312
+ or removed, is read as the package having been modified underneath a running
1313
+ daemon: the tick claims nothing, the fleet is paused, and a tier-2 escalation
1314
+ naming the first few differing paths pages you **once**, not every five minutes.
1182
1315
 
1183
1316
  **A normal deploy never trips it.** The baseline is recorded per daemon process,
1184
1317
  so installing a new build and restarting the unit re-records it from the new
@@ -1187,9 +1320,32 @@ diverge from it. That also means `omp-conductor resume` on its own will not hold
1187
1320
  — the next tick re-walks, still differs, and pauses again. Put the files back, or
1188
1321
  restart onto the build you meant to be running.
1189
1322
 
1190
- This is detection, not prevention. It catches the worker that wandered, and it
1191
- catches the human who edited the live install "just to test something", which in
1192
- practice is the commoner of the two.
1323
+ This catches a worker (or human) that still managed to edit the live install —
1324
+ including via `bash` after the fact. It is detection for the package boundary,
1325
+ not a substitute for the worktree gate or a dedicated uid.
1326
+
1327
+ #### Least-privilege worker uid (deploy)
1328
+
1329
+ The largest remaining win is OS-level: run the daemon (or at least worker
1330
+ sessions, when the harness supports a uid switch) as a user that can write only
1331
+ its worktrees and mirrors. A sketch that matches the reference single-host
1332
+ deploy:
1333
+
1334
+ 1. Create a system user, e.g. `conductor-worker`, with home under
1335
+ `/var/lib/conductor-worker` (or similar).
1336
+ 2. `chown` the project's `workspaceRoot` and `mirrorRoot` to that user; leave
1337
+ `~/.omp/conductor/config.json` readable only by the operator/daemon account
1338
+ (`0600` as shipped).
1339
+ 3. Do **not** put the worker uid in `docker` / `sudoers`, and do not give it the
1340
+ operator's `gh` auth if a narrower deploy token can open PRs in the routed
1341
+ repos alone.
1342
+ 4. Point the [example systemd unit](systemd/omp-conductor.service.example)
1343
+ `User=` / `Group=` at that account once the daemon itself should run
1344
+ unprivileged end-to-end.
1345
+
1346
+ Until that uid exists, a root-or-operator daemon still has a mechanical
1347
+ worktree gate on structured tools and an integrity tripwire on its own package —
1348
+ but `bash` plus host credentials remain a prompt-and-deploy problem.
1193
1349
 
1194
1350
  ## Limitations
1195
1351
 
@@ -1241,15 +1397,16 @@ Known and deliberate in this version:
1241
1397
  `slots = maxConcurrentWorkers - live workers`, admits at most that many issues
1242
1398
  per tick, and dispatches them together. To see them, read `omp-conductor
1243
1399
  status`, which lists every occupied issue, or follow `daemon.log`.
1244
- - **Merges, releases and deploys are human-only, by design.** The conductor
1245
- produces green PRs and stops.
1246
- - **Worker confinement is behavioural.** `runWorker` hands the session a `cwd` and
1247
- a brief; nothing stops it reading or writing elsewhere on the host, and on a
1248
- single-user deploy that includes the state directory and the host's `gh`
1249
- credentials. The [integrity tripwire](#the-boundary-is-prompt-text--and-one-tripwire)
1250
- turns one case of this into a paused, paged fleet after the fact; the prevention
1251
- half is a deployment concern — run workers as a least-privileged uid whose write
1252
- access ends at its worktree and mirror.
1400
+ - **Workers stop at green PRs.** They never merge, release or deploy. Those
1401
+ actions default to a human, but setup may grant either to the orchestrator;
1402
+ `authority` never grants them to a worker or the dispatch daemon.
1403
+ - **Worker confinement is partial.** Structured `write` / `edit` / `read` /
1404
+ `grep` / `glob` calls are blocked outside the worktree by an inline harness
1405
+ extension (`confineToCwd`). `bash` is not: a shell one-liner can still leave
1406
+ the tree. Prefer a [least-privilege worker uid](#least-privilege-worker-uid-deploy);
1407
+ the [integrity tripwire](#integrity-tripwire-package-self-hash) still pages if
1408
+ the installed package itself changes under a live daemon.
1409
+
1253
1410
 
1254
1411
  ## License
1255
1412
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.3.13",
3
+ "version": "0.3.16",
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.",
@@ -17,12 +17,13 @@
17
17
  ]
18
18
  },
19
19
  "bin": {
20
- "omp-conductor": "./src/cli.ts"
20
+ "omp-conductor": "src/cli.ts"
21
21
  },
22
22
  "files": [
23
23
  "src",
24
24
  "!src/**/*.test.ts",
25
25
  "skills",
26
+ "systemd",
26
27
  "README.md",
27
28
  "LICENSE"
28
29
  ],