omp-conductor 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +549 -234
- package/package.json +8 -5
- package/schema/config.schema.json +609 -0
- package/src/availability.ts +165 -0
- package/src/board.ts +19 -32
- package/src/brief-upgrade.ts +1 -1
- package/src/briefs/orchestrator.md +72 -31
- package/src/briefs/policy.md +48 -36
- package/src/briefs/probes/gates.md +51 -0
- package/src/briefs/probes/project-context.md +59 -0
- package/src/briefs/probes/release-procedure.md +81 -0
- package/src/cli.ts +356 -212
- package/src/config-schema.ts +352 -0
- package/src/config.ts +1037 -679
- package/src/confinement.ts +54 -0
- package/src/daemon.ts +644 -390
- package/src/diff-flags.ts +73 -4
- package/src/digest-schedule.ts +92 -24
- package/src/escalate.ts +89 -22
- package/src/fleet.ts +351 -46
- package/src/generate-schema.ts +21 -0
- package/src/graph.ts +3 -3
- package/src/host.ts +16 -0
- package/src/omp.ts +21 -1
- package/src/orchestrator-tick.ts +732 -56
- package/src/privileged.ts +264 -0
- package/src/reports.ts +203 -6
- package/src/session-host.ts +3 -0
- package/src/setup-host.ts +209 -24
- package/src/setup-install.ts +320 -0
- package/src/setup-probe.ts +412 -0
- package/src/setup-wizard.ts +1946 -0
- package/src/setup.ts +457 -53
- package/src/store.ts +610 -98
- package/src/tracker/github.ts +43 -5
- package/src/types.ts +153 -14
- package/src/upgrade.ts +44 -10
- package/src/verbs/actions.ts +131 -13
- package/src/verbs/server.ts +40 -18
- package/src/wizard-ui.ts +249 -0
- package/src/worker.ts +24 -7
- package/skills/conductor-onboarding/SKILL.md +0 -748
- package/skills/conductor-update/SKILL.md +0 -51
- package/src/plugin.ts +0 -1495
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
|
|
34
|
+
The package ships two deployables:
|
|
35
35
|
|
|
36
36
|
| Deployable | Entry | What it is for |
|
|
37
37
|
| --- | --- | --- |
|
|
38
|
-
|
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
the
|
|
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
|
-
|
|
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:
|
|
@@ -75,34 +79,31 @@ Reporting is the one half of that the config also knows about, because the wizar
|
|
|
75
79
|
has to ask something in order to seed the brief, and it is the one half the
|
|
76
80
|
runtime acts on:
|
|
77
81
|
|
|
78
|
-
|
|
|
82
|
+
| Setup choice | What may interrupt | What waits |
|
|
79
83
|
| --- | --- | --- |
|
|
80
|
-
| `material`
|
|
81
|
-
| `escalations` |
|
|
82
|
-
| `decisions` | Tier-2 decisions and fleet-stopping conditions
|
|
84
|
+
| `material` | Every configured material event, when operator availability permits. | Non-bypass events outside configured hours wait for the next configured digest or opening. |
|
|
85
|
+
| `escalations` | Tier-2 escalations, when operator availability permits. | Everything else waits for the configured digest. |
|
|
86
|
+
| `decisions` (recommended) | Tier-2 decisions and fleet-stopping conditions, when operator availability permits. | Every other material event is recorded durably and ships with the configured digest as one message. |
|
|
87
|
+
| `quiet` | Tier-2 escalations, fleet stops, and confirmed failures, when operator availability permits. | Everything else waits for one daily rollup. |
|
|
83
88
|
|
|
84
89
|
**What the scope does:** the [orchestrator heartbeat](#orchestrator-tick) appends
|
|
85
|
-
|
|
86
|
-
prompt instead of only in a brief the session read hours ago.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
The
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Changing the key later does not rewrite an `ORCHESTRATOR.md` you already have:
|
|
104
|
-
the tick line changes, the brief does not. Edit its Reporting section too, or the
|
|
105
|
-
session is carrying two versions of your policy.
|
|
90
|
+
the current constraint to every tick it sends, so the reporting contract arrives
|
|
91
|
+
with the prompt instead of only in a brief the session read hours ago. Explicit
|
|
92
|
+
policies name their actual interrupt categories and digest cadence. The policy is
|
|
93
|
+
re-read from `~/.omp/conductor/config.json` on **every** tick, and escalation,
|
|
94
|
+
direct Telegram, and durable report paths apply it mechanically. Turning the
|
|
95
|
+
volume up or down — `omp-conductor setup` again, or an edit to the file — therefore
|
|
96
|
+
binds the next tick without restarting the session.
|
|
97
|
+
|
|
98
|
+
No config, an unreadable or invalid config, or several unnamed projects fall
|
|
99
|
+
back to the legacy `material` scope for the heartbeat and log the reason once.
|
|
100
|
+
An invalid live availability policy blocks autonomous Telegram fail-closed; it
|
|
101
|
+
does not guess that the operator is awake.
|
|
102
|
+
|
|
103
|
+
Changing the key later does not rewrite an `ORCHESTRATOR.md` you already have.
|
|
104
|
+
The generated `POLICY.md` describes every scope without pinning the current
|
|
105
|
+
choice; the tick constraint remains derived from live config. Keep any
|
|
106
|
+
operator-owned reporting additions in `ORCHESTRATOR.md` consistent with it.
|
|
106
107
|
|
|
107
108
|
## Where issues come from
|
|
108
109
|
|
|
@@ -117,7 +118,7 @@ backend would swap in.
|
|
|
117
118
|
|
|
118
119
|
You tell the conductor where to look with three keys, all in
|
|
119
120
|
`~/.omp/conductor/config.json` (the [Configuration](#configuration) section has
|
|
120
|
-
the full annotated example, and
|
|
121
|
+
the full annotated example, and `omp-conductor setup` will interview you for these
|
|
121
122
|
and create any missing labels):
|
|
122
123
|
|
|
123
124
|
| Key | Meaning |
|
|
@@ -135,6 +136,10 @@ number of code repos, and both label names are yours to configure.
|
|
|
135
136
|
omp plugin install omp-conductor
|
|
136
137
|
```
|
|
137
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
|
+
|
|
138
143
|
From a checkout of the monorepo, `./setup.sh` checks both plugins. It preserves
|
|
139
144
|
an existing npm-managed `omp-conductor` and links only the Herdr half, so running
|
|
140
145
|
setup on a release-based fleet cannot silently switch omp to mutable source.
|
|
@@ -264,60 +269,89 @@ The command never publishes npm, edits an install root, or delegates lifecycle
|
|
|
264
269
|
steps to an AI session. It refuses to run inside a Herdr-managed session because
|
|
265
270
|
an updater that restarts itself cannot verify the result.
|
|
266
271
|
|
|
267
|
-
The bundled `skill://conductor-update` remains available as a natural-language
|
|
268
|
-
entry point, but it only runs this command; the lifecycle is implemented and
|
|
269
|
-
verified by the CLI.
|
|
270
|
-
|
|
271
272
|
## Onboarding
|
|
272
273
|
|
|
273
|
-
|
|
274
|
+
`omp-conductor setup` is the whole of it. One command, in a plain terminal, doing
|
|
275
|
+
the two jobs onboarding has always had:
|
|
274
276
|
|
|
275
|
-
|
|
|
276
|
-
| --- | --- |
|
|
277
|
-
|
|
|
278
|
-
|
|
|
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. |
|
|
279
281
|
|
|
280
|
-
The split
|
|
282
|
+
The split matters because the two halves fail differently. A wrong config value is
|
|
281
283
|
a run that errors on the next tick; a wrong release boundary is a fleet that
|
|
282
|
-
publishes something at 03:00. The first is worth a
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
- **
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
- **
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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`:
|
|
314
344
|
|
|
315
|
-
```
|
|
316
|
-
|
|
345
|
+
```bash
|
|
346
|
+
omp-conductor setup --no-ai
|
|
317
347
|
```
|
|
318
348
|
|
|
319
|
-
|
|
320
|
-
|
|
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
|
+
```
|
|
321
355
|
|
|
322
356
|
### Changing one setting
|
|
323
357
|
|
|
@@ -332,6 +366,8 @@ opens with one question:
|
|
|
332
366
|
asks one area's questions; every other answer is carried through from the saved config
|
|
333
367
|
Walk every question again
|
|
334
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
|
|
335
371
|
```
|
|
336
372
|
|
|
337
373
|
Amending is the default. Pick it and the eight areas are listed with what each one
|
|
@@ -365,13 +401,45 @@ amending code graph — project platform
|
|
|
365
401
|
|
|
366
402
|
A first run, or a project name this config has never seen, never sees either
|
|
367
403
|
question: there is nothing to amend, so it is the full interview exactly as
|
|
368
|
-
before. Choosing *Walk every question again*
|
|
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
|
|
369
406
|
pre-filled with what is configured, Enter to keep it — with one wrinkle worth
|
|
370
407
|
knowing: the two authority confirms and the orchestrator-session confirm cannot
|
|
371
408
|
start on "yes", so Entering through the full interview **revokes** a delegation
|
|
372
409
|
rather than renewing it. Amending the `authority` area names the current grant in
|
|
373
410
|
the question, which is the safer way to leave one alone.
|
|
374
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
|
+
|
|
375
443
|
### Keeping a brief current
|
|
376
444
|
|
|
377
445
|
The standing prompt is two layers:
|
|
@@ -382,7 +450,6 @@ The standing prompt is two layers:
|
|
|
382
450
|
| Fleet policy | `POLICY.md` | Yours. Setup writes the scaffold once; the Learning loop edits only this file. |
|
|
383
451
|
| Composed view | `ORCHESTRATOR.md` | Regenerated from floor + `POLICY.md` on each tick (and at setup). Do not hand-amend it for durable policy. |
|
|
384
452
|
| Worker brief | `src/briefs/worker.md` | Read per run from the package. |
|
|
385
|
-
| Onboarding skill | `skills/conductor-onboarding/SKILL.md` | Read from the installed package. |
|
|
386
453
|
|
|
387
454
|
```bash
|
|
388
455
|
omp-conductor brief-upgrade # report overlay / legacy state
|
|
@@ -439,7 +506,7 @@ time Telegram approval, `POLICY.md`-only edit, Hard-boundary prohibition, and
|
|
|
439
506
|
3. Run the wizard:
|
|
440
507
|
|
|
441
508
|
```text
|
|
442
|
-
|
|
509
|
+
omp-conductor setup
|
|
443
510
|
```
|
|
444
511
|
|
|
445
512
|
The wizard reads the tracker with the same routing code as the daemon. It shows every issue that the next tick can route.
|
|
@@ -509,18 +576,19 @@ The package also ships a generic unit at
|
|
|
509
576
|
[`systemd/omp-conductor.service.example`](systemd/omp-conductor.service.example).
|
|
510
577
|
|
|
511
578
|
|
|
512
|
-
### Stop the conductor (hold /
|
|
579
|
+
### Stop the conductor (hold / stop)
|
|
513
580
|
|
|
514
|
-
|
|
581
|
+
Two words, and one of them takes a flag:
|
|
515
582
|
|
|
516
583
|
| Verb | Claiming | Tick sends | Dispatch daemon | Conductor pane |
|
|
517
584
|
| --- | --- | --- | --- | --- |
|
|
518
585
|
| `hold` | paused | disarmed | left running | left running |
|
|
519
|
-
| `
|
|
520
|
-
| `
|
|
521
|
-
| `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 |
|
|
522
588
|
|
|
523
|
-
`resume` clears pause
|
|
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`.
|
|
590
|
+
|
|
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.
|
|
524
592
|
|
|
525
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.
|
|
526
594
|
|
|
@@ -583,7 +651,7 @@ message on stderr) on every uncertainty:
|
|
|
583
651
|
- no tick config exists at all — `recover.sh` reads only
|
|
584
652
|
`$FLEET_CWD/.conductor-pane-halted`, and without that file the pane's own
|
|
585
653
|
directory is unknown, so the pin would land somewhere recovery never looks and
|
|
586
|
-
the agent would be respawned seconds later. `
|
|
654
|
+
the agent would be respawned seconds later. `resume` refuses for the
|
|
587
655
|
same reason, and `status` shows `recovery unpinnable` rather than `clear`
|
|
588
656
|
- the tick config does not parse — the agent name would be a guess
|
|
589
657
|
- `herdr agent list` is unreachable, prints nothing, or prints output with no
|
|
@@ -603,7 +671,7 @@ The pin is written to the pane's own directory (the one holding
|
|
|
603
671
|
`.conductor-tick.json`, which is `FLEET_CWD` — the only place `recover.sh` looks),
|
|
604
672
|
including when that tick config is the thing that failed to parse.
|
|
605
673
|
|
|
606
|
-
Clear the pin with `omp-conductor
|
|
674
|
+
Clear the pin with `omp-conductor resume` when you want recovery again.
|
|
607
675
|
|
|
608
676
|
|
|
609
677
|
## How one tick works
|
|
@@ -622,7 +690,7 @@ Per tick, for the daemon's project:
|
|
|
622
690
|
[what settles a green PR](#what-settles-a-green-pr).
|
|
623
691
|
2. **Paused?** If the pause sentinel exists, the tick claims nothing and returns.
|
|
624
692
|
Settlement has already run, but no queue or admission work occurs. This makes
|
|
625
|
-
`omp-conductor
|
|
693
|
+
`omp-conductor hold` take effect without signalling the process.
|
|
626
694
|
3. **List the queue.** Open issues in `tracker.repo` labelled `queueLabel`.
|
|
627
695
|
4. **Filter and route.** An issue is eligible only if it carries the queue label
|
|
628
696
|
and none of the three state labels (`inProgress`, `blocked`, `failed`). Eligible
|
|
@@ -808,6 +876,10 @@ every check succeeded or was skipped. Missing or nonterminal checks become
|
|
|
808
876
|
`pushed-pending` and are rechecked on later ticks; red or cancelled checks become
|
|
809
877
|
`failed` with a bounded job/log digest. Only verified evidence becomes
|
|
810
878
|
`pushed-green`.
|
|
879
|
+
If a failed report mentions PRs only in prose, the daemon retains the last URL
|
|
880
|
+
whose `owner/repo` matches the run's repository; links to other repositories are
|
|
881
|
+
ignored. This preserves the continuation target without trusting an unrelated
|
|
882
|
+
PR mentioned in the same report.
|
|
811
883
|
|
|
812
884
|
What happens after verification is a human's decision, taken minutes to days
|
|
813
885
|
later and never announced to the daemon — so every tick asks the tracker about
|
|
@@ -816,14 +888,15 @@ every pushed PR it is still holding:
|
|
|
816
888
|
| PR | Row becomes | Why |
|
|
817
889
|
| --- | --- | --- |
|
|
818
890
|
| merged | `merged` | The work landed. This is the state `merged` was reserved for. |
|
|
819
|
-
| closed without merging | `failed`, with the PR in `lastError` | A human read the work and
|
|
891
|
+
| closed without merging | `failed`, class `returned-for-revision`, with the PR in `lastError` | A human read the work and asked for another pass. Leaving it `pushed-green` strands the issue forever behind a PR nobody will merge, and calling it `merged` is a lie about code that is not on the base branch. `failed` is the honest row state; the class preserves the review decision and releases the issue so a re-queue can be attempted again. |
|
|
820
892
|
| still open | unchanged | The normal steady state. Its issue must stay occupied, or a second attempt lands on the live PR. |
|
|
821
893
|
| could not be determined | unchanged | A flaky network, a revoked token, a deleted PR. An unknown answer never settles a row; the next tick asks again for free. |
|
|
822
894
|
|
|
823
|
-
Run history is untouched. A PR closed without merging
|
|
824
|
-
|
|
825
|
-
loses `agent:in-progress` from its issue: the row transition and
|
|
826
|
-
fact, and a terminal answer about the PR proves no worker
|
|
895
|
+
Run history is untouched. A PR closed without merging consumes one continuation,
|
|
896
|
+
not a failed implementation attempt; a merge spends neither budget. A settled
|
|
897
|
+
row also loses `agent:in-progress` from its issue: the row transition and label
|
|
898
|
+
removal are one fact, and a terminal answer about the PR proves no worker
|
|
899
|
+
process owns the issue,
|
|
827
900
|
so the duplicate-dispatch guard it exists for is spent. The removal is *enqueued
|
|
828
901
|
on the label projection outbox in the same breath as the terminal write* — a
|
|
829
902
|
durable local write that cannot fail on the tracker — so the row settles at once
|
|
@@ -848,6 +921,30 @@ issues carrying `agent:in-progress`, which eligibility reads as "a worker owns
|
|
|
848
921
|
this" — with the brief forbidding the orchestrator from editing a state label and
|
|
849
922
|
`unblock` declining to clear that one, neither issue could ever be claimed again.
|
|
850
923
|
|
|
924
|
+
### Base-branch health after merge
|
|
925
|
+
|
|
926
|
+
The daemon records two different facts after a merge:
|
|
927
|
+
|
|
928
|
+
- The **post-merge audit** attributes a regression to one merge. For up to 24
|
|
929
|
+
hours, it checks only push-triggered workflow runs for the exact merge SHA and
|
|
930
|
+
base branch. A new red result adds `base-branch-red` evidence and escalates.
|
|
931
|
+
- **Current health** drives `status` and release policy. On every sweep, the
|
|
932
|
+
daemon resolves the live head of each branch it merged into during the last
|
|
933
|
+
seven days, then reads only push-triggered runs for that head and branch. The
|
|
934
|
+
status row includes the head SHA and run count.
|
|
935
|
+
|
|
936
|
+
Current health is `green` only when every observed run completed successfully,
|
|
937
|
+
neutrally, or skipped. A failing conclusion is `red`; an in-progress or
|
|
938
|
+
unrecognised conclusion is `pending`; and a head with no push-triggered run is
|
|
939
|
+
`unknown`, never green. Pending and unknown heads are rechecked. Green and red
|
|
940
|
+
heads are read again when the branch moves, so an old verdict cannot describe a
|
|
941
|
+
new commit. If GitHub cannot return the head or its runs, the daemon keeps the
|
|
942
|
+
last honest row instead of replacing evidence with a network failure.
|
|
943
|
+
|
|
944
|
+
The `base-branch-green` release requirement reads this current live-head row for
|
|
945
|
+
the repository being released. Red, pending, unknown, and absent evidence all
|
|
946
|
+
refuse the release.
|
|
947
|
+
|
|
851
948
|
### The settlement audit
|
|
852
949
|
|
|
853
950
|
Verifying `state: pushed-green` left two lines of the same report still taken on
|
|
@@ -868,6 +965,7 @@ change a run's state, hold a merge, or spend an attempt.
|
|
|
868
965
|
| `undisclosed-file` | The PR touched a file the `changed:` line never named. Lockfiles are exempt — they are derived from a manifest the report did disclose. |
|
|
869
966
|
| `changed-line-missing` | The report had no usable `changed:` line at all. One flag, not one per file. |
|
|
870
967
|
| `unmatched-claim` | `changed:` named a path the PR never touched. The weaker direction, and reported as such. |
|
|
968
|
+
| `report-format-unparsed` | The same file appeared as both claimed-but-untouched and touched-but-unclaimed, so the `changed:` line's format defeated the parser. Read the diff directly; this is not a trust signal against the worker. |
|
|
871
969
|
| `test-file-deleted` | A test file left the tree with no rename to account for it. |
|
|
872
970
|
| `test-disabled` | A `.skip` / `.only` / `xit` / `@pytest.mark.skip` / `t.Skip` marker appears on a line the PR added. |
|
|
873
971
|
| `assertions-removed` | An assertion was commented out, or a test file lost more assertions than it gained. |
|
|
@@ -950,6 +1048,11 @@ inside the service's cgroup, so systemd's Memory peak for
|
|
|
950
1048
|
MCP stdio children those sessions mount. `MemoryMax=` governs that whole total,
|
|
951
1049
|
not one process.
|
|
952
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
|
+
|
|
953
1056
|
On the reference deploy that produced [issue #51](https://github.com/TerrifiedBug/conductor/issues/51):
|
|
954
1057
|
|
|
955
1058
|
| Shape | Observed |
|
|
@@ -968,12 +1071,13 @@ measured in the hundreds of thousands of characters of tool output.
|
|
|
968
1071
|
- Prefer **≥16 GiB RAM** for the default `maxConcurrentWorkers: 2`, and do **not**
|
|
969
1072
|
co-locate ClickHouse / other multi-GB services beside that fleet on an ≤8 GiB
|
|
970
1073
|
box.
|
|
971
|
-
- On hosts under ~16 GiB,
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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.
|
|
977
1081
|
- `omp-conductor status` prints daemon `rss` from `/healthz` when the process is
|
|
978
1082
|
up, so you can see pressure without scraping journald.
|
|
979
1083
|
|
|
@@ -1001,7 +1105,7 @@ Set `dailySpendUsd` to `null` (wizard: blank) for no money gate — turns and wa
|
|
|
1001
1105
|
limit simply defers work to a later tick. The spend cap **pauses the daemon and
|
|
1002
1106
|
pages at Tier 2**: a loop that is burning money has to halt itself, because
|
|
1003
1107
|
waiting for someone to notice tomorrow is how a runaway becomes expensive.
|
|
1004
|
-
Work resumes only after `omp-conductor resume
|
|
1108
|
+
Work resumes only after `omp-conductor resume`.
|
|
1005
1109
|
|
|
1006
1110
|
`workerMaxTurns` and `workerWallClockMs` are enforced inside the session driver.
|
|
1007
1111
|
The daemon reads a live run's effective turn ceiling at every turn boundary. Use
|
|
@@ -1021,7 +1125,7 @@ escalation.
|
|
|
1021
1125
|
Pause one live worker cooperatively with
|
|
1022
1126
|
`omp-conductor worker pause <issue> [--project NAME]`. The daemon aborts the
|
|
1023
1127
|
active turn to an idle harness state, freezes the remaining wall-clock budget,
|
|
1024
|
-
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>`
|
|
1025
1129
|
continues the same session with a prompt to re-check its last action before
|
|
1026
1130
|
proceeding. To end that run instead, use
|
|
1027
1131
|
`omp-conductor worker stop <issue> --reason TEXT [--project NAME]`. Stop works
|
|
@@ -1146,21 +1250,21 @@ defined" in one call instead of twenty greps.
|
|
|
1146
1250
|
`omp-conductor` never installs, starts, imports, or depends on the indexer for
|
|
1147
1251
|
dispatch. With `graphProject` unset, nothing about dispatch, caps, escalation, or
|
|
1148
1252
|
status changes. A fresh host needs both of these before an index is worth
|
|
1149
|
-
anything, and `graph
|
|
1253
|
+
anything, and `setup graph` reports them as step 0:
|
|
1150
1254
|
|
|
1151
1255
|
1. **`codebase-memory-mcp` on PATH** — a separate project,
|
|
1152
1256
|
[DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp).
|
|
1153
1257
|
2. **Mounted as an MCP server** in `~/.omp/agent/mcp.json`, on the account the
|
|
1154
1258
|
daemon runs as. Miss this and the failure is silent: every index builds
|
|
1155
1259
|
correctly, no worker session can read any of them, so workers fall back to
|
|
1156
|
-
grepping and the feature looks like a no-op. `graph
|
|
1260
|
+
grepping and the feature looks like a no-op. `setup graph` prints the entry.
|
|
1157
1261
|
|
|
1158
1262
|
Say yes and the wizard asks for one root, then derives one clone per routed repo
|
|
1159
1263
|
underneath it (default `~/.cache/conductor-graph/<org>/<repo>`) and writes it to
|
|
1160
1264
|
each repo's [`graphProject`](#configuration). The only automatic interaction is
|
|
1161
1265
|
a bounded, read-only health query; this package never clones, fetches, builds an
|
|
1162
1266
|
index, or changes systemd. Dispatch, caps and escalation do not depend on graph
|
|
1163
|
-
health. On a fleet configured before this key existed,
|
|
1267
|
+
health. On a fleet configured before this key existed, `omp-conductor setup` and
|
|
1164
1268
|
the `code graph` area add it in two prompts — see
|
|
1165
1269
|
[Changing one setting](#changing-one-setting).
|
|
1166
1270
|
|
|
@@ -1185,11 +1289,11 @@ edits — orient with it, then read the real file before changing it.
|
|
|
1185
1289
|
### Creating and refreshing them
|
|
1186
1290
|
|
|
1187
1291
|
```bash
|
|
1188
|
-
omp-conductor graph
|
|
1189
|
-
omp-conductor
|
|
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
|
|
1190
1294
|
```
|
|
1191
1295
|
|
|
1192
|
-
`graph
|
|
1296
|
+
`setup graph --print` prints a `git clone` for every clone that does not exist yet, the
|
|
1193
1297
|
one-shot index command per repo, and a `cbm-reindex.service` + `cbm-reindex.timer`
|
|
1194
1298
|
pair built from the project's own repos and branches. `--write` stages all three
|
|
1195
1299
|
in the state directory and prints the two `sudo` lines that install and enable
|
|
@@ -1217,7 +1321,7 @@ Two properties of the generated unit are deliberate:
|
|
|
1217
1321
|
The unit spells out `HOME` and an explicit `PATH`, because systemd supplies
|
|
1218
1322
|
neither usefully: the indexer resolves its store from `HOME`, systemd's default
|
|
1219
1323
|
`PATH` has no `~/.local/bin`, and the indexer shells out to `git`. Both are the
|
|
1220
|
-
user that ran `graph
|
|
1324
|
+
user that ran `setup graph`; the unit sets no `User=`, so check them if that is
|
|
1221
1325
|
not the account the timer runs as.
|
|
1222
1326
|
|
|
1223
1327
|
### Seeing whether the graph is usable
|
|
@@ -1298,20 +1402,72 @@ a report reached you only if the model remembered to call `telegram_send`. On
|
|
|
1298
1402
|
none of the three arrived, and nothing anywhere recorded that fact — an undelivered
|
|
1299
1403
|
report and a quiet tick look identical.
|
|
1300
1404
|
|
|
1405
|
+
### Material events survive the session
|
|
1406
|
+
|
|
1407
|
+
A deferred digest does not use the session transcript as its source of truth.
|
|
1408
|
+
Record each ordinary outcome when it happens:
|
|
1409
|
+
|
|
1410
|
+
```bash
|
|
1411
|
+
omp-conductor event record \
|
|
1412
|
+
--category merge \
|
|
1413
|
+
--summary "#42 merged" \
|
|
1414
|
+
--evidence "https://github.com/acme/api/pull/42"
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
`--category` is a short lowercase slug. `--summary` states the outcome, and
|
|
1418
|
+
`--evidence` names the issue, PR, release, run, commit, or URL that proves it.
|
|
1419
|
+
Use `--occurred-at <ISO timestamp>` when the event happened earlier; otherwise,
|
|
1420
|
+
the command uses the current time. The command writes one row to SQLite and
|
|
1421
|
+
sends nothing. The row survives later ticks, session compaction, session
|
|
1422
|
+
replacement, and daemon restarts.
|
|
1423
|
+
|
|
1424
|
+
When a digest is due, its tick prompt lists a bounded, oldest-first set of
|
|
1425
|
+
owed material events and deferred escalations. Each line includes its ledger id.
|
|
1426
|
+
The prompt gives the exact handoff shape:
|
|
1427
|
+
|
|
1428
|
+
```bash
|
|
1429
|
+
omp-conductor report \
|
|
1430
|
+
--kind digest \
|
|
1431
|
+
--events EVENT_ID_1,EVENT_ID_2 \
|
|
1432
|
+
--notices NOTICE_ID_1,NOTICE_ID_2 \
|
|
1433
|
+
--text "<the whole digest>"
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
Remove the id of any row you did not use. Omitted rows stay owed. The report row
|
|
1437
|
+
and the named ledger rows are associated in one SQLite transaction. If the
|
|
1438
|
+
handoff fails, no row is consumed. If a daily report deduplicates against a
|
|
1439
|
+
daily report already queued that day, newly named rows also stay owed. If
|
|
1440
|
+
delivery exhausts its retry budget and the report becomes `failed`, its rows
|
|
1441
|
+
return to the owed backlog, where a replacement digest can claim them.
|
|
1442
|
+
`omp-conductor status` always shows the
|
|
1443
|
+
material-event and held-escalation backlog counts, including the age of the
|
|
1444
|
+
oldest row when one exists.
|
|
1445
|
+
|
|
1446
|
+
This accumulator does not poll GitHub and does not infer outcomes from tracker
|
|
1447
|
+
state. The orchestrator still decides what is material and records the evidence.
|
|
1448
|
+
The mechanism only makes that decision durable until a non-failed digest owns it.
|
|
1449
|
+
|
|
1301
1450
|
Authorship still needs judgement the daemon does not have, so it stays with the
|
|
1302
1451
|
model. Delivery does not, so it moved:
|
|
1303
1452
|
|
|
1304
1453
|
```bash
|
|
1305
|
-
omp-conductor report --text "<the whole report>"
|
|
1306
|
-
omp-conductor report
|
|
1454
|
+
omp-conductor report --text "<the whole report>" # immediate report, when policy permits
|
|
1455
|
+
omp-conductor report \
|
|
1456
|
+
--text "<the whole digest>" --kind digest \
|
|
1457
|
+
--events EVENT_IDS --notices NOTICE_IDS # use row ids from its tick
|
|
1307
1458
|
```
|
|
1308
1459
|
|
|
1309
|
-
The command
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1460
|
+
The command persists the text before anything is sent and prints a durable
|
|
1461
|
+
handoff id. An immediate report admitted during quiet hours is stored as a held
|
|
1462
|
+
notice and prints that id; the daemon includes it in the next digest or in a
|
|
1463
|
+
catch-up report when the configured window opens. Otherwise it writes a
|
|
1464
|
+
`reports` row and prints its report id. Both survive the session being
|
|
1465
|
+
compacted, interrupted or restarted,
|
|
1466
|
+
and the daemon being restarted under it. The daemon delivers over the same bot
|
|
1467
|
+
token tier 2 uses, with bounded retries, and `omp-conductor status` lists
|
|
1468
|
+
anything it still owes. If availability closes after a material report was
|
|
1469
|
+
queued but before its first attempt, the outbox atomically converts that row to
|
|
1470
|
+
the same held-notice path instead of leaking the update through quiet hours.
|
|
1315
1471
|
|
|
1316
1472
|
### Delivery is at-least-once, and the docs will not pretend otherwise
|
|
1317
1473
|
|
|
@@ -1374,14 +1530,16 @@ then the only surface — a report has no tracker issue, so there is no issue
|
|
|
1374
1530
|
comment to fall back to. The page goes through the ordinary escalation ledger and
|
|
1375
1531
|
carries the report id, so one undeliverable report pages exactly once.
|
|
1376
1532
|
|
|
1377
|
-
###
|
|
1533
|
+
### Daily digests are deduplicated from the ledger
|
|
1378
1534
|
|
|
1379
|
-
`--kind digest` is accepted at most once per
|
|
1380
|
-
second hand-over on the same day is refused and
|
|
1381
|
-
the slot, including when that report has already
|
|
1382
|
-
from the `reports` table, not from the model's
|
|
1383
|
-
restarted or compacted session cannot send a second
|
|
1384
|
-
|
|
1535
|
+
With `digest.cadence: "daily"`, `--kind digest` is accepted at most once per
|
|
1536
|
+
**local** day, per project. The second hand-over on the same day is refused and
|
|
1537
|
+
told which report already holds the slot, including when that report has already
|
|
1538
|
+
been delivered. This is decided from the `reports` table, not from the model's
|
|
1539
|
+
memory of the last tick — a restarted or compacted session cannot send a second
|
|
1540
|
+
daily digest by forgetting the first. A `per-tick` digest carries no daily key,
|
|
1541
|
+
so later ticks can hand off newly accumulated rows. Material reports carry no
|
|
1542
|
+
dedupe key either: two events in a day are two events.
|
|
1385
1543
|
|
|
1386
1544
|
### What `status` shows
|
|
1387
1545
|
|
|
@@ -1439,9 +1597,17 @@ conditions, each one something this package can check without asking you:
|
|
|
1439
1597
|
| `rate-limit-reset:github` | GraphQL quota on `github` has any remaining capacity again. |
|
|
1440
1598
|
|
|
1441
1599
|
The daemon evaluates them beside each tick, fire-and-forget: a hanging registry
|
|
1442
|
-
costs one unevaluated condition, never the tick. A
|
|
1443
|
-
|
|
1444
|
-
|
|
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.
|
|
1445
1611
|
|
|
1446
1612
|
Anything else exits `2` and lists the six forms. An unparseable condition on an
|
|
1447
1613
|
existing row is *listed and never treated as met*: a grammar a future release
|
|
@@ -1477,6 +1643,7 @@ persists the class on the row, and performs the one recovery that class names.
|
|
|
1477
1643
|
| --- | --- | --- | --- |
|
|
1478
1644
|
| `env-start-failure` | turn 0 plus an explicit harness start error (`No model selected`, a rejected key) | escalate — the session never read the issue | none |
|
|
1479
1645
|
| `settlement-stuck` | a row carrying a PR that has since merged | settle: release the label, mark the row merged | none |
|
|
1646
|
+
| `returned-for-revision` | a `pushed-green` or `pushed-pending` PR was closed without merging | none — preserve the review decision for a human re-queue | continuation |
|
|
1480
1647
|
| `merge-conflict` | `pushed-green`, PR open, GitHub reports conflicting | requeue for a rebase continuation | continuation |
|
|
1481
1648
|
| `question` | the worker stopped to ask something (`blocked`) | escalate, carrying the worker's own report as evidence | none |
|
|
1482
1649
|
| `orphan-dirty` | orphaned with a failed salvage and no operator ack | hold — recorded only; the tree is the only copy | none |
|
|
@@ -1486,7 +1653,9 @@ persists the class on the row, and performs the one recovery that class names.
|
|
|
1486
1653
|
| `admin-kill` | killed *below* its own ceiling — a restart or a drain | requeue | none |
|
|
1487
1654
|
| `ci-infra` | PR open, every unresolved check cancelled / timed out / stale | re-run the failed jobs | none |
|
|
1488
1655
|
| `ci-deterministic` | PR open, a check genuinely reports `FAILURE` | escalate with the failing check names and links | failed attempt |
|
|
1656
|
+
| `dispatch-infra` | the conductor's own Git path failed before the worker's first turn | requeue, bounded by per-class strikes | none |
|
|
1489
1657
|
| `provider-credit` | the provider refused the run for credit (HTTP 402, or its own out-of-credit text read off the transcript) | pause the fleet and require `omp-conductor resume` once the provider has credit | none |
|
|
1658
|
+
| `provider-transient` | the provider aborted a request stream before the run produced a verdict | requeue, bounded by per-class strikes | none |
|
|
1490
1659
|
| `unknown` | anything unrecognised | escalate | as recorded |
|
|
1491
1660
|
|
|
1492
1661
|
**Unknown escalates; it never silently retries.** A shape this table does not
|
|
@@ -1496,13 +1665,14 @@ cause nobody has named — the behaviour this exists to end.
|
|
|
1496
1665
|
### The budgets follow the cause
|
|
1497
1666
|
|
|
1498
1667
|
`failuresFor` (implementation attempts) excludes `ci-infra`, `settlement-stuck`,
|
|
1499
|
-
`env-start-failure`
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1668
|
+
`env-start-failure`, `dispatch-infra`, `provider-credit`, `provider-transient`
|
|
1669
|
+
and `returned-for-revision`. `continuationsFor` excludes `admin-kill`,
|
|
1670
|
+
`settlement-stuck`, `env-start-failure`, `dispatch-infra`, `provider-credit`
|
|
1671
|
+
and `provider-transient`, but explicitly counts a failed
|
|
1672
|
+
`returned-for-revision` row. Environment, dispatch and provider faults charge
|
|
1673
|
+
neither budget because the issue did not receive a valid implementation
|
|
1674
|
+
attempt. A merge conflict and a returned review both charge a continuation:
|
|
1675
|
+
each asks for more work, but neither is a failed implementation attempt.
|
|
1506
1676
|
|
|
1507
1677
|
An **unclassified** row (every row written before 0.4.3) counts exactly as it
|
|
1508
1678
|
did before classification existed. Upgrading therefore changes no existing
|
|
@@ -1562,7 +1732,22 @@ The file is validated on every read. A malformed config produces one readable er
|
|
|
1562
1732
|
listing every fault, and the daemon refuses to start rather than running with half
|
|
1563
1733
|
a project.
|
|
1564
1734
|
|
|
1565
|
-
|
|
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
|
|
1566
1751
|
it already knows it can rewrite one area of it without re-asking the rest — see
|
|
1567
1752
|
[Changing one setting](#changing-one-setting).
|
|
1568
1753
|
|
|
@@ -1628,6 +1813,7 @@ A complete, valid config for one project with two target repos:
|
|
|
1628
1813
|
"workerModel": "smol",
|
|
1629
1814
|
"escalation": {
|
|
1630
1815
|
"telegramChatId": "123456789",
|
|
1816
|
+
"telegramTopicId": 8713,
|
|
1631
1817
|
"fallbackToIssueComment": true,
|
|
1632
1818
|
"orchestrator": "embedded"
|
|
1633
1819
|
},
|
|
@@ -1686,7 +1872,7 @@ Field notes:
|
|
|
1686
1872
|
| `escalation.orchestrator` | Optional; `"embedded"` (default) or `"external"`. `external` means an orchestrator session already runs elsewhere: the daemon starts none, and tier-1 escalations post as issue comments for that session to drain. Any other value is an error. |
|
|
1687
1873
|
| `authority` | Optional; `{ "merge": …, "release": … }`, each `"human"` (default) or `"orchestrator"`. It grants nothing to the daemon — it words the orchestrator's standing orders and the Releases paragraph of the rendered brief, so the config and the prompt cannot disagree about who holds the merge button. Unknown keys and any other value are errors, never folded to the default. |
|
|
1688
1874
|
| `releasePolicy` | Optional; `"none"` (default) or `"operator-brief"`. `none` installs a pre-tool-call tripwire in worker, embedded-orchestrator and external-orchestrator sessions. It blocks `git tag`, tag pushes, package publishing, GitHub release creation and recognised deploy commands before execution. `operator-brief` opens that gate only for the procedure in the operator-owned brief. Unknown values are errors. Every rejection is written to `release-policy-blocks.jsonl`; the heartbeat carries that day's count into the daily digest so configured intent and observed behaviour cannot drift silently. This is the mechanical gate; `authority.release` still says who owns the decision. |
|
|
1689
|
-
| `reporting` | Optional; a **legacy scope preset** (`reporting.scope` — `"material"` default, `"decisions"`, `"escalations"`) or the **explicit form** `{ "interruptOn": [...], "digest": { ... } }`. The preset writes which categories may page the operator (`interruptOn`) and when the rollup happens (`digest.cadence`); the explicit form sets both directly and
|
|
1875
|
+
| `reporting` | Optional; a **legacy scope preset** (`reporting.scope` — `"material"` default, `"decisions"`, `"escalations"`) or the **explicit form** `{ "interruptOn": [...], "digest": { ... }, "availability": { ... } }`. The preset writes which categories may page the operator (`interruptOn`) and when the rollup happens (`digest.cadence`); the explicit form sets both directly and may add a weekly operator-availability window. The two forms are mutually exclusive in one config. See [Reporting policy](#reporting-policy-reporting). |
|
|
1690
1876
|
| `orchestratorReadPaths` | **Retired in 0.4.3.** Still accepted in a config and ignored, so a fleet carrying it upgrades without an edit. It widened the orchestrator's file-tool allowlist; there is no allowlist any more — the orchestrator is [unconfined by design](#the-orchestrator-is-unconfined-deliberately). |
|
|
1691
1877
|
| `policy` | Optional; the gating conditions a merge or a release must satisfy, in two sections — `policy.merge` and `policy.release`. Any member may be omitted and the loader fills it from the strict default; an unknown key in either section, or a value outside its vocabulary, is an error naming the field, never a silent downgrade. See [Merge and release preconditions](#merge-and-release-preconditions-policy). |
|
|
1692
1878
|
| `workspaceRoot` / `mirrorRoot` | Optional; default to `worktrees/` and `mirrors/` under the state directory. `~` is expanded. |
|
|
@@ -1708,13 +1894,66 @@ spellings, mutually exclusive in one config (the loader rejects a `scope` next t
|
|
|
1708
1894
|
- **Explicit** — `reporting: { "interruptOn": ["tier2", "fleet-stopped", ...], "digest": { "cadence": "none" | "per-tick" | "daily" } }`.
|
|
1709
1895
|
`interruptOn` must be a non-empty array of known categories (`tier2`, `decision-needed`, `fleet-stopped`, `confirmed-failure`, `material`), each an escalation's tier-2 category. `daily` may add `at` (`HH:MM`, 24h) and `timezone` (a known IANA zone, defaulting to the host zone) — both only valid with `daily`.
|
|
1710
1896
|
|
|
1897
|
+
The explicit form may add a weekly local-time window:
|
|
1898
|
+
|
|
1899
|
+
```json
|
|
1900
|
+
{
|
|
1901
|
+
"reporting": {
|
|
1902
|
+
"interruptOn": ["tier2", "fleet-stopped"],
|
|
1903
|
+
"digest": { "cadence": "daily", "at": "17:00", "timezone": "Europe/London" },
|
|
1904
|
+
"availability": {
|
|
1905
|
+
"timezone": "Europe/London",
|
|
1906
|
+
"days": ["mon", "tue", "wed", "thu", "fri"],
|
|
1907
|
+
"start": "09:00",
|
|
1908
|
+
"end": "17:00",
|
|
1909
|
+
"bypass": ["fleet-stopped"]
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
```
|
|
1914
|
+
|
|
1915
|
+
`timezone` must be a known IANA zone. For a daily digest, its timezone defaults
|
|
1916
|
+
to this value and must match it when both are set.
|
|
1917
|
+
|
|
1918
|
+
`days` is a non-empty set of `mon` through `sun`; `start` is inclusive and
|
|
1919
|
+
`end` is exclusive. A start later than the end defines an overnight window on
|
|
1920
|
+
the day it opens. `bypass` is an explicit list of known interrupt categories
|
|
1921
|
+
that may still page outside the window; it may be empty. For ordinary notices,
|
|
1922
|
+
a bypass has no effect on a category omitted from `interruptOn`. Urgent recovery
|
|
1923
|
+
notices may bypass category batching when the digest loop itself is unavailable,
|
|
1924
|
+
but they still require the configured availability bypass outside the window.
|
|
1925
|
+
|
|
1926
|
+
The setup wizard offers this as **Weekly availability window** and asks for the
|
|
1927
|
+
zone, days, start/end, bypass categories, and digest schedule: every tick,
|
|
1928
|
+
model-timed daily, disabled, or a fixed daily `HH:MM`. Re-running setup or
|
|
1929
|
+
amending reporting preselects and preserves the configured `none`, `per-tick`,
|
|
1930
|
+
or `daily` cadence. Choosing **Continuous (24-hour interrupts)** is the explicit
|
|
1931
|
+
opt-out and preserves the behavior of every existing config; an absent
|
|
1932
|
+
`availability` key also means continuous operation.
|
|
1933
|
+
|
|
1934
|
+
Outside the window, an otherwise interruptible escalation is stored durably
|
|
1935
|
+
instead of sent. A daily digest may consume it first. Otherwise the daemon
|
|
1936
|
+
atomically queues one working-hours catch-up report when the window opens,
|
|
1937
|
+
including after downtime; associating the held rows before delivery prevents a
|
|
1938
|
+
later tick from authoring a duplicate. Each heartbeat prompt names the
|
|
1939
|
+
mechanically computed current mode and next transition. `status` shows the same
|
|
1940
|
+
state plus the next digest opportunity (`due now`, every tick, disabled, or its
|
|
1941
|
+
next operator-local timestamp). Config, escalation routing, and report transport
|
|
1942
|
+
are re-read at tick or send time, so changing the window or Telegram target
|
|
1943
|
+
does not require a daemon restart.
|
|
1944
|
+
|
|
1945
|
+
Attachment-bearing autonomous Telegram sends cannot be replayed by the text
|
|
1946
|
+
digest, so they are blocked with an explicit “nothing sent or held” error rather
|
|
1947
|
+
than silently dropping their files.
|
|
1948
|
+
|
|
1711
1949
|
A tier-2 escalation whose category is **not** in `interruptOn` is not dropped: it
|
|
1712
1950
|
is held (`held_notices`) and the next accepted digest is its delivery authority.
|
|
1713
|
-
|
|
1714
|
-
configured zone), which remains the delivery authority across restarts.
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1951
|
+
A `daily` digest is at-most-once per local day (`digest:<YYYY-MM-DD>` in the
|
|
1952
|
+
configured zone), which remains the delivery authority across restarts.
|
|
1953
|
+
`per-tick` digests are not daily-deduplicated, so a later tick can claim newly
|
|
1954
|
+
accumulated rows. A scheduled `daily` digest is only sent on a day it has not
|
|
1955
|
+
already run, once the local clock has passed `at`; a restart after `at` still
|
|
1956
|
+
sends today's (one catch-up), and a fully missed day is skipped, never sent late.
|
|
1718
1957
|
|
|
1719
1958
|
### Merge and release preconditions (`policy`)
|
|
1720
1959
|
|
|
@@ -1737,13 +1976,13 @@ policy instead of restating it — no threshold lives in two places.
|
|
|
1737
1976
|
|
|
1738
1977
|
| Field | Values | Default | Means |
|
|
1739
1978
|
| --- | --- | --- | --- |
|
|
1740
|
-
| `requires` | `runs-settled`, `no-open-prs`, `queue-drained`, `base-branch-green`, `epic-children-closed` | `["runs-settled"]` | What must already have landed. `base-branch-green` requires the
|
|
1979
|
+
| `requires` | `runs-settled`, `no-open-prs`, `queue-drained`, `base-branch-green`, `epic-children-closed` | `["runs-settled"]` | What must already have landed. `base-branch-green` requires the current live head's push-triggered workflow verdict for that routed repository to be green; pending, unknown, red, or no observation refuses release. Order and duplicates do not matter; the loader canonicalises. |
|
|
1741
1980
|
| `requiredChecks` | any check names | `[]` | Checks that must be green on the branch being released. Empty means every check it reports. |
|
|
1742
1981
|
| `artefacts` | any names | `[]` | The packages or images this project releases. **Empty denies**: nothing has been authorised to ship. |
|
|
1743
1982
|
| `environments` | any names | `[]` | Deploy targets. **Empty denies** every environment. |
|
|
1744
1983
|
|
|
1745
1984
|
A project with no `policy` block loads as the whole default above, which is the
|
|
1746
|
-
strictest reading of the prose it replaced.
|
|
1985
|
+
strictest reading of the prose it replaced. `omp-conductor setup` asks for all of
|
|
1747
1986
|
it under the **merge & release preconditions** area, so changing one condition
|
|
1748
1987
|
costs eight prompts rather than a hand-edit — see
|
|
1749
1988
|
[Changing one setting](#changing-one-setting).
|
|
@@ -1779,13 +2018,14 @@ it never gets prompted, so it never runs anything. Installing
|
|
|
1779
2018
|
`omp plugin install omp-conductor` also installs a heartbeat that prompts it.
|
|
1780
2019
|
|
|
1781
2020
|
The heartbeat is **inert unless the session cwd contains
|
|
1782
|
-
`.conductor-tick.json`**, so an ordinary session has no timer.
|
|
2021
|
+
`.conductor-tick.json`**, so an ordinary session has no timer. `omp-conductor setup`
|
|
1783
2022
|
writes this file for external orchestration. A manual configuration has this form:
|
|
1784
2023
|
|
|
1785
2024
|
```json
|
|
1786
2025
|
{
|
|
1787
2026
|
"intervalSeconds": 900,
|
|
1788
|
-
"
|
|
2027
|
+
"project": "fleet",
|
|
2028
|
+
"armedFile": "/home/fleet/.omp/conductor/armed-fleet",
|
|
1789
2029
|
"accessFile": "/home/fleet/.omp/agent/telegram/access.json",
|
|
1790
2030
|
"message": "Run your standing loop from ORCHESTRATOR.md now."
|
|
1791
2031
|
}
|
|
@@ -1794,16 +2034,71 @@ writes this file for external orchestration. A manual configuration has this for
|
|
|
1794
2034
|
| Key | Required | Default | Notes |
|
|
1795
2035
|
| --- | --- | --- | --- |
|
|
1796
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. |
|
|
1797
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. |
|
|
1798
|
-
| `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. |
|
|
1799
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. |
|
|
1800
|
-
| `message` | no | `Tick <ISO timestamp>: re-read <workspaceRoot>/ORCHESTRATOR.md from disk, then run your standing loop from it.`, then the
|
|
1801
|
-
| `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,
|
|
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. |
|
|
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
|
+
```
|
|
1802
2081
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
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.
|
|
2096
|
+
|
|
2097
|
+
A default tick sends one message (`customType` `omp-conductor.tick`, attributed
|
|
2098
|
+
to the user): the standing-loop prompt, the reporting-policy constraint re-read
|
|
2099
|
+
from conductor config on every tick, the delivery rule, and the mechanically
|
|
2100
|
+
computed operator-availability state. A configured `message` replaces the first
|
|
2101
|
+
three parts but not that clock state. The delivery rule is there
|
|
1807
2102
|
because end-of-turn text reaches the operator's Telegram only on a turn that
|
|
1808
2103
|
*began* as an inbound Telegram message: a tick is injected locally, so anything
|
|
1809
2104
|
the session merely writes at the end of one is read by nobody, and a reportable
|
|
@@ -1836,9 +2131,11 @@ registered agent name: the heartbeat asks `herdr agent list` for the entry whose
|
|
|
1836
2131
|
is the *session* — every pane in it shares `HERDR_SESSION` and the cwd — and
|
|
1837
2132
|
herdr's `agent` field is the *runtime*, `omp` for the orchestrator and for the
|
|
1838
2133
|
shell beside it, so neither can tell them apart. The registered name can, it is
|
|
1839
|
-
what `herdr agent start fleet --kind omp --pane <id>` sets
|
|
1840
|
-
|
|
1841
|
-
|
|
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.
|
|
1842
2139
|
|
|
1843
2140
|
**Without herdr**, the session claims the directory in a sibling
|
|
1844
2141
|
`.conductor-tick-owner.json` (pid, session file, claim time) and ticks only while
|
|
@@ -1900,8 +2197,8 @@ that happens to have a `.conductor-tick.json` has no bridge to check. It is not
|
|
|
1900
2197
|
off switch for the check: **a fleet deploy always sets it.**
|
|
1901
2198
|
|
|
1902
2199
|
Every tick — sent or skipped — is logged with its reason (`not armed`,
|
|
1903
|
-
`escalation channel down`, `tick already pending`) to the omp log.
|
|
1904
|
-
|
|
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*
|
|
1905
2202
|
claiming work, and the tick drives a different session — one whose duties
|
|
1906
2203
|
(grooming the queue, draining escalations, reporting) are exactly what stays
|
|
1907
2204
|
useful while dispatch is stopped. Its own off switch is the arm marker. Skips
|
|
@@ -1987,6 +2284,9 @@ least of all on a fleet whose session lives somewhere else.
|
|
|
1987
2284
|
## CLI reference
|
|
1988
2285
|
|
|
1989
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]
|
|
1990
2290
|
omp-conductor start [--port N] [--project NAME]
|
|
1991
2291
|
omp-conductor --version
|
|
1992
2292
|
omp-conductor stop
|
|
@@ -1996,10 +2296,9 @@ omp-conductor status [--project NAME]
|
|
|
1996
2296
|
omp-conductor ledger [--issue N] [--limit N] [--project NAME]
|
|
1997
2297
|
omp-conductor board [--project NAME]
|
|
1998
2298
|
omp-conductor hold [--project NAME]
|
|
1999
|
-
omp-conductor
|
|
2299
|
+
omp-conductor stop [--pane] [--project NAME]
|
|
2000
2300
|
omp-conductor arm [--project NAME]
|
|
2001
2301
|
omp-conductor disarm [--project NAME]
|
|
2002
|
-
omp-conductor release-pane [--project NAME]
|
|
2003
2302
|
omp-conductor tail <issue> [--project NAME]
|
|
2004
2303
|
omp-conductor extend <issue> --turns N [--project NAME]
|
|
2005
2304
|
omp-conductor worker pause <issue> [--project NAME]
|
|
@@ -2008,41 +2307,42 @@ omp-conductor worker stop <issue> --reason TEXT [--project NAME]
|
|
|
2008
2307
|
omp-conductor unblock <issue> [--force] [--no-requeue] [--project NAME]
|
|
2009
2308
|
omp-conductor verb <conductor_*> [--project NAME] [--arg k=v ...]
|
|
2010
2309
|
omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
|
|
2011
|
-
omp-conductor
|
|
2310
|
+
omp-conductor event record --category NAME --summary TEXT --evidence REF [--occurred-at ISO] [--project NAME]
|
|
2311
|
+
omp-conductor report --text TEXT [--kind material|digest] [--events IDS] [--notices IDS] [--project NAME]
|
|
2012
2312
|
omp-conductor decision open --question TEXT [--blocks TEXT] [--resolves-when COND] [--project NAME]
|
|
2013
2313
|
omp-conductor decision resolve <id> --answer TEXT [--project NAME]
|
|
2014
2314
|
omp-conductor decision withdraw <id> [--reason TEXT] [--project NAME]
|
|
2015
2315
|
omp-conductor decision list [--project NAME]
|
|
2016
2316
|
omp-conductor daemon [--once] [--port N] [--project NAME]
|
|
2017
|
-
omp-conductor
|
|
2018
|
-
omp-conductor resume
|
|
2019
|
-
omp-conductor graph-setup [--project NAME] [--write]
|
|
2317
|
+
omp-conductor resume [--project NAME]
|
|
2020
2318
|
omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
|
|
2021
2319
|
omp-conductor help
|
|
2022
2320
|
```
|
|
2023
2321
|
|
|
2024
2322
|
| Command | Behaviour |
|
|
2025
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). |
|
|
2026
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`. |
|
|
2027
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. |
|
|
2028
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). |
|
|
2029
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. |
|
|
2030
|
-
| `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. |
|
|
2031
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`. |
|
|
2032
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. |
|
|
2033
|
-
| `hold [--project NAME]` | Soft stop: pause claiming **and** disarm ticks. Daemon and pane stay up. Prefer this
|
|
2034
|
-
| `
|
|
2035
|
-
| `arm [--project NAME]` | Proof-gated: send a Telegram challenge and write
|
|
2036
|
-
| `disarm [--project NAME]` | Remove
|
|
2037
|
-
| `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. |
|
|
2038
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>`. |
|
|
2039
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. |
|
|
2040
|
-
| `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 `
|
|
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. |
|
|
2041
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. |
|
|
2042
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. |
|
|
2043
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`. |
|
|
2044
2344
|
| `friction <kind> --detail TEXT [--issue N]` | Record one bounded judgment the daemon cannot infer: an escalation belonged in a digest, or a tick report was noise/surprising. The detail is limited to 160 characters. One event never changes policy; three observations inside seven days make the aggregate eligible for one Learning-loop prompt, followed by a seven-day cooldown. |
|
|
2045
|
-
| `report --text TEXT [--kind material|digest]` | Hand a rendered report to the daemon's durable outbox. The
|
|
2345
|
+
| `report --text TEXT [--kind material|digest]` | Hand a rendered report to the daemon's durable outbox. The command persists the text **before** anything can send and prints a durable handoff id. A material report submitted during quiet hours becomes a held notice until the window opens; otherwise it becomes a report whose delivery the daemon owns, retries with bounded backoff, and records. Delivery is [at-least-once](#report-delivery-the-outbox), so a crash mid-send is retried as a possible repeat and `delivered` never proves exactly one message. `--kind digest` is accepted at most once per local day, decided from the ledger; an unknown `--kind` exits `2`. Anything still owed appears in `status` with its age. |
|
|
2046
2346
|
| `decision open --question TEXT [--blocks TEXT] [--resolves-when COND]` | Record a question the orchestrator has put to you, and print its id. A question that lives only in a session's context is lost at the next compaction — after which it is either asked twice or dropped silently. `--resolves-when` attaches a machine-checkable condition: `pr-merged:<https url>`, `pr-checks-green:<https url>`, `pr-mergeable:<https url>`, `issue-closed:<n>`, `npm-version:<pkg>@<version>`, or `rate-limit-reset:github`; anything else exits `2` listing the six forms. See [The decision ledger](#the-decision-ledger-resolves-when). |
|
|
2047
2347
|
| `decision resolve <id> --answer TEXT` | Record what you decided. Exits `1` naming the id when it is unknown or no longer open, so a second answer cannot overwrite the first. |
|
|
2048
2348
|
| `decision withdraw <id> [--reason TEXT]` | Close a question the session stopped needing, with why. Same guard as `resolve`. |
|
|
@@ -2050,12 +2350,10 @@ omp-conductor help
|
|
|
2050
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. |
|
|
2051
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. |
|
|
2052
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. |
|
|
2053
|
-
| `--project NAME` |
|
|
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. |
|
|
2054
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. |
|
|
2055
|
-
| `resume` | Clear pause
|
|
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. |
|
|
2056
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. |
|
|
2057
|
-
| `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). |
|
|
2058
|
-
| `--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. |
|
|
2059
2357
|
| `brief-upgrade` | Inspect the package-floor + `POLICY.md` overlay. Reports by default; see [Keeping a brief current](#keeping-a-brief-current). |
|
|
2060
2358
|
| `--migrate` | Only for `brief-upgrade`. Lift a bannered `ORCHESTRATOR.md` owned half into `POLICY.md` and recompose. Dry-run unless `--apply`. |
|
|
2061
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. |
|
|
@@ -2063,18 +2361,18 @@ omp-conductor help
|
|
|
2063
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. |
|
|
2064
2362
|
| `help`, `--help`, `-h` | Print usage. An unknown or missing verb prints it too, and exits `2`. |
|
|
2065
2363
|
|
|
2066
|
-
Pause is a
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
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.
|
|
2072
2371
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
`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.
|
|
2078
2376
|
|
|
2079
2377
|
### Health endpoint
|
|
2080
2378
|
|
|
@@ -2085,50 +2383,58 @@ curl -s localhost:8787/healthz
|
|
|
2085
2383
|
```json
|
|
2086
2384
|
{
|
|
2087
2385
|
"ok": true,
|
|
2088
|
-
"paused": false,
|
|
2089
|
-
"activeRuns": 1,
|
|
2090
|
-
"project": "demo",
|
|
2091
2386
|
"rssBytes": 123456789,
|
|
2092
|
-
"
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
{
|
|
2109
|
-
"
|
|
2110
|
-
"
|
|
2111
|
-
"
|
|
2112
|
-
"
|
|
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"]
|
|
2113
2424
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
"refresh": {
|
|
2117
|
-
"result": "success",
|
|
2118
|
-
"fresh": true,
|
|
2119
|
-
"lastSuccessAt": "2026-08-08T12:50:00.000Z",
|
|
2120
|
-
"ageMs": 600000
|
|
2121
|
-
},
|
|
2122
|
-
"reasons": ["worker MCP configuration does not mount the indexer"]
|
|
2123
|
-
}
|
|
2425
|
+
}
|
|
2426
|
+
]
|
|
2124
2427
|
}
|
|
2125
2428
|
```
|
|
2126
2429
|
|
|
2127
|
-
Any other path or method returns `404`. `ok`
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
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.
|
|
2132
2438
|
|
|
2133
2439
|
## What a worker may and may not do
|
|
2134
2440
|
|
|
@@ -2250,8 +2556,8 @@ daemon, across a process boundary, not in a prompt the model can rewrite.
|
|
|
2250
2556
|
| --- | --- | --- |
|
|
2251
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. |
|
|
2252
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`. |
|
|
2253
|
-
| `conductor_pr_status` | worker or orchestrator | Read-only. A worker reads only its own run's PR. |
|
|
2254
|
-
| `conductor_pr_update_branch` | orchestrator, or the worker owning the run | The PR belongs to this
|
|
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. |
|
|
2255
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. |
|
|
2256
2562
|
| `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. |
|
|
2257
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. |
|
|
@@ -2261,6 +2567,14 @@ role must equal the configured holder.** `authority` has exactly two values, so
|
|
|
2261
2567
|
a `!== "human"` test would have let a *worker* release. A worker is refused
|
|
2262
2568
|
every release shape under the most permissive config there is.
|
|
2263
2569
|
|
|
2570
|
+
For Git-backed releases, `git-tag` is idempotent when the named tag exists
|
|
2571
|
+
locally but has not been pushed: it re-points the tag to the verified live
|
|
2572
|
+
default-branch head. `git-push-tags` performs the same re-point immediately
|
|
2573
|
+
before pushing if the default branch moved between the two calls. A tag already
|
|
2574
|
+
published on origin is immutable: an identical tag is accepted as already
|
|
2575
|
+
complete, while a different published target is refused and must use a new tag
|
|
2576
|
+
name.
|
|
2577
|
+
|
|
2264
2578
|
### The transport
|
|
2265
2579
|
|
|
2266
2580
|
Identity is never an argument. `project`, `run`, `issue` and the caller's role
|
|
@@ -2392,8 +2706,9 @@ Known and deliberate in this version:
|
|
|
2392
2706
|
was spent.
|
|
2393
2707
|
- **GitHub is the only tracker.** The internal `Tracker` port is deliberately
|
|
2394
2708
|
provider-neutral, but `tracker.kind` accepts only `"github"` today.
|
|
2395
|
-
- **One project
|
|
2396
|
-
|
|
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.
|
|
2397
2712
|
- **Labels are matched exactly and case-sensitively.** `Ready-For-Agent` is not
|
|
2398
2713
|
`ready-for-agent`, and the mismatch is silent: the issue is simply never picked
|
|
2399
2714
|
up.
|