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 +227 -70
- package/package.json +3 -2
- package/skills/conductor-update/SKILL.md +157 -0
- package/src/brief-upgrade.ts +1 -1
- package/src/briefs/orchestrator.md +3 -2
- package/src/briefs/worker.md +1 -1
- package/src/cli.ts +161 -40
- package/src/confinement.ts +123 -0
- package/src/daemon.ts +55 -2
- package/src/fleet.ts +1271 -0
- package/src/host.ts +90 -0
- package/src/lifecycle.ts +182 -77
- package/src/omp.ts +12 -0
- package/src/orchestrator-tick.ts +64 -3
- package/src/plugin.ts +110 -17
- package/src/tracker/github.ts +25 -1
- package/src/types.ts +10 -0
- package/src/worker.ts +2 -0
- package/systemd/omp-conductor.service.example +54 -0
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
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
46
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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 —
|
|
239
|
-
gates —
|
|
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
|
|
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
|
|
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
|
|
274
|
+
amending code graph — project platform
|
|
256
275
|
was not configured — workers grep
|
|
257
|
-
now
|
|
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
|
|
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.
|
|
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`
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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` |
|
|
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]` |
|
|
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` |
|
|
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
|
-
|
|
1122
|
-
`/conductor
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
###
|
|
1291
|
+
### Worker confinement and the integrity tripwire
|
|
1166
1292
|
|
|
1167
|
-
A worker session
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
- **
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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.
|
|
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": "
|
|
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
|
],
|