omp-conductor 0.3.18 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,11 +18,11 @@ authority are chosen during setup and default to `human`. Granting either action
18
18
  to the orchestrator never grants it to a worker or the dispatch daemon. Releases
19
19
  remain batched from coherent groups of merged work, never cut one per worker PR.
20
20
 
21
- Code counts every limit that decides whether work starts: concurrency, dollars per
22
- day, turns and wall clock per worker, attempts per issue. None of it is left to the
23
- model. A worker asked to respect a budget eventually talks itself out of it, so the
24
- dispatcher enforces the budget before anything is claimed and kills anything over
25
- the line.
21
+ Code counts every limit that decides whether work starts: concurrency, dollars
22
+ per day, turns and wall clock per worker, failed implementation attempts, and
23
+ operational continuations per issue. None is left to the model. The dispatcher
24
+ enforces these budgets before anything is claimed and kills workers that exceed
25
+ their per-run limits.
26
26
 
27
27
  When a run does get stuck, the first responder is not you. A tier-1 escalation is
28
28
  injected into a long-lived **orchestrator session** that can read the issue and the
@@ -39,7 +39,7 @@ The package ships three deployables, plus two skills:
39
39
  | Standalone daemon | `omp-conductor` binary | The dispatch loop, managed as a background process (`start` / `stop` / `restart`) with a `/healthz` endpoint for a supervisor. |
40
40
  | Orchestrator heartbeat | omp extension, activated by `.conductor-tick.json` | Prompts a 24/7 orchestrator session on a fixed interval so its standing loop actually runs, and marks the session stalled when its prompts stop being consumed. Inert in every other session — including a second session opened in the fleet's own directory. See [Orchestrator tick](#orchestrator-tick). |
41
41
  | Onboarding skill | `skill://conductor-onboarding` | Directs an omp session to interview you, read your repos for real CI gates, and tailor `ORCHESTRATOR.md` — then finish through the wizard. Discovered automatically once the plugin is installed. See [Onboarding](#onboarding). |
42
- | Update skill | `skill://conductor-update` | Treats the Bun-global CLI, omp plugin, and Herdr plugin as one operation: pause claims, drain, install one pinned release, reload, verify twice, and restore the prior pause state. See [Updating](#updating). |
42
+ | Update skill | `skill://conductor-update` | Thin natural-language wrapper over the deterministic `omp-conductor upgrade` command. See [Updating](#updating). |
43
43
 
44
44
  The first two are thin wrappers over the same `daemon.ts`, so the plugin and the
45
45
  CLI cannot disagree about what a cap means or where the state lives. Claiming is
@@ -134,8 +134,11 @@ number of code repos, and both label names are yours to configure.
134
134
  omp plugin install omp-conductor
135
135
  ```
136
136
 
137
- From a checkout of the monorepo, `./setup.sh` links this plugin and the Herdr half
138
- after checking everything below.
137
+ From a checkout of the monorepo, `./setup.sh` checks both plugins. It preserves
138
+ an existing npm-managed `omp-conductor` and links only the Herdr half, so running
139
+ setup on a release-based fleet cannot silently switch omp to mutable source.
140
+ `./setup.sh install --force-link` is the explicit opt-in to link both checkout
141
+ directories.
139
142
 
140
143
  ### Prerequisites
141
144
 
@@ -168,34 +171,53 @@ Also required on the host:
168
171
 
169
172
  ## Updating
170
173
 
171
- Say “update conductor” from an operator shell or maintenance omp session outside
172
- the target `herdr-fleet.service`. That is the whole operator interface. The bundled
173
- `skill://conductor-update` discovers the installed and registry versions, pauses
174
- new claims while active work drains, and pins one release across the Bun-global
175
- CLI, omp plugin, and Herdr plugin. It converts an old local Herdr link to a managed
176
- checkout when necessary, reloads the Herdr service and dispatch daemon, verifies
177
- the layered status twice, and restores the original dispatch state.
174
+ Run one command from a shell outside the target `herdr-fleet.service`:
178
175
 
179
- Ticks remain in their existing armed or disarmed state, so an ordinary update
180
- does not halt the exact pane or require another Telegram arm challenge. Any
181
- installation, reload, or verification failure leaves dispatch paused instead of
182
- bringing up a mixed fleet. The skill does not publish npm or edit an install root.
183
- It also refuses to run from the fleet pane that Herdr must restart: an updater
184
- that kills itself cannot verify the result. With skill commands enabled, invoke
185
- it directly with:
186
-
187
- ```text
188
- /skill:conductor-update
176
+ ```bash
177
+ omp-conductor upgrade
189
178
  ```
190
179
 
180
+ It resolves the latest published npm release, pauses new claims, drains active
181
+ workers, and pins that exact release across the Bun-global CLI, omp plugin, and
182
+ Herdr plugin. It also recomposes the conductor-owned brief floor, restarts Herdr
183
+ and the daemon, waits for pane recovery, verifies the installed identities and
184
+ layered fleet status twice, then restores the original dispatch state.
185
+
186
+ Use `omp-conductor upgrade --to X.Y.Z` for an explicit published version. The
187
+ command exits without changing anything when all three surfaces already use that
188
+ release, the Herdr plugin is pinned to its exact `gitHead`, and the brief is
189
+ current.
190
+
191
+ When a daemon is live, the command drains and restarts its recorded project.
192
+ An explicit `--project` that names a different project is rejected before pause
193
+ or installation, so an operator cannot drain one queue and kill another queue's
194
+ workers.
195
+
196
+ Ticks remain in their existing armed or disarmed state, so an ordinary update
197
+ does not halt the exact pane or require another Telegram arm challenge. Progress
198
+ names the Bun-global CLI, omp plugin, Herdr plugin, brief, reloads, and both
199
+ verification passes separately.
200
+
201
+ An installation, brief, reload, or verification failure pauses dispatch and
202
+ attempts to restore the exact CLI/plugin identities that were present before the
203
+ command. If rollback also fails, the error names every failed restoration and
204
+ keeps dispatch paused; it never brings a known mixed fleet back into service.
205
+ The command never publishes npm, edits an install root, or delegates lifecycle
206
+ steps to an AI session. It refuses to run inside a Herdr-managed session because
207
+ an updater that restarts itself cannot verify the result.
208
+
209
+ The bundled `skill://conductor-update` remains available as a natural-language
210
+ entry point, but it only runs this command; the lifecycle is implemented and
211
+ verified by the CLI.
212
+
191
213
  ## Onboarding
192
214
 
193
215
  Onboarding this package has two layers, and installing it gives you both.
194
216
 
195
217
  | Layer | What it is | What it owns |
196
218
  | --- | --- | --- |
197
- | **`/conductor setup`** | The deterministic wizard. Closed questions, a label plan, a dry run, one confirm. On a project it already knows, it offers to amend one area instead of re-asking everything see [Changing one setting](#changing-one-setting). | **Mechanical config.** It is the only thing that writes `config.json`, and it mutates nothing before you confirm. |
198
- | **`skill://conductor-onboarding`** | A skill bundled in this package (`skills/conductor-onboarding/SKILL.md`), discovered automatically by any omp session once the plugin is installed. | **Brief authoring.** The judgement the wizard cannot prompt for. |
219
+ | **`/conductor setup`** | The deterministic wizard. It shows the label, runtime, and dry-run plans before one consent step. A configured project can amend one area. | **Mechanical setup.** It writes `config.json`, labels, the brief, an external heartbeat config, and a staged systemd unit. It also completes the paused smoke and arm gates. |
220
+ | **`skill://conductor-onboarding`** | A skill bundled in this package (`skills/conductor-onboarding/SKILL.md`). Any omp session can discover it after plugin installation. | **Brief authoring.** It supplies the judgment that fixed prompts cannot. |
199
221
 
200
222
  The split exists because the two halves fail differently. A wrong config value is
201
223
  a run that errors on the next tick; a wrong release boundary is a fleet that
@@ -313,75 +335,113 @@ omp-conductor brief-upgrade --retrofit --apply
313
335
  ```
314
336
 
315
337
  - **Overlay already active** (`POLICY.md` present): protocol updates need no brief-upgrade.
316
- - **Legacy bannered brief**: `--migrate` lifts the owned half into `POLICY.md` and recomposes, with backups.
338
+ - **Legacy bannered brief**: `--migrate` lifts the owned half into `POLICY.md`
339
+ and recomposes. Previous brief and policy versions go to
340
+ `$OMP_CONDUCTOR_HOME/backups/briefs/` (default
341
+ `~/.omp/conductor/backups/briefs/`), named with their source filename and
342
+ timestamp.
317
343
  - **Hand-written brief** (no banner): `--retrofit` inserts the banner before the first Releases / Project context / Reporting / Amendments heading; then `--migrate`.
318
344
  - **Legacy `--apply`**: still merges a bannered single-file brief when you need the old path.
345
+ - **Existing sidecars**: a composed refresh relocates conductor-generated
346
+ `ORCHESTRATOR.md.bak-<timestamp>` and `POLICY.md.bak-<timestamp>` files into
347
+ that backup directory. Other `.bak` files stay untouched.
319
348
 
320
349
  `--file PATH` checks a brief that is not where the wizard would have put it.
321
350
 
322
- The **Learning loop** proposes diffs against `POLICY.md` for you to approve over Telegram.
351
+ The **Learning loop** proposes diffs against `POLICY.md` for you to approve over
352
+ Telegram. It also learns from repeated operational friction. The daemon
353
+ automatically rolls up repairable admission holds; the orchestrator records
354
+ judgments code cannot make with:
355
+
356
+ ```bash
357
+ omp-conductor friction escalation-digest --detail "routine retry belonged in the digest" [--issue N]
358
+ omp-conductor friction report-noise --detail "green status repeated with no operator action"
359
+ omp-conductor friction report-surprise --detail "a material failure was missing from the report"
360
+ ```
361
+
362
+ Three observations within seven days make a bounded signal eligible for one
363
+ tick. After it is surfaced, that signal cools down for seven days. A signal is
364
+ evidence to investigate, never an automatic policy edit: the existing one-at-a-
365
+ time Telegram approval, `POLICY.md`-only edit, Hard-boundary prohibition, and
366
+ **Amendments** log still apply.
323
367
 
324
368
  ## Quick start
325
369
 
326
- 1. Write a config (see [Configuration](#configuration)) at
327
- `~/.omp/conductor/config.json`.
328
- 2. From an omp session:
370
+ 1. Install `omp-conductor` and `omp-telegram`. Pair the Telegram bot and enable its bridge.
371
+ 2. Open the long-lived omp session in the fleet workspace.
372
+
373
+ The default workspace is `~/.omp/conductor/worktrees`. The wizard shows the actual path before it writes files.
374
+
375
+ 3. Run the wizard:
329
376
 
330
377
  ```text
331
378
  /conductor setup
332
379
  ```
333
380
 
334
- This is a **dry run first**. It reads the tracker through the same routing code
335
- the loop uses and prints exactly what the next tick would pick up, which repo
336
- each issue routes to, the branch it would cut, and every issue that cannot be
337
- routed. **Nothing is mutated** — no label written, no run row, no worktree —
338
- until you answer the "Arm omp-conductor?" confirmation. Arming does exactly two
339
- things: create the state database, and clear the pause flag. Declining leaves
340
- the machine untouched.
381
+ The wizard reads the tracker with the same routing code as the daemon. It shows every issue that the next tick can route.
382
+
383
+ Nothing changes before the consent step. After consent, setup does these actions:
384
+
385
+ - creates the required labels;
386
+ - writes `config.json` and the selected brief;
387
+ - writes `.conductor-tick.json` for external orchestration;
388
+ - stages `omp-conductor.service` under the conductor state directory;
389
+ - runs `daemon --once` while dispatch is paused;
390
+ - starts a temporary daemon and proves `/healthz` and stored status;
391
+ - stops the temporary daemon;
392
+ - proves the current inbound Telegram path before it arms or resumes an external heartbeat;
393
+ - clears the dispatch pause only after all required gates pass.
341
394
 
342
- Two of its questions are about you rather than the fleet: how loud the
343
- orchestrator should be (`reporting.scope`), and whether to write an
344
- `ORCHESTRATOR.md` you then own. See
345
- [Your workflow vs. the package](#your-workflow-vs-the-package). If you would
346
- rather be interviewed through those two, and have the brief tailored and your
347
- gates read out of your CI config, start from
348
- [Onboarding](#onboarding) instead.
395
+ If the arm proof cannot complete, setup keeps dispatch paused. The result shows the exact recovery commands.
349
396
 
350
- 3. Start the daemon in the background:
397
+ 4. On a systemd host, run the install commands that setup prints.
398
+
399
+ The commands install the staged unit, reload systemd, enable the unit, and restart it. The generated unit contains the current user, paths, project, port, and memory ceiling.
400
+
401
+ On a host without systemd, start the daemon directly:
351
402
 
352
403
  ```bash
353
404
  omp-conductor start
354
405
  ```
355
406
 
356
- `start` first starts `herdr-fleet.service` when that optional unit is installed,
357
- clearing a previous `halt --pane` recovery pin so Herdr can resume the exact
358
- conductor pane. Hosts without systemd or without that unit keep the standalone
359
- daemon behaviour. It then waits until the daemon actually answers
360
- `GET /healthz`; spawning is not starting. A daemon whose config is broken,
361
- whose port is taken or whose database is locked exits within a second, and the
362
- command fails with the tail of `daemon.log` instead of printing a false
363
- success. It refuses to start a second daemon, naming the live pid. Starting
364
- processes does not clear `pause` or arm ticks; those remain explicit operator
365
- decisions.
366
-
367
- Pane recovery spans two separately installed plugins: npm ships the omp
368
- heartbeat/status half, while `herdr-conductor` supplies `recover.sh`. After an
369
- npm upgrade, refresh the Herdr plugin from `TerrifiedBug/conductor/herdr` as
370
- well; publishing or installing npm alone cannot add the recovery-side tick
371
- request.
372
-
373
- For a first run, take a single tick in the foreground and watch it:
407
+ 5. Read the layered status:
374
408
 
375
409
  ```bash
376
- omp-conductor daemon --once
410
+ omp-conductor status
377
411
  ```
378
412
 
379
- The loop ticks every 5 minutes. `omp-conductor stop` shuts the loop down after
380
- the current tick rather than mid-run. When the live process is the MainPID of
381
- `omp-conductor.service`, stop goes through `systemctl stop` so a unit with
382
- `Restart=on-failure` cannot bring it straight back; otherwise it is a raw
383
- `SIGTERM` (then `SIGKILL` after 10 seconds). An example unit (with
384
- `SuccessExitStatus=0 143` and `MemoryMax=5G`) ships as
413
+ The result must show a running daemon, a healthy `/healthz`, armed ticks for external orchestration, and the configured project.
414
+
415
+ ### First worker drill
416
+
417
+ Use a disposable target repository for this drill. Replace the values below with labels that the wizard showed.
418
+
419
+ ```bash
420
+ TRACKER=acme/planning
421
+ QUEUE=ready-for-agent
422
+ ROUTE=repo:api
423
+
424
+ gh issue create --repo \"$TRACKER\" \
425
+ --title \"Conductor setup drill: add a marker file\" \
426
+ --body $'Add `conductor-smoke.txt` to the target repository.\\n\\nAcceptance: the file contains `setup path verified` and the pull request checks pass.' \
427
+ --label \"$ROUTE\" \
428
+ --label \"$QUEUE\"
429
+ ```
430
+
431
+ The daemon claims the issue, creates a worktree, runs the configured gates, and opens a pull request. Follow it with:
432
+
433
+ ```bash
434
+ omp-conductor status
435
+ omp-conductor tail <issue-number>
436
+ ```
437
+
438
+ Merge the green pull request. Then make sure that status moves the run to `merged` and frees its worker slot.
439
+
440
+ The loop ticks every 5 minutes while workers remain active. Merge settlement and capacity checks do not wait for the longest worker.
441
+
442
+ `omp-conductor stop` drains active workers before it stops. On systemd, it uses `systemctl stop` to prevent an automatic restart.
443
+
444
+ The package also ships a generic unit at
385
445
  [`systemd/omp-conductor.service.example`](systemd/omp-conductor.service.example).
386
446
 
387
447
 
@@ -400,6 +460,25 @@ Four control planes used to answer "stop" differently. The package verbs:
400
460
 
401
461
  `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.
402
462
 
463
+ `omp-conductor board [--project NAME]` opens the same facts as a live terminal
464
+ kanban instead of a scrolling wall of status text. Its columns are Queue,
465
+ Claimed, Running, Green, Blocked, Failed, and Merged. Queue cards are the
466
+ bounded per-issue hold sample recorded by the latest dispatch; the Queue count
467
+ is the authoritative ready count even when there are more ready issues than
468
+ sampled cards. Run columns show the newest attempt for each issue. Merged keeps
469
+ only the last 24 hours so the board stays operational rather than becoming an
470
+ analytics archive.
471
+
472
+ The board refreshes run, spend, turn, and dispatch values from SQLite every
473
+ second. It refreshes the slower daemon, Herdr, Telegram, and code-graph health
474
+ layer every ten seconds or immediately with `r`. Use arrow keys or `h/j/k/l` to
475
+ select a card, `Enter` to inspect and follow its worker transcript, `u` to run
476
+ the normal unblock workflow, `i` / `p` to open the issue / pull request, `?` for
477
+ help, and `q`, `Esc`, or `Ctrl-C` to go back and quit. Narrow terminals show a
478
+ sliding subset of columns around the selection; terminals below 50×20 get a
479
+ single resize instruction instead of a broken layout. The board is read-only
480
+ except for the explicit `u` action: it never claims work or changes stages.
481
+
403
482
  `halt --pane` is **fail-closed**: it exits `0` only when the conductor agent is
404
483
  *proven* gone. It writes the recovery pin first, so a failed stop still cannot be
405
484
  undone by `herdr-conductor` respawning the agent, and then refuses (nonzero exit,
@@ -435,16 +514,18 @@ Clear the pin with `omp-conductor release-pane` when you want recovery again.
435
514
 
436
515
  Per tick, for the daemon's project:
437
516
 
438
- 1. **Paused?** If the pause sentinel exists, the tick claims nothing and returns.
439
- Pause is checked first, so `omp-conductor pause` takes effect on the next tick
440
- without signalling the process.
441
- 2. **Settle the green PRs.** For every run in `pushed-green`, ask the tracker
442
- what became of its PR. Merged the row becomes `merged`; closed without
443
- merging `failed`, with the PR named in the row's `lastError`. Still open, or
444
- an answer that could not be obtained at all the row is left exactly as it
445
- is. Bounded by the number of green PRs awaiting a merge, which is a handful by
446
- construction. This runs above admission so a row settled here frees its issue
447
- in the same tick. See [what settles a green PR](#what-settles-a-green-pr).
517
+ 1. **Verify and settle pushed PRs.** For every run in `pushed-pending`, repeat
518
+ the independent head/check verification; green `pushed-green`, red
519
+ `failed`, and still pending stays occupied. For every verified
520
+ `pushed-green` run, ask what became of its PR. Merged `merged`; closed
521
+ without merging `failed`. Unknown answers leave the row unchanged. This
522
+ maintenance runs even while dispatch is paused or workers are active, so
523
+ status converges on the five-minute tick cadence. It also runs above admission
524
+ so a row settled here frees its issue in the same tick. See
525
+ [what settles a green PR](#what-settles-a-green-pr).
526
+ 2. **Paused?** If the pause sentinel exists, the tick claims nothing and returns.
527
+ Settlement has already run, but no queue or admission work occurs. This makes
528
+ `omp-conductor pause` take effect without signalling the process.
448
529
  3. **List the queue.** Open issues in `tracker.repo` labelled `queueLabel`.
449
530
  4. **Filter and route.** An issue is eligible only if it carries the queue label
450
531
  and none of the three state labels (`inProgress`, `blocked`, `failed`). Eligible
@@ -454,26 +535,31 @@ Per tick, for the daemon's project:
454
535
  6. **Check spend.** If spend since local midnight has reached `dailySpendUsd`, the
455
536
  daemon **pauses itself**, pages at Tier 2, and returns.
456
537
  7. **Check capacity.** `maxConcurrentWorkers` minus *live* workers (runs in
457
- `claimed` or `running`) gives the free slots. A green PR awaiting a human
458
- merge occupies its issue but not a slot: its worker is finished, and counting
459
- it would let two green PRs stop the fleet. If no slot is free, the tick logs
460
- and returns.
538
+ `claimed` or `running`) gives the free slots. A pending or green PR occupies
539
+ its issue but not a slot: its worker is finished, and counting pushed PRs
540
+ would let two completed workers stop the fleet.
541
+ If no slot is free, the tick logs and returns.
461
542
  8. **Admit issues** up to the free slots, skipping any issue that already has
462
- an active run — including a green PR, so a second attempt can never land on a
463
- live PR. An issue that has used `maxAttemptsPerIssue` escalates at Tier 1
464
- instead of being admitted.
543
+ an active run — including a pending or green PR, so a second attempt cannot
544
+ land on a live PR. Repeated implementation failures consume
545
+ `maxAttemptsPerIssue`; cap kills, daemon orphans and answered blocks consume
546
+ the independent `maxContinuationsPerIssue`. Exhausting either escalates.
465
547
  9. **Ask the tracker whether the work already exists.** For each candidate that
466
548
  survived step 8 — so at most one API call per free slot, never one per queued
467
- issue — the daemon asks whether an **open** PR already closes the issue. If one
468
- does, the issue is skipped with its PR named in the log. Drafts count: a draft
469
- PR's branch still holds the only copy of the work. This is the guard the store
470
- cannot provide, because a store younger than the PRs (a migration, a wiped or
471
- relocated state directory, a restore onto a new host) has no row to object
472
- with. If the check itself fails, the candidate is **held**, not admitted, and
549
+ issue — the daemon asks whether an **open** PR already closes it. An open PR
550
+ normally holds the issue. One narrow exception permits a routed continuation:
551
+ the PR URL must match the latest terminal run's retained PR exactly. Drafts
552
+ count because their branch can hold the only copy of the work.
553
+ The tracker also finds work missing from a new, moved, restored, or cleared
554
+ store. If the check fails, the candidate is **held**, not admitted, and
473
555
  retried next tick: the cost of holding is five minutes, the cost of admitting
474
556
  on an unknown is a burned attempt and a duplicate PR. Only that candidate is
475
557
  held, so a flaky API cannot stall the rest of the queue.
476
- 10. **Dispatch** the admitted issues concurrently.
558
+ 10. **Record the pass.** Persist ready/routed/admitted counts and group every hold
559
+ under a stable reason code with at most five sample issue numbers. Tracker
560
+ failures mark the summary `DEGRADED`; capacity, sibling, open-PR and budget
561
+ holds remain normal policy state.
562
+ 11. **Dispatch** the admitted issues concurrently.
477
563
 
478
564
  Then, per admitted issue:
479
565
 
@@ -496,13 +582,15 @@ Then, per admitted issue:
496
582
 
497
583
  | Outcome | Labels | Worktree | Escalation |
498
584
  | --- | --- | --- | --- |
585
+ | `pushed-pending` | `agent:in-progress` stays while the daemon rechecks GitHub | removed | none |
499
586
  | `pushed-green` | `agent:in-progress` stays until the merge closes the issue | removed | none |
500
587
  | `blocked` | swapped to `agent:blocked` | removed | Tier 1 |
501
- | `failed` / `killed` | swapped to `agent:failed` | dirty tree committed to the branch, then **kept** as evidence | Tier 1 |
588
+ | `failed` / `killed` | swapped to `agent:failed` | dirty tree committed to the branch, then retained until the PR or issue is terminal | Tier 1 |
502
589
  | unexpected error | swapped to `agent:failed` | same | Tier 1 |
503
590
 
504
- `pushed-green` is not the end of the row: a later tick settles it once the PR
505
- resolves. See [what settles a green PR](#what-settles-a-green-pr).
591
+ `pushed-pending` and `pushed-green` are not the end of the row: later ticks
592
+ verify outstanding checks and settle the PR once it resolves. See
593
+ [what settles a green PR](#what-settles-a-green-pr).
506
594
 
507
595
  Label swaps add the new label before removing the old one: the reverse order
508
596
  leaves a window in which the issue carries no state label at all, which is
@@ -521,6 +609,13 @@ Then, per admitted issue:
521
609
  run is deliberately *not* salvaged — it stopped on purpose, with turns still
522
610
  in hand to commit for itself.
523
611
 
612
+ Later ticks reap retained failure trees in bounded batches after the tracker
613
+ proves their PR merged/closed or their issue closed, provided no live run or
614
+ queued continuation owns the issue. Cleanup fetches remote refs first and
615
+ keeps any dirty tree or branch with uniquely local commits. Only then does it
616
+ remove the physical tree, prune registrations, and delete the obsolete local
617
+ mirror branch. Unknown tracker, network, repo, or git state is a no-op.
618
+
524
619
  ### What a restart does to runs that were in flight
525
620
 
526
621
  A `claimed` or `running` row is a promise that a worker process exists, and a
@@ -538,8 +633,9 @@ label is the crash guard against double-dispatch — and deciding what the dead
538
633
  worker's remains are worth is the orchestrator's drain-duty judgement, spelled
539
634
  out in its brief: an open green PR goes to the merge path, a salvaged sha is a
540
635
  continuation hand-off, and a clean orphan has its label released so the next
541
- tick re-claims it. Orphaned attempts still count toward `maxAttemptsPerIssue`,
542
- so a crash loop escalates instead of redispatching forever.
636
+ tick re-claims it. Orphans consume `maxContinuationsPerIssue`, not failed
637
+ implementation attempts, so crashes cannot starve the retry needed for a real
638
+ code or CI failure — and a crash loop still escalates.
543
639
 
544
640
  ### Deploying a new package onto a busy fleet
545
641
 
@@ -548,34 +644,36 @@ this version is installed: startup salvage commits dirty trees before orphaning
548
644
  rows, and salvage rewrites the mirror's managed `info/exclude` to the package's
549
645
  current list before `git add` so a narrowed ignore cannot hide deliverables.
550
646
 
551
- It is still disruptive for **in-flight sessions** (the worker process dies; the
552
- attempt is spent). Prefer draining when you can wait:
553
-
554
- 1. `omp-conductor pause` — stop new claims; live workers finish.
555
- 2. Wait until `omp-conductor status` shows `workers 0 / N` (no `deploy` hint line).
556
- 3. Install the new package (`bun add -g omp-conductor@…`, `omp plugin install …`).
557
- 4. `systemctl restart omp-conductor` (or `omp-conductor restart`).
558
- 5. `omp-conductor resume` if you left it paused.
647
+ It is still disruptive for **in-flight sessions** because the worker process dies
648
+ and the attempt is spent. Update through the lifecycle command rather than
649
+ hand-installing or restarting individual surfaces:
559
650
 
560
- If you cannot wait:
651
+ ```bash
652
+ omp-conductor upgrade
653
+ ```
561
654
 
562
- 1. `omp-conductor pause` (optional but keeps new claims off during the swap).
563
- 2. Install.
564
- 3. Restart salvage runs on boot for every live worktree, then rows go `orphaned`.
565
- 4. Resume; the pane orchestrator triages `agent:in-progress` orphans (continuation
566
- / merge / release label). Status prints a `deploy` line while live workers > 0
567
- so you can see the risk before you restart.
655
+ It pauses new claims, drains workers, installs one pinned release across all
656
+ surfaces, restarts, verifies twice, and resumes only if dispatch was initially
657
+ running. If an install, restart, or verification step fails, dispatch remains
658
+ paused and the command exits nonzero.
568
659
 
569
660
  Do **not** edit files under the running install and expect the daemon to keep
570
- dispatching — the integrity tripwire pauses and pages. Install, then restart, so
571
- the new process records a fresh baseline.
661
+ dispatching — the integrity tripwire pauses and pages. Upgrade by whole release
662
+ so the new process records a fresh baseline.
572
663
 
573
664
  ### What settles a green PR
574
665
 
575
- `pushed-green` means the worker finished, pushed, and watched the checks go green.
576
- What happens next is a human's decision, taken minutes to days later and never
577
- announced to the daemon so every tick asks the tracker about every green PR it
578
- is still holding, and settles the ones that resolved:
666
+ The worker watches CI, then reports the PR URL and the exact remote head SHA it
667
+ observed. The daemon independently reads the PR again and requires it to be open,
668
+ non-draft, still at that head, and backed by a non-empty check rollup in which
669
+ every check succeeded or was skipped. Missing or nonterminal checks become
670
+ `pushed-pending` and are rechecked on later ticks; red or cancelled checks become
671
+ `failed` with a bounded job/log digest. Only verified evidence becomes
672
+ `pushed-green`.
673
+
674
+ What happens after verification is a human's decision, taken minutes to days
675
+ later and never announced to the daemon — so every tick asks the tracker about
676
+ every pushed PR it is still holding:
579
677
 
580
678
  | PR | Row becomes | Why |
581
679
  | --- | --- | --- |
@@ -584,12 +682,12 @@ is still holding, and settles the ones that resolved:
584
682
  | still open | unchanged | The normal steady state. Its issue must stay occupied, or a second attempt lands on the live PR. |
585
683
  | 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. |
586
684
 
587
- The attempt counter is untouched either way, because both were real attempts. Run
588
- rows are the only thing that changes: a merge normally closes the issue, and a
589
- human who closed a PR is already looking at it, so what an issue's labels should
590
- say next is the orchestrator's drain-duty judgement — the same division of labour
591
- as a restart, above. One unreachable PR costs its own row and nothing else; the
592
- rest of the sweep still settles.
685
+ Run history is untouched. A PR closed without merging becomes a concrete failed
686
+ attempt; a merge does not spend failure or continuation budget. A merge normally
687
+ closes the issue, and a human who closed a PR is already looking at it, so what
688
+ an issue's labels should say next remains the orchestrator's drain-duty
689
+ judgement. One unreachable PR costs its own row and nothing else; the rest of
690
+ the sweep still settles.
593
691
 
594
692
  Until this existed, nothing ever revisited a `pushed-green` row: the startup
595
693
  reconciler only settles rows that held a process, and `merged` went unwritten. On
@@ -687,9 +785,10 @@ rest. `0` is a real value (a hard stop), not "unset".
687
785
  | --- | --- | --- |
688
786
  | `maxConcurrentWorkers` | `2` (setup may write `1` on &lt;16 GiB hosts) | Parallel in-process omp sessions inside the daemon PID. Two, because **CI runner slots, not model tokens, are the usual throughput ceiling** — a third worker would starve its own PR checks on a small self-hosted runner pool. On hosts under ~16 GiB RAM, prefer `1` so the unit stays out of swap ([host sizing](#host-sizing-and-memory)). Raise it only if you actually have the runners *and* the RAM. |
689
787
  | `dailySpendUsd` | `25` | Rolling-day spend ceiling in USD, or `null` for no spend gate. `0` is a hard stop. Metered from assistant `usage.cost.total`. |
690
- | `workerMaxTurns` | `120` | Turn ceiling for one worker. Catches a session looping without converging. |
788
+ | `workerMaxTurns` | `120` | Startup ceiling for each new worker. Catches a session looping without converging; use `omp-conductor extend` to raise one live run without changing this default. |
691
789
  | `workerWallClockMs` | `5400000` (90 minutes) | Wall-clock ceiling for one worker. A session that is merely stuck spends no turns, so turns alone cannot detect it. |
692
- | `maxAttemptsPerIssue` | `2` | Retries per issue before it escalates. One clean retry recovers from flaky CI; a third attempt almost always means the issue itself is underspecified. |
790
+ | `maxAttemptsPerIssue` | `2` | Failed implementation or CI attempts before escalation. Operational stops do not consume this budget, so salvage can continue without stealing the retry needed for a real failure. |
791
+ | `maxContinuationsPerIssue` | `2` | Cap-kill, daemon-orphan and answered-block resumes before escalation. This independently bounds crash/resume loops. |
693
792
 
694
793
  Days are counted from **local midnight**, matching how a human reads "today".
695
794
 
@@ -699,8 +798,15 @@ pages at Tier 2**: a loop that is burning money has to halt itself, because
699
798
  waiting for someone to notice tomorrow is how a runaway becomes expensive.
700
799
  Work resumes only after `omp-conductor resume` (or `/conductor resume`).
701
800
 
702
- `workerMaxTurns` and `workerWallClockMs` are enforced inside the session driver: the
703
- run is aborted, recorded as `killed`, and the escalation names which ceiling fired.
801
+ `workerMaxTurns` and `workerWallClockMs` are enforced inside the session driver.
802
+ The daemon reads a live run's effective turn ceiling at every turn boundary. Use
803
+ `omp-conductor extend <issue> --turns N [--project NAME]` to raise it without
804
+ restarting or reconstructing the session. Extension is monotonic: equal or lower
805
+ values are refused, as are runs whose live controller has already settled. The
806
+ effective value is persisted and shown beside that active run in `status`; editing
807
+ `config.json` changes defaults for future daemon starts, not workers already in
808
+ flight. A cap that fires aborts the run, records it as `killed`, and names the
809
+ ceiling in the escalation.
704
810
 
705
811
  ## Worker model
706
812
 
@@ -736,10 +842,10 @@ defined" in one call instead of twenty greps.
736
842
 
737
843
  ### Two things this package does not do for you
738
844
 
739
- `omp-conductor` never installs, spawns, imports or depends on the indexer — with
740
- `graphProject` unset, nothing about dispatch, caps or escalation changes. That
741
- means a fresh host needs both of these before an index is worth anything, and
742
- `graph-setup` reports them as step 0:
845
+ `omp-conductor` never installs, starts, imports, or depends on the indexer for
846
+ dispatch. With `graphProject` unset, nothing about dispatch, caps, escalation, or
847
+ status changes. A fresh host needs both of these before an index is worth
848
+ anything, and `graph-setup` reports them as step 0:
743
849
 
744
850
  1. **`codebase-memory-mcp` on PATH** — a separate project,
745
851
  [DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp).
@@ -750,11 +856,11 @@ means a fresh host needs both of these before an index is worth anything, and
750
856
 
751
857
  Say yes and the wizard asks for one root, then derives one clone per routed repo
752
858
  underneath it (default `~/.cache/conductor-graph/<org>/<repo>`) and writes it to
753
- each repo's [`graphProject`](#configuration). Nothing else changes: this package
754
- never runs an indexer, never imports one, and behaves identically with the graph
755
- server absent dispatch, caps and escalation do not know it exists. On a fleet
756
- that was configured before this key existed, `/conductor setup` and the `code
757
- graph` area add it in two prompts — see
859
+ each repo's [`graphProject`](#configuration). The only automatic interaction is
860
+ a bounded, read-only health query; this package never clones, fetches, builds an
861
+ index, or changes systemd. Dispatch, caps and escalation do not depend on graph
862
+ health. On a fleet configured before this key existed, `/conductor setup` and
863
+ the `code graph` area add it in two prompts — see
758
864
  [Changing one setting](#changing-one-setting).
759
865
 
760
866
  ### Why the clone, and not your checkout or the worktree
@@ -813,6 +919,18 @@ neither usefully: the indexer resolves its store from `HOME`, systemd's default
813
919
  user that ran `graph-setup`; the unit sets no `User=`, so check them if that is
814
920
  not the account the timer runs as.
815
921
 
922
+ ### Seeing whether the graph is usable
923
+
924
+ When at least one routed repo has `graphProject`, `omp-conductor status` adds a
925
+ `code graph` block. It proves the indexer is on `PATH`, the worker MCP config
926
+ mounts it, every configured clone exists and exactly matches an indexed
927
+ `root_path`, the refresh timer is enabled and active, and the last service run
928
+ succeeded within 45 minutes. A running daemon refreshes this evidence every
929
+ minute and publishes the cached result through `/healthz`; status probes the host
930
+ directly when that cache is unavailable. Every command is read-only, runs with a
931
+ one-second timeout, and graph degradation never changes `/healthz.ok` or blocks
932
+ dispatch. Unconfigured projects omit the block entirely.
933
+
816
934
  ## Escalation tiers
817
935
 
818
936
  | Tier | Meaning | Raised by | Delivered to |
@@ -873,7 +991,10 @@ The config lives at `$OMP_CONDUCTOR_HOME/config.json`, or
873
991
  `~/.omp/conductor/config.json` when that variable is unset. It is written with mode
874
992
  `0600` in a directory created `0700`, because it carries chat ids and clone URLs.
875
993
  That same directory holds the SQLite store (`conductor.db`), the `paused` sentinel,
876
- the `sessions/` worker transcripts and the `orchestrator/` session directory.
994
+ the `sessions/` worker transcripts, the `orchestrator/` session directory,
995
+ `backups/briefs/` for timestamped brief and policy safety copies, and
996
+ `release-policy-blocks.jsonl`, the append-only audit of mechanically rejected
997
+ release/deploy calls.
877
998
 
878
999
  Runtime state lives elsewhere, under `$OMP_CONDUCTOR_RUNTIME_DIR` (default
879
1000
  `~/.omp/run/daemons/omp-conductor`): `daemon.json`, a mode-`0600` pidfile written
@@ -909,7 +1030,8 @@ A complete, valid config for one project with two target repos:
909
1030
  "dailySpendUsd": 25,
910
1031
  "workerMaxTurns": 120,
911
1032
  "workerWallClockMs": 5400000,
912
- "maxAttemptsPerIssue": 2
1033
+ "maxAttemptsPerIssue": 2,
1034
+ "maxContinuationsPerIssue": 2
913
1035
  },
914
1036
  "projects": [
915
1037
  {
@@ -959,6 +1081,7 @@ A complete, valid config for one project with two target repos:
959
1081
  "merge": "human",
960
1082
  "release": "human"
961
1083
  },
1084
+ "releasePolicy": "none",
962
1085
  "reporting": {
963
1086
  "scope": "material"
964
1087
  },
@@ -986,6 +1109,7 @@ Field notes:
986
1109
  | `escalation.fallbackToIssueComment` | Defaults to `true`. Absent means "yes, still tell me". |
987
1110
  | `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. |
988
1111
  | `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. |
1112
+ | `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. |
989
1113
  | `reporting.scope` | Optional; `"material"` (default) or `"escalations"`. Every orchestrator tick appends the matching constraint line to its prompt, re-read from this file each tick — see [Your workflow vs. the package](#your-workflow-vs-the-package). It constrains what the session is told to report; it is not an outbound filter. A config written without the key keeps reporting material events. Any other value is an error, never folded to the default. |
990
1114
  | `workspaceRoot` / `mirrorRoot` | Optional; default to `worktrees/` and `mirrors/` under the state directory. `~` is expanded. |
991
1115
 
@@ -1000,9 +1124,9 @@ running its loop. A 24/7 omp session with a standing brief and nobody typing int
1000
1124
  it never gets prompted, so it never runs anything. Installing
1001
1125
  `omp plugin install omp-conductor` also installs a heartbeat that prompts it.
1002
1126
 
1003
- The heartbeat is **inert unless the session's cwd contains
1004
- `.conductor-tick.json`**, so it costs an ordinary session nothing. Drop the file
1005
- in the orchestrator's working directory:
1127
+ The heartbeat is **inert unless the session cwd contains
1128
+ `.conductor-tick.json`**, so an ordinary session has no timer. `/conductor setup`
1129
+ writes this file for external orchestration. A manual configuration has this form:
1006
1130
 
1007
1131
  ```json
1008
1132
  {
@@ -1199,14 +1323,18 @@ omp-conductor start [--port N] [--project NAME]
1199
1323
  omp-conductor --version
1200
1324
  omp-conductor stop
1201
1325
  omp-conductor restart [--port N] [--project NAME]
1326
+ omp-conductor upgrade [--to VERSION] [--project NAME]
1202
1327
  omp-conductor status [--project NAME]
1328
+ omp-conductor board [--project NAME]
1203
1329
  omp-conductor hold [--project NAME]
1204
1330
  omp-conductor halt [--pane] [--project NAME]
1205
1331
  omp-conductor arm [--project NAME]
1206
1332
  omp-conductor disarm [--project NAME]
1207
1333
  omp-conductor release-pane [--project NAME]
1208
1334
  omp-conductor tail <issue> [--project NAME]
1335
+ omp-conductor extend <issue> --turns N [--project NAME]
1209
1336
  omp-conductor unblock <issue> [--project NAME]
1337
+ omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
1210
1338
  omp-conductor daemon [--once] [--port N] [--project NAME]
1211
1339
  omp-conductor pause
1212
1340
  omp-conductor resume
@@ -1220,16 +1348,20 @@ omp-conductor help
1220
1348
  | `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`. |
1221
1349
  | `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. |
1222
1350
  | `restart` | Prefer `systemctl restart` when the unit owns the live pid so the replacement stays supervised; otherwise `stop` then `start`, inheriting the running daemon's port and project unless a flag overrides them. The new process **salvages dirty live worktrees before orphaning** those rows — see [Deploying a new package onto a busy fleet](#deploying-a-new-package-onto-a-busy-fleet). |
1223
- | `status [--project NAME]` | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / `daemon`, then the project body. The next time comes from the live heartbeat process, not a guess from log timestamps. Telegram health uses `getMe` to prove API authentication without sending a message and reports inbound bridge configuration separately. The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
1351
+ | `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. |
1352
+ | `status [--project NAME]` | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / optional `code graph` / `daemon`, then the project body. The project body includes the latest completed dispatch timestamp, ready/routed/admitted counts, and bounded hold groups; 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. The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
1353
+ | `board [--project NAME]` | Live keyboard-driven kanban over the same SQLite and `/healthz` truth as `status`: Queue, Claimed, Running, Green, Blocked, Failed, and the last 24 hours of Merged. Refreshes run/spend/turn values every second and slower health every ten seconds. `Enter` follows the selected transcript in place; `u` invokes the existing unblock workflow; `i` / `p` open the issue / PR; `r` refreshes health; `?` shows all keys. Requires an interactive terminal of at least 50×20. |
1224
1354
  | `hold [--project NAME]` | Soft stop: pause claiming **and** disarm ticks. Daemon and pane stay up. Prefer this over `pause` when the intent is "stop the conductor" without killing processes. See [Stop the conductor](#stop-the-conductor-hold--halt). |
1225
1355
  | `halt [--pane] [--project NAME]` | `hold`, then stop the dispatch daemon (systemctl-aware). Pane stays up unless `--pane` is passed. `halt --pane` also pins herdr-conductor recovery off for the conductor agent only — it does **not** stop `herdr-fleet.service` or any other herdr session. Fail-closed: exits nonzero unless the agent is proven gone. |
1226
1356
  | `arm [--project NAME]` | Proof-gated: send a Telegram challenge and write the arm marker only after your reply appears as a user turn in the orchestrator transcript. Never auto-armed by `resume` / `hold`. |
1227
1357
  | `disarm [--project NAME]` | Remove the arm marker so ticks skip. Processes untouched. |
1228
1358
  | `release-pane [--project NAME]` | Clear the `halt --pane` recovery pin so herdr-conductor may resume the fleet agent again. |
1229
1359
  | `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>`. |
1230
- | `unblock <issue>` | Remove that issue's `blocked` and `failed` state labels through the tracker, so the next tick can claim it again. This is the supported way back for an escalation you answered: eligibility disqualifies any issue carrying a state label, so an answered issue that keeps one is never re-claimed and the answer is inert. Removing a label the issue does not carry is a no-op, so both are always cleared and neither has to be looked up first. `agent:in-progress` is deliberately not touched — it means a worker process exists, which is not something an answer changes. The run history is left exactly as it is: an answered block still spent a worker, so it still counts toward `maxAttemptsPerIssue`, and the output says how many attempts remain — or warns that the next tick will escalate instead of dispatching, when none do. Exits `2` with `unblock needs an issue number` on a missing or malformed positional. |
1231
- | `daemon` | Run the loop in the **foreground**, ticking every 5 minutes and serving `/healthz`. This is what `start` launches, and what a systemd unit should call. Writes the pidfile itself, and refuses with `another daemon is alive (pid N); stop it first` rather than becoming a second dispatcher. |
1232
- | `daemon --once` | Run a single tick and exit. No HTTP server, and no pidfile a drill must not register itself as the daemon, or the next reader believes it and the real daemon's in-flight runs get reconciled as orphans. |
1360
+ | `extend <issue> --turns N [--project NAME]` | Monotonically raise that live worker's effective turn ceiling through its owning daemon. The current omp session keeps running; no restart or continuation is created. The daemon persists the new ceiling for `status` and rejects missing, settled, cap-killed, equal, or lower requests instead of implying that an immutable session changed. |
1361
+ | `unblock <issue>` | Remove that issue's `blocked` and `failed` labels so an answered escalation can be claimed again. `agent:in-progress` is never touched. 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. Exits `2` when the issue number is missing or malformed. |
1362
+ | `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. |
1363
+ | `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. |
1364
+ | `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. |
1233
1365
  | `--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. |
1234
1366
  | `--project NAME` | Pick the project to service. One daemon process serves exactly one project; with several configured projects the name is required. |
1235
1367
  | `pause` | Stop claiming new work only. The running daemon notices on its next tick; runs already in flight finish. The orchestrator heartbeat keeps ticking if armed — its gate is the arm marker, not this flag. Prefer `hold` to silence both. |
@@ -1261,17 +1393,52 @@ curl -s localhost:8787/healthz
1261
1393
  ```
1262
1394
 
1263
1395
  ```json
1264
- { "ok": true, "paused": false, "activeRuns": 1, "project": "demo" }
1396
+ {
1397
+ "ok": true,
1398
+ "paused": false,
1399
+ "activeRuns": 1,
1400
+ "project": "demo",
1401
+ "rssBytes": 123456789,
1402
+ "dispatch": {
1403
+ "completedAt": 1786185678000,
1404
+ "ready": 8,
1405
+ "routed": 8,
1406
+ "admitted": 0,
1407
+ "degraded": true,
1408
+ "holds": [
1409
+ { "reason": "parent-lookup-error", "count": 8, "issues": [321, 320, 318] }
1410
+ ]
1411
+ },
1412
+ "codeGraph": {
1413
+ "configured": true,
1414
+ "status": "degraded",
1415
+ "checkedAt": "2026-08-08T13:00:00.000Z",
1416
+ "prerequisites": { "indexer": "present", "mcpMount": "missing" },
1417
+ "repos": [
1418
+ {
1419
+ "name": "api",
1420
+ "path": "/home/fleet/.cache/conductor-graph/acme/api",
1421
+ "clone": "present",
1422
+ "index": "present"
1423
+ }
1424
+ ],
1425
+ "timer": { "enabled": "enabled", "active": "active" },
1426
+ "refresh": {
1427
+ "result": "success",
1428
+ "fresh": true,
1429
+ "lastSuccessAt": "2026-08-08T12:50:00.000Z",
1430
+ "ageMs": 600000
1431
+ },
1432
+ "reasons": ["worker MCP configuration does not mount the indexer"]
1433
+ }
1434
+ }
1265
1435
  ```
1266
1436
 
1267
- Any other path or method returns `404`. Note that `ok` reports only that the
1268
- process is serving. It does not report that the fleet is doing work: read
1269
- `paused` to tell those apart.
1270
-
1271
- `activeRuns` counts occupied issues — live workers plus green PRs still awaiting a
1272
- merge — and each of those is settled by the tick once its PR resolves, so the
1273
- number goes back down on its own. A count that only ever grows is the bug this
1274
- used to have: read [what settles a green PR](#what-settles-a-green-pr).
1437
+ Any other path or method returns `404`. `ok` reports process liveness only.
1438
+ Nonfatal admission errors and graph degradation keep it `true` so a supervisor
1439
+ does not restart-loop. Inspect `dispatch.degraded` and its bounded reason groups
1440
+ for queue starvation; inspect `codeGraph` for configured graph health.
1441
+ `activeRuns` counts occupied issues — live workers plus green PRs awaiting merge.
1275
1442
 
1276
1443
  ## What a worker may and may not do
1277
1444
 
@@ -1287,11 +1454,11 @@ dispatcher. The brief is explicit about the boundary:
1287
1454
  | Run the repo's configured cheap gates, each from its listed `cwd`, over the whole tree. | Run docker or image builds, production builds, browser/e2e suites, or the full test suite on the shared host — CI owns the heavy gates. |
1288
1455
  | Review its whole diff, then commit and **push once**. One corrective push if CI is red. | Force-push, `git add -f`, or add AI/co-author attribution. Red twice means stop and report, not push a third time. |
1289
1456
  | Open a PR that links the issue, and watch CI to a verdict with `gh pr checks --watch`. | Run `gh pr merge`. **A worker never merges** — that one is absolute, whoever else holds the authority — so PRs land one at a time with a freshness re-check; two workers merging concurrently is how agent PRs clobber each other. Who *may* merge is the [`authority`](#configuration) answer, and it is never the worker. |
1290
- | Escalate: ambiguity, a cross-repo contract, a needed credential, a product or data-migration decision, a blocking existing test, CI red twice, or most of the wall-clock budget burned. | **Cut a release**, push a tag, publish to npm, edit a deployment pin, deploy, or touch infrastructure or secrets permanently out of scope. Releases are batched and decided outside this loop, so "this needs releasing" is a thing to report, never a task to take on. |
1457
+ | Escalate: ambiguity, a cross-repo contract, a needed credential, a product or data-migration decision, a blocking existing test, CI red twice, or most of the wall-clock budget burned. | **Under the default `releasePolicy: "none"`:** cut a release, push a tag, publish to npm, edit a deployment pin, deploy, or touch infrastructure or secrets. The harness blocks recognised release/deploy tool calls before they run and audits the attempt. A project may deliberately set `operator-brief` only after its operator-owned brief contains the exact release procedure; that opens the tool gate but does not change `authority.release`. |
1291
1458
 
1292
- The worker ends with a six-line evidence report (issue, pr, state, gates, changed,
1293
- next). `pushed-green` means it watched the checks go green rather than expecting
1294
- them to.
1459
+ The worker ends with a seven-line evidence report (issue, PR, observed head SHA,
1460
+ state, gates, changed, next). A textual `pushed-green` claim is not success: the
1461
+ daemon repeats the PR/head/check verification before it records that state.
1295
1462
 
1296
1463
  ### Worker confinement and the integrity tripwire
1297
1464
 
@@ -1303,10 +1470,11 @@ which installs an inline harness extension that blocks `write` / `edit` /
1303
1470
  `routing.repos` is ever checked out — and the caps still bound *how much* work
1304
1471
  happens.
1305
1472
 
1306
- **`bash` is not gated.** Its argument is an opaque shell string; pretending to
1307
- parse it would be a false sense of security. The brief still forbids escaping
1308
- via the shell, and the deploy-level answer is a least-privilege worker uid
1309
- (below).
1473
+ General shell access is not confined to the worktree. Its argument is an opaque
1474
+ program, so the brief still forbids path escape and the deploy-level answer is a
1475
+ least-privilege worker uid (below). The narrower release-policy tripwire does
1476
+ inspect explicit command shapes such as `git tag`, `npm publish`, and deploy
1477
+ verbs; it blocks those before execution when `releasePolicy` is `none`.
1310
1478
 
1311
1479
  #### Integrity tripwire (package self-hash)
1312
1480
 
@@ -1378,25 +1546,27 @@ Known and deliberate in this version:
1378
1546
  - **No cross-process lock on the mirrors.** Two dispatch loops fetching the same
1379
1547
  repo at the same instant can collide on git's ref locks; the run fails and is
1380
1548
  retried rather than corrupted.
1381
- - **Mirrors grow one branch ref per run.** Unpushed work is never discarded, so
1382
- refs accumulate until you reap them.
1383
- - **`stop` is a deadline, not a clean drain.** Whether it goes through
1384
- `systemctl` or a raw signal, the loop is asked to finish the tick it is on, and
1385
- a tick with a worker in flight can run for that worker's whole wall clock; the
1386
- signal path escalates to `SIGKILL` after 10 seconds. There is no "stop once the
1387
- current worker lands". A unit that supervises the daemon should set
1388
- `SuccessExitStatus=0 143` (belt-and-braces for a handled `SIGTERM` exit) and
1389
- operators should prefer `omp-conductor stop` / `systemctl stop` over a raw
1390
- `kill`, so `Restart=on-failure` cannot misread a deliberate stop as a crash.
1549
+ - **Uniquely local mirror branches are retained.** Terminal runs are reaped
1550
+ automatically only after every commit exists on a remote ref. A failed salvage
1551
+ push deliberately leaves its branch and tree for an operator rather than
1552
+ trading disk hygiene for data loss.
1553
+ - **`stop` is a bounded best-effort drain.** A signal stops new ticks and the
1554
+ daemon waits for its active worker pool before closing the store. The CLI
1555
+ escalates to `SIGKILL` after 10 seconds, so a worker that needs longer is
1556
+ orphaned and salvaged on restart. Use `pause`, wait for `workers 0 / N`, then
1557
+ stop when a clean drain matters. A supervising unit should set
1558
+ `SuccessExitStatus=0 143`, and operators should prefer `omp-conductor stop` /
1559
+ `systemctl stop` over raw `kill`, so `Restart=on-failure` cannot misread a
1560
+ deliberate stop as a crash.
1391
1561
  - **A failed orchestrator degrades quietly.** The daemon logs a warning and keeps
1392
1562
  running, but tier-1 escalations then land in issue comments — which is exactly the
1393
1563
  "nobody reads it until morning" path the orchestrator exists to avoid. The warning
1394
1564
  is in `daemon.log`; nothing pages you about it.
1395
- - **Workers are not terminal panes, so you cannot watch them.** A worker is an
1396
- in-process omp session inside the daemon, started by `createSession` and driven
1397
- concurrently via `Promise.allSettled`. Herdr therefore shows exactly one pane
1398
- (the orchestrator's) no matter how many workers are running, and no amount of
1399
- `maxConcurrentWorkers` changes that.
1565
+ - **Workers are not terminal panes, so you cannot watch them there.** Each
1566
+ worker is an in-process omp session started by `createSession`. The resident
1567
+ daemon tracks workers in a background pool so the five-minute loop keeps
1568
+ settling PRs and checking capacity; shutdown waits for that pool. Herdr still
1569
+ shows exactly one pane (the orchestrator's) regardless of concurrency.
1400
1570
 
1401
1571
  The cap does work. The admission loop (`admitCandidates` in `src/daemon.ts`) computes
1402
1572
  `slots = maxConcurrentWorkers - live workers`, admits at most that many issues