omp-conductor 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/REFERENCE.md +71 -17
- package/agents/to-spec.md +90 -0
- package/package.json +2 -1
- package/schema/config.schema.json +53 -1
- package/src/admission.ts +308 -76
- package/src/ask.ts +307 -10
- package/src/backups.ts +2 -2
- package/src/board.ts +17 -3
- package/src/briefs/orchestrator.md +43 -14
- package/src/briefs/to-spec.md +84 -0
- package/src/briefs/worker.md +37 -19
- package/src/cli.ts +2 -0
- package/src/command-help.ts +19 -1
- package/src/command-manifest.ts +27 -2
- package/src/commands/context.ts +1 -0
- package/src/commands/drain.ts +176 -0
- package/src/commands/extend.ts +6 -10
- package/src/commands/status.ts +5 -1
- package/src/commands/watch.ts +110 -3
- package/src/commands/worker.ts +9 -10
- package/src/config-schema.ts +57 -0
- package/src/config.ts +102 -2
- package/src/daemon.ts +1220 -1517
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/server.ts +5 -2
- package/src/decisions.ts +279 -16
- package/src/depends-on.ts +261 -1
- package/src/diff-flags.ts +425 -1
- package/src/digest-schedule.ts +37 -0
- package/src/doctor.ts +52 -0
- package/src/escalate.ts +9 -3
- package/src/failure-class.ts +43 -4
- package/src/fleet.ts +166 -24
- package/src/gitops.ts +188 -81
- package/src/graph-health.ts +55 -8
- package/src/graph.ts +379 -69
- package/src/harness-loader.ts +59 -0
- package/src/host.ts +567 -2
- package/src/lifecycle.ts +158 -6
- package/src/omp.ts +269 -20
- package/src/orchestrator-tick.ts +1489 -26
- package/src/orchestrator.ts +12 -0
- package/src/privileged.ts +1 -4
- package/src/release-policy.ts +503 -9
- package/src/routing.ts +11 -3
- package/src/session-host.ts +115 -5
- package/src/settlement.ts +1780 -0
- package/src/setup-host.ts +1205 -6
- package/src/setup-install.ts +119 -30
- package/src/setup-wizard.ts +88 -2
- package/src/setup.ts +119 -13
- package/src/shell.ts +15 -0
- package/src/status-render.ts +100 -11
- package/src/store.ts +519 -45
- package/src/to-spec.ts +387 -0
- package/src/tracker/github.ts +150 -14
- package/src/types.ts +470 -16
- package/src/upgrade-verify.ts +209 -2
- package/src/upgrade.ts +175 -1
- package/src/verbs/protocol.ts +39 -0
- package/src/verbs/server.ts +770 -40
- package/src/verbs/socket.ts +24 -5
- package/src/worker.ts +239 -9
- package/src/worktree.ts +142 -18
package/README.md
CHANGED
|
@@ -271,6 +271,40 @@ The Herdr half owns recovery, not dispatch or policy: it restores the exact
|
|
|
271
271
|
session identity, requests an immediate heartbeat, or reports through Telegram
|
|
272
272
|
and a Herdr notification that the fleet is down.
|
|
273
273
|
|
|
274
|
+
### The worker identity and its harness binding
|
|
275
|
+
|
|
276
|
+
Worker sessions do not run as you. `setup host` creates a dedicated
|
|
277
|
+
unprivileged account, `omp-worker` (home `/var/lib/omp-worker`), and the daemon
|
|
278
|
+
launches every worker session under it through `setpriv`. The account is granted
|
|
279
|
+
search access to the paths a session needs and read access to your agent config
|
|
280
|
+
files; its writable world is the worktree and session directory dispatch hands
|
|
281
|
+
it per run, and nothing else.
|
|
282
|
+
|
|
283
|
+
That boundary means the account cannot list your home — which is also where
|
|
284
|
+
`omp-conductor` and its `@oh-my-pi/pi-coding-agent` peer are installed, and
|
|
285
|
+
module resolution needs to list a directory to find the `node_modules` inside
|
|
286
|
+
it. Left there, a worker silently resolved a *different* harness version out of
|
|
287
|
+
its own package cache. So `setup host` also installs one mount unit,
|
|
288
|
+
`var-lib-omp\x2dworker\x2dharness-node_modules.mount`, binding your install
|
|
289
|
+
read-only at `/var/lib/omp-worker-harness/node_modules`, and launches worker
|
|
290
|
+
children from that path. It is a bind, not a copy: upgrade the harness and every
|
|
291
|
+
worker picks it up with nothing to re-materialise. The mount's parent directory
|
|
292
|
+
is `root:omp-worker` `0750`, so the bound tree is reachable by root and the
|
|
293
|
+
worker account and by no other local account.
|
|
294
|
+
|
|
295
|
+
Three consequences worth knowing:
|
|
296
|
+
|
|
297
|
+
- `omp-conductor` must be installed, not run from a source checkout, for worker
|
|
298
|
+
dispatch to work — a checkout has no install root to bind.
|
|
299
|
+
- If the binding is missing or no longer resolves to your install, the daemon
|
|
300
|
+
refuses to launch workers and says so (`setup host` reports the same thing as
|
|
301
|
+
pending work). Re-run `omp-conductor setup host`; no attempt is charged to the
|
|
302
|
+
issue, because no session ever started.
|
|
303
|
+
- The check runs at every launch, not once at daemon startup. The mount unit is
|
|
304
|
+
ordered before the daemon, so a reboot brings them up in the right order — and
|
|
305
|
+
if the binding ever arrives late anyway, the next dispatch picks it up with no
|
|
306
|
+
restart needed.
|
|
307
|
+
|
|
274
308
|
### Stop the conductor (hold / stop)
|
|
275
309
|
|
|
276
310
|
Two words, and one of them takes a flag:
|
package/REFERENCE.md
CHANGED
|
@@ -763,6 +763,40 @@ or `/`) is `bug`, and `feat` otherwise. The slug is the issue title folded to
|
|
|
763
763
|
issue alone, so a retried run recomputes the same branch and finds its own work
|
|
764
764
|
instead of forking a second one.
|
|
765
765
|
|
|
766
|
+
### The file-lane declaration
|
|
767
|
+
|
|
768
|
+
Two workers writing the same file clobber each other, so admission holds a
|
|
769
|
+
candidate whose declared lane overlaps a live run's actual lane (`file-lane`
|
|
770
|
+
hold). The declaration is read from the issue body or a pre-dispatch comment —
|
|
771
|
+
the two surfaces the worker brief renders — through one grammar, so the lane the
|
|
772
|
+
gate enforces and the lane the promotion echo reports are always the same parse:
|
|
773
|
+
|
|
774
|
+
- **Inline line** (the brief form since #555): a line beginning with `File
|
|
775
|
+
lane:` or `File-lane=` (case-insensitive, tolerating heading/list markers)
|
|
776
|
+
whose rest carries the paths, backtick-delimited, with a bare
|
|
777
|
+
comma/space-separated fallback for tokens that look like relative paths:
|
|
778
|
+
`**File lane:** `omp/src/a.ts`, `omp/src/b.ts` are yours — nothing else holds
|
|
779
|
+
them.`
|
|
780
|
+
- **Write-lane section** (the package-floor decomposition form, #825): a
|
|
781
|
+
markdown heading `## Exact write lane` (or `## Write lane` /
|
|
782
|
+
`## write-lane`) whose immediately following bullet items carry one
|
|
783
|
+
backticked path each — `- `omp/src/types.ts` — why it changes`. Only the
|
|
784
|
+
contiguous bullet run under that heading is read: a following paragraph
|
|
785
|
+
(like a `Read only:` caveat) or the next section ends it, so read-only
|
|
786
|
+
entry points, proof commands and acceptance bullets elsewhere in the issue
|
|
787
|
+
are never captured as write files.
|
|
788
|
+
|
|
789
|
+
When both are present, the inline line wins — it has held since #555, and the
|
|
790
|
+
promotion echo shows exactly which declaration admission enforces. Across
|
|
791
|
+
surfaces, the latest declaration among the body and the whole comment thread
|
|
792
|
+
supersedes (a pre-dispatch correction comment replaces the body's lane). A body
|
|
793
|
+
or thread with no declaration admits exactly as before: `fail open`, and the
|
|
794
|
+
label response says `no lane declared (fail open)`. A clearly delimited
|
|
795
|
+
write-lane section that parsed nothing is **not** an absent declaration: adding
|
|
796
|
+
the queue label is refused with `file-lane-unparseable` and an actionable
|
|
797
|
+
message, because promoting beside overlapping work on a section that plainly
|
|
798
|
+
tried to declare is the exact gap the interlock exists to close.
|
|
799
|
+
|
|
766
800
|
## Routing
|
|
767
801
|
|
|
768
802
|
An issue must carry **exactly one** `repo:<name>` label naming a repo in
|
|
@@ -1078,11 +1112,15 @@ omp-conductor setup graph --print # print the plan: clones, index commands,
|
|
|
1078
1112
|
omp-conductor setup graph # run it: clone, install, enable, seed, verify
|
|
1079
1113
|
```
|
|
1080
1114
|
|
|
1081
|
-
`setup graph --print` prints a `git clone` for
|
|
1082
|
-
one-shot index command per repo, and
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1115
|
+
`setup graph --print` prints the plan: the host prerequisites, a `git clone` for
|
|
1116
|
+
every clone that does not exist yet, the one-shot index command per repo, and
|
|
1117
|
+
this project's `cbm-reindex-<project>.sh` script plus `cbm-reindex-<project>.service`
|
|
1118
|
+
+ `cbm-reindex-<project>.timer` unit pair, named for the project so a second
|
|
1119
|
+
project on the same host gets its own refresh instead of silently replacing the
|
|
1120
|
+
first project's (#720). A run stages those three files in the state directory
|
|
1121
|
+
**only after its consent prompt** — declining leaves the staged tree
|
|
1122
|
+
byte-identical — then clones, installs and enables the timer as root, seeds one
|
|
1123
|
+
indexing run, and verifies; it never runs `systemctl` on its own authority.
|
|
1086
1124
|
|
|
1087
1125
|
**Run it as the account the fleet runs as, never under `sudo`** — it refuses if
|
|
1088
1126
|
you try. Everything it derives resolves per-account: the config it loads, the
|
|
@@ -1416,7 +1454,7 @@ conditions, each one something this package can check without asking you:
|
|
|
1416
1454
|
| Condition | Met when |
|
|
1417
1455
|
| --- | --- |
|
|
1418
1456
|
| `pr-merged:<https url>` | `gh` reports that pull request merged. |
|
|
1419
|
-
| `pr-checks-green:<https url>` | Every check on that pull request has a green verdict (a non-empty list, all `success`/`neutral`); a failing or still-pending check is not met. |
|
|
1457
|
+
| `pr-checks-green:<https url>` | Every check on that pull request has a green verdict (a non-empty list, all `success`/`neutral`); a failing or still-pending check is not met. The verdict is bound to the exact PR head it was observed at: if the head changes, the row returns to pending until the new head's own checks are green (#808). |
|
|
1420
1458
|
| `pr-mergeable:<https url>` | The pull request is mergeable (`clean`, not `unknown` or conflicting). |
|
|
1421
1459
|
| `issue-closed:<number>` | That issue is closed on the tracker. |
|
|
1422
1460
|
| `npm-version:<pkg>@<version>` | `npm view <pkg>@<version> version` succeeds — the version is published. |
|
|
@@ -1430,10 +1468,11 @@ arm/channel/pending single-flight) instead of waiting a full interval. The poke
|
|
|
1430
1468
|
reason and the digest flag `[CONDITION MET — act on this now]` both surface the
|
|
1431
1469
|
wake so the session acts when the answer becomes actionable. Repeated sweeps
|
|
1432
1470
|
while the condition stays true do nothing further — the store marks the
|
|
1433
|
-
transition once.
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
here merges on its
|
|
1471
|
+
transition once. A `pr-checks-green` watch survives a head change on its own:
|
|
1472
|
+
the binding is about the exact commit, so an updated branch drops the row back
|
|
1473
|
+
to pending and the new head's green transition wakes merge review the same way
|
|
1474
|
+
(`conductor_pr_update_branch` needs no fresh watch). Nothing here merges on its
|
|
1475
|
+
own.
|
|
1437
1476
|
|
|
1438
1477
|
Anything else exits `2` and lists the six forms. An unparseable condition on an
|
|
1439
1478
|
existing row is *listed and never treated as met*: a grammar a future release
|
|
@@ -1940,10 +1979,19 @@ injected tick the extension refuses it and mounts its own bounded surface,
|
|
|
1940
1979
|
|
|
1941
1980
|
- The call carries `question`, `on-timeout` (`auto-proceed` or `park`, required),
|
|
1942
1981
|
and optionally `timeoutSeconds`, `blocks`, `recommended`, `options` and
|
|
1943
|
-
`category`.
|
|
1982
|
+
`category`. `recommended` is required whenever `on-timeout` is `auto-proceed`
|
|
1983
|
+
(the row must record what was auto-applied) and, when `options` are supplied,
|
|
1984
|
+
must be one of their labels — the label as delivered, never an index.
|
|
1944
1985
|
- The tool records a decision row first (durable, seven-day expiry), then
|
|
1945
|
-
delivers the question
|
|
1946
|
-
|
|
1986
|
+
delivers the question. When the Telegram surface can (a bot token, a
|
|
1987
|
+
configured escalation chat, a paired owner, and the reporting policy
|
|
1988
|
+
permitting an interrupt), it posts the options as the same selectable
|
|
1989
|
+
buttons `telegram_ask` posts and a tap resolves the decision row with the
|
|
1990
|
+
chosen option's label. When it cannot, the question goes out as plain text
|
|
1991
|
+
through the same path `omp-conductor message` uses — immediately when the
|
|
1992
|
+
reporting policy permits, durably held otherwise — and the decision row
|
|
1993
|
+
records the degraded delivery, so a prose reply is never treated as a
|
|
1994
|
+
selection (resolve or withdraw the row by hand if the operator answers).
|
|
1947
1995
|
- It waits at most the ceiling: `timeoutSeconds` if the ask names one, else the
|
|
1948
1996
|
tick config's `askTimeoutSeconds`, else 300 seconds — always capped at the
|
|
1949
1997
|
turn budget, so the ask can never outlive the turn it runs in. An ask issued
|
|
@@ -2223,6 +2271,9 @@ omp-conductor doctor [--project NAME] [--json] [--probe-telegram]
|
|
|
2223
2271
|
omp-conductor ledger [--issue N] [--limit N] [--project NAME] [--json]
|
|
2224
2272
|
omp-conductor board [--project NAME] [--json]
|
|
2225
2273
|
omp-conductor hold [--keep-ticks] [--project NAME]
|
|
2274
|
+
omp-conductor drain start --until ISO|DURATION [--reason TEXT] [--project NAME]
|
|
2275
|
+
omp-conductor drain status [--project NAME]
|
|
2276
|
+
omp-conductor drain cancel [--project NAME]
|
|
2226
2277
|
omp-conductor stop [--pane] [--project NAME]
|
|
2227
2278
|
omp-conductor arm [--project NAME]
|
|
2228
2279
|
omp-conductor disarm [--project NAME]
|
|
@@ -2255,7 +2306,7 @@ omp-conductor help
|
|
|
2255
2306
|
| --- | --- | --- |
|
|
2256
2307
|
| `setup [area] [--no-ai] [--answers FILE] [--save-answers FILE] [--project NAME]` | project | The deterministic interview, with styled Clack prompts on an interactive TTY and byte-stable plain output for pipes or `OMP_CONDUCTOR_PLAIN_UI=1`. `--answers` validates a JSON object of stable prompt keys before setup and replaces every prompt; a missing required key exits `1` naming the key and file instead of hanging. `--save-answers` records accepted interactive answers as replayable JSON after a successful run. Bare setup 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. |
|
|
2257
2308
|
| `setup host [--project NAME]` | host | Re-render and stage the systemd unit, then **run** the install: `install -m 0644` into `/etc/systemd/system`, `daemon-reload`, `enable`, `restart`. Stages the fleet recovery oneshot (`omp-conductor-recover.service`) and its playbook (`/usr/local/sbin/omp-conductor-recover`) alongside, and installs them **before** the fleet units: both fleet units carry `OnFailure=` to the recovery unit, so a crash-looped daemon or herdr session now collects evidence durably, attempts one bounded recovery, and pages tier-2 instead of dying silently (#485). 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. |
|
|
2258
|
-
| `setup graph [--no-seed] [--print] [--project NAME]` | project | 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
|
|
2309
|
+
| `setup graph [--no-seed] [--print] [--project NAME]` | project | 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); stage this project's `cbm-reindex-<project>.{sh,service,timer}` **after** the confirm — never before, so a declined run leaves the staged tree byte-identical — refusing a stem that already belongs to another project or to a file it did not generate (#720); `git clone` each missing index-only checkout **as you, never through sudo**; install and enable `cbm-reindex-<project>.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). |
|
|
2259
2310
|
| `start` | host | Start `herdr-fleet.service` when that optional unit is installed, clearing a previous pane-recovery pin, then start the dispatch daemon and wait until it answers `GET /healthz`. When `omp-conductor.service` is installed, systemd is the only start path: even `start --project NAME` restores the shared unit and uses the name only to verify that `/healthz` serves the requested project. A detached daemon is allowed only when the unit is proven absent. It never clears pause or arms ticks. Refuses if a daemon is already live, naming its pid; manager refusal or unprovable ownership is an error rather than a detached fallback. |
|
|
2260
2311
|
| `stop` | fleet | 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. |
|
|
2261
2312
|
| `restart [--now] [--timeout SECONDS] [--port N] [--project NAME]` | host | 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. A daemon serving multiple configured projects makes restart host-wide: `--project` is rejected because draining one queue and restarting the shared process would kill another project's workers. Prefer `systemctl restart` when the unit owns the live pid so the replacement stays supervised; only a host proven not to have the installed unit may fall back to the standalone stop/start path. `--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). |
|
|
@@ -2264,6 +2315,7 @@ omp-conductor help
|
|
|
2264
2315
|
| `ledger [--issue N] [--limit N] [--json]` | project | 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. `--json` emits one stable object with `project`, optional `issue`, `entries`, `refused`, and `turnOverrides`. Recent verb refusals and pending turn overrides also appear in `status`. |
|
|
2265
2316
|
| `board [--project NAME]` | fleet | 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. |
|
|
2266
2317
|
| `hold [--keep-ticks] [--project NAME]` | fleet | Soft stop: pause claiming **and** disarm ticks. Daemon and pane stay up. Prefer this when the intent is "stop the conductor" without killing processes. `--keep-ticks` pauses claiming but leaves the arm marker, so the heartbeat keeps reporting and `resume` alone restores the fleet — no fresh arm challenge. See [Stop the conductor](README.md#stop-the-conductor-hold--stop). |
|
|
2318
|
+
| `drain start --until ISO\|DURATION [--reason TEXT]` / `drain status` / `drain cancel` | project | Start, inspect, or cancel the project's self-expiring admission fence (#484): a durable, bounded alternative to queue-label churn before a release. `start` writes the project's drain record through the landed `createDrain` — new claims pause while active runs settle, and admission resumes automatically at the absolute deadline even if the orchestrator dies. A successful mediated `conductor_release` also clears the drain once the terminal release act completes — version-bump preparation stays latched until the tag is actually cut — so the release window ends with the release itself rather than latching until the deadline (#791). `--until` takes an ISO instant or a relative duration (`90s`, `45m`, `2h`, `1d`) that must be bounded and in the future; a missing, unparseable, unbounded, or past expiry exits `2` before any state changes. `--reason` (1–500 characters) is persisted on the record. `status` reports inactive, or the active drain's creation time, absolute expiry, reason, and remaining active runs from the structured status snapshot. `cancel` removes only the named project's drain and is idempotent. The drain never touches the pause sentinel, the arm marker, or any queue label — it is a file record, not a hold. |
|
|
2267
2319
|
| `stop [--pane] [--project NAME]` | fleet | 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`. |
|
|
2268
2320
|
| `arm [--project NAME]` | fleet | 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`. |
|
|
2269
2321
|
| `disarm [--project NAME]` | fleet | 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. |
|
|
@@ -2272,7 +2324,7 @@ omp-conductor help
|
|
|
2272
2324
|
| `worker pause <issue>` / `worker resume <issue>` | project | 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. |
|
|
2273
2325
|
| `worker stop <issue> --reason TEXT [--project NAME]` | project | 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. |
|
|
2274
2326
|
| `unblock <issue> [--force] [--no-requeue]` | project | 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](#how-one-tick-works): 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. |
|
|
2275
|
-
| `verb <conductor_*> [--arg k=v ...]` | project | 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
|
|
2327
|
+
| `verb <conductor_*> [--arg k=v ...]` | project | 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`), recover a settled run's missing PR (`conductor_pr_recover`), 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…
|
|
2276
2328
|
| `friction <kind> --detail TEXT [--issue N]` | project | 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. |
|
|
2277
2329
|
| `report --text TEXT [--kind material|digest|tier2|decision-needed|fleet-stopped|confirmed-failure]` | project | 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`. The remaining kinds declare the report's interrupt category — the escalation handoff: the reporting policy decides between immediate delivery and a durable hold exactly as for a daemon escalation of that category, A repeated identical call exits `2` only while the earlier handoff is still queued undelivered; once it lands, the same text is admitted again (the handoff state decides, not a permanent ledger). Anything still owed appears in `status` with its age. |
|
|
2278
2330
|
| `decision open --question TEXT [--blocks TEXT] [--resolves-when COND]` | project | 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-136). |
|
|
@@ -2355,7 +2407,7 @@ curl -s localhost:8787/healthz
|
|
|
2355
2407
|
"configured": true,
|
|
2356
2408
|
"status": "degraded",
|
|
2357
2409
|
"checkedAt": "2026-08-08T13:00:00.000Z",
|
|
2358
|
-
"prerequisites": { "indexer": "present", "mcpMount": "
|
|
2410
|
+
"prerequisites": { "indexer": "present", "mcpMount": "unconfigured" },
|
|
2359
2411
|
"repos": [
|
|
2360
2412
|
{
|
|
2361
2413
|
"name": "api",
|
|
@@ -2507,10 +2559,12 @@ daemon, across a process boundary, not in a prompt the model can rewrite.
|
|
|
2507
2559
|
| --- | --- | --- |
|
|
2508
2560
|
| `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. |
|
|
2509
2561
|
| `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`. |
|
|
2562
|
+
| `conductor_pr_review` | **orchestrator only** | The PR is one a run of this project opened; the run is in a revisable settled state (`pushed-green`, or `failed`/`killed` after pushing green); the live head still equals the reviewed head and the checks at it are green; no review revision is already in flight; the review-round ceiling has not been reached. |
|
|
2563
|
+
| `conductor_pr_recover` | **orchestrator only** | The target issue has a recorded terminal run whose branch still exists at its exact recorded 40-hex head; the run is not live and not merged; the issue is open; no open PR already closes the issue at that head, and a recorded PR is open (returned), closed/merged (refused), definitively missing (replaced) or unreadable (retryable). Creates the one missing PR from that branch to the configured `defaultBranch` and records it on the run. |
|
|
2510
2564
|
| `conductor_pr_status` | worker or orchestrator | Read-only — nothing to gate. A worker reads only its own run's PR; an orchestrator may name any syntactically valid PR URL, open, merged, or closed, and gets its live state and head (checks are reported when available; a merged or closed PR reports its state instead of an `expected OPEN` refusal). |
|
|
2511
2565
|
| `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. |
|
|
2512
2566
|
| `conductor_pr_merge` | **orchestrator only** | Ordinarily, `authority.merge` equals the caller. A hand-edited `recoveryMerges` entry may instead authorize one exact unrecorded PR/head/reason while held. In both paths, `headSha` equals the live head *at execution time*; checks are green at that same SHA; the project route and migration chain are valid; the project's single merge slot is free. |
|
|
2513
|
-
| `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. |
|
|
2567
|
+
| `conductor_label` | **orchestrator only** | The label is in the project's own vocabulary. Lifecycle labels stay the daemon's. Adding the queue label echoes the parsed [file lane](#the-file-lane-declaration), or refuses with `file-lane-unparseable` when a clearly delimited write-lane section parsed nothing — it never claims fail-open beside a declaration that was actually attempted. |
|
|
2514
2568
|
| `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. `version-bump-pr` creates or re-validates one deterministic version-only PR and, on a later call, merges only its exact green head through the project's single merge slot. |
|
|
2515
2569
|
| `conductor_install` | **orchestrator only** | Gated like a release act: the `install` shape defaults to `human` and a grant is what moves it. The daemon refuses a version npm does not expose with a full `gitHead`, refuses while another install is still in flight, and otherwise starts a detached transient unit that pauses, drains, installs the CLI/omp plugin/Herdr plugin and reloads — outside this session and the daemon. The unit never declares its own success; the first tick after the restart verifies and reports through the durable outbox. |
|
|
2516
2570
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: to-spec
|
|
3
|
+
description: Groom exactly ONE backlog candidate into a strict, source-verified to-spec verdict. Read-only: reads the candidate and the authoritative source, returns one fenced JSON block matching the to-spec schema. NEVER use for implementation or edits.
|
|
4
|
+
tools: read, grep, glob, web_search
|
|
5
|
+
read-summarize: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# to-spec — groom one backlog candidate into a verified, to-spec result
|
|
9
|
+
|
|
10
|
+
You are grooming exactly ONE backlog candidate for a conductor fleet: you read
|
|
11
|
+
candidate and source, and you return one strict JSON verdict. You never change
|
|
12
|
+
anything. You have reading tools only — no shell, no editor, no GitHub verbs,
|
|
13
|
+
no task spawning, no label changes. The whole of your work is the structured
|
|
14
|
+
result below, and the fleet treats anything else as a failed grooming pass.
|
|
15
|
+
|
|
16
|
+
## The candidate
|
|
17
|
+
|
|
18
|
+
Your item's task text names the candidate: the tracker issue number and title,
|
|
19
|
+
the issue body, and the authoritative source repository and ref. Read the
|
|
20
|
+
issue's premise against the code *in that source, at that ref* — never from
|
|
21
|
+
memory, never from another checkout, never from the issue alone. If you cannot
|
|
22
|
+
reach a source you trust, say so through the verdict (`BLOCKED` names what
|
|
23
|
+
failed) — never an invented fallback source.
|
|
24
|
+
|
|
25
|
+
## The verdict
|
|
26
|
+
|
|
27
|
+
Exactly one of these five strings, nothing else:
|
|
28
|
+
|
|
29
|
+
- `ALREADY DONE` — the work already exists in the source (a later epic retired
|
|
30
|
+
the issue's premise counts as done; prove it with the symbol/file, never the
|
|
31
|
+
title).
|
|
32
|
+
- `PROMOTABLE` — well-specified, fits one worker budget, and the acceptance
|
|
33
|
+
criteria are checkable; carries the proposed brief.
|
|
34
|
+
- `NEEDS DECOMPOSITION` — the plan is real but too big for one budget; say what
|
|
35
|
+
slices it splits into and why each is a separate slice.
|
|
36
|
+
- `BLOCKED` — a named open prerequisite, lane, or credential gap stands in the
|
|
37
|
+
way.
|
|
38
|
+
- `NEEDS PRODUCT DECISION` — the issue cannot proceed until a human decides
|
|
39
|
+
product shape, slice order, or scope; state the one question that unblocks it.
|
|
40
|
+
|
|
41
|
+
## The return contract
|
|
42
|
+
|
|
43
|
+
Answer in **one fenced JSON block, nothing else after it**. Every field is
|
|
44
|
+
required and no extra keys are accepted:
|
|
45
|
+
|
|
46
|
+
- `verdict` — one of the five strings above.
|
|
47
|
+
- `routing` — exactly one `owner/repo`, or `"MULTI"`.
|
|
48
|
+
- `routingSplit` — required iff `routing` is `"MULTI"`: what each slice goes to.
|
|
49
|
+
- `source` — `{ name, ref, freshAt }`: the authoritative source you read, the
|
|
50
|
+
exact ref, and `freshAt` = epoch milliseconds when you actually observed it.
|
|
51
|
+
Conductor refuses results whose source is older than 24 hours or missing
|
|
52
|
+
name/ref/freshAt — an unsourced verdict is not grooming, it is prose.
|
|
53
|
+
- `laterWorkInvalidates` — boolean: did later work (an epic committed after
|
|
54
|
+
this candidate was filed) retire its premise?
|
|
55
|
+
- `laterWorkNote` — what you searched for that check and what you found. Even
|
|
56
|
+
when false this must name the search, so "false" cannot be written without
|
|
57
|
+
looking.
|
|
58
|
+
- `entryPoints` — 3–6 files to change or read first, the discovery a worker's
|
|
59
|
+
budget dies on when absent.
|
|
60
|
+
- `existingTests` — tests that already exercise the behaviour, by path; `[]`
|
|
61
|
+
when you found none.
|
|
62
|
+
- `likelySilentFake` — the one thing most likely to be silently faked while
|
|
63
|
+
implementing, and how to prove it is not.
|
|
64
|
+
- `proofCommands` — the focused commands that prove the work, each with its
|
|
65
|
+
`cwd` when it matters.
|
|
66
|
+
- `fileLane` — the files and directories this slice writes; `dependencies` —
|
|
67
|
+
open prerequisites, `[]` when none.
|
|
68
|
+
- `proposedBrief` — required iff `verdict` is `PROMOTABLE`: the brief a worker
|
|
69
|
+
would be dispatched with, including the silent fake and the proof commands.
|
|
70
|
+
- `reasonNotToPromote` — required for every other verdict: why this must not
|
|
71
|
+
be promoted.
|
|
72
|
+
|
|
73
|
+
## Three traps, each of which produces a confidently wrong verdict
|
|
74
|
+
|
|
75
|
+
- **Prose is not evidence.** A verdict without the source-backed contract is
|
|
76
|
+
refused as malformed: every field above is required, and `source` must name
|
|
77
|
+
the ref you read and when.
|
|
78
|
+
- **Stale source reads like good source.** Judge the candidate against the
|
|
79
|
+
stated ref as it is now; a verdict drawn from memory of a different clone is
|
|
80
|
+
stale and will be refused.
|
|
81
|
+
- **A later epic retires the premise.** On anything old, check whether later
|
|
82
|
+
open work invalidated the candidate before concluding anything else. That
|
|
83
|
+
check is mechanical, read-only, and exactly what you are cheap at —
|
|
84
|
+
`laterWorkNote` must name what you searched.
|
|
85
|
+
|
|
86
|
+
## Answer
|
|
87
|
+
|
|
88
|
+
One fenced JSON block, nothing else after it. Anything unparseable or
|
|
89
|
+
off-schema is persisted as `blocked(malformed)` and the candidate counts as
|
|
90
|
+
not groomed — a refusal is a failed grooming, not a free pass to skip it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
23
23
|
"!src/**/*.test.ts",
|
|
24
|
+
"agents",
|
|
24
25
|
"systemd",
|
|
25
26
|
"schema",
|
|
26
27
|
"README.md",
|
|
@@ -99,6 +99,35 @@
|
|
|
99
99
|
"type": "string",
|
|
100
100
|
"minLength": 1
|
|
101
101
|
},
|
|
102
|
+
"host": {
|
|
103
|
+
"description": "Host facts every worker brief renders; absent renders no section",
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"description": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"minLength": 1,
|
|
109
|
+
"description": "What this host is and what else it runs"
|
|
110
|
+
},
|
|
111
|
+
"path": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"minLength": 1,
|
|
114
|
+
"description": "The non-interactive PATH a script or `ssh host \"<cmd>\"` invocation must export"
|
|
115
|
+
},
|
|
116
|
+
"conventions": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"propertyNames": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"minLength": 1
|
|
121
|
+
},
|
|
122
|
+
"additionalProperties": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"minLength": 1
|
|
125
|
+
},
|
|
126
|
+
"description": "Per-repo command conventions, keyed by the brief's `owner/repo` slug"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"additionalProperties": false
|
|
130
|
+
},
|
|
102
131
|
"projects": {
|
|
103
132
|
"minItems": 1,
|
|
104
133
|
"type": "array",
|
|
@@ -136,7 +165,8 @@
|
|
|
136
165
|
"properties": {
|
|
137
166
|
"inProgress": {},
|
|
138
167
|
"blocked": {},
|
|
139
|
-
"failed": {}
|
|
168
|
+
"failed": {},
|
|
169
|
+
"backlog": {}
|
|
140
170
|
},
|
|
141
171
|
"additionalProperties": false
|
|
142
172
|
},
|
|
@@ -340,6 +370,28 @@
|
|
|
340
370
|
"additionalProperties": false,
|
|
341
371
|
"description": "How `arm` proves a human just approved arming"
|
|
342
372
|
},
|
|
373
|
+
"review": {
|
|
374
|
+
"type": "object",
|
|
375
|
+
"properties": {
|
|
376
|
+
"strictness": {
|
|
377
|
+
"default": "medium",
|
|
378
|
+
"type": "string",
|
|
379
|
+
"enum": [
|
|
380
|
+
"low",
|
|
381
|
+
"medium",
|
|
382
|
+
"high"
|
|
383
|
+
]
|
|
384
|
+
},
|
|
385
|
+
"maxRounds": {
|
|
386
|
+
"default": 3,
|
|
387
|
+
"type": "integer",
|
|
388
|
+
"minimum": 1,
|
|
389
|
+
"maximum": 6
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
"additionalProperties": false,
|
|
393
|
+
"description": "Review strictness and round ceiling for green PRs"
|
|
394
|
+
},
|
|
343
395
|
"authority": {
|
|
344
396
|
"type": "object",
|
|
345
397
|
"properties": {
|