omp-conductor 0.18.0 → 0.18.2
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 +35 -1
- package/REFERENCE.md +61 -11
- package/agents/to-spec.md +94 -0
- package/package.json +2 -1
- package/schema/config.schema.json +35 -1
- package/src/admission.ts +204 -75
- package/src/arm-challenge.ts +250 -57
- package/src/ask.ts +268 -7
- package/src/board.ts +17 -3
- package/src/briefs/orchestrator.md +62 -21
- package/src/briefs/to-spec.md +88 -0
- package/src/briefs/worker.md +2 -1
- package/src/cli.ts +124 -1
- package/src/command-help.ts +11 -0
- package/src/command-manifest.ts +38 -5
- package/src/commands/arm.ts +1 -1
- package/src/commands/context.ts +1 -0
- package/src/commands/drain.ts +176 -0
- package/src/commands/extend.ts +6 -10
- package/src/commands/intake.ts +4 -19
- package/src/commands/status.ts +5 -1
- package/src/commands/watch.ts +51 -16
- package/src/commands/worker.ts +9 -10
- package/src/config-schema.ts +43 -6
- package/src/config.ts +65 -9
- package/src/daemon.ts +879 -41
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/server.ts +5 -2
- package/src/decisions.ts +243 -17
- package/src/diff-flags.ts +75 -1
- package/src/doctor.ts +60 -82
- package/src/escalate.ts +31 -14
- package/src/failure-class.ts +28 -2
- package/src/fleet.ts +239 -240
- package/src/gitops.ts +188 -81
- package/src/graph-health.ts +35 -1
- package/src/graph.ts +66 -1
- package/src/harness-loader.ts +59 -0
- package/src/host.ts +242 -2
- package/src/lifecycle.ts +122 -1
- package/src/omp-settings.ts +19 -0
- package/src/omp.ts +183 -21
- package/src/orchestrator-tick.ts +1591 -32
- package/src/orchestrator.ts +12 -0
- package/src/privileged.ts +1 -4
- package/src/release-policy.ts +503 -9
- package/src/session-host.ts +65 -6
- package/src/settlement.ts +69 -17
- package/src/setup-host.ts +1225 -9
- package/src/setup-install.ts +28 -0
- package/src/setup-wizard.ts +154 -3
- package/src/setup.ts +83 -17
- package/src/shell.ts +15 -0
- package/src/status-render.ts +216 -12
- package/src/store.ts +443 -42
- package/src/to-spec.ts +408 -0
- package/src/tracker/github.ts +104 -14
- package/src/types.ts +405 -19
- 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 +765 -56
- package/src/verbs/socket.ts +24 -5
- package/src/worker.ts +12 -2
- package/src/worktree.ts +29 -12
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:
|
|
@@ -282,7 +316,7 @@ Two words, and one of them takes a flag:
|
|
|
282
316
|
| `stop` | paused | disarmed | stopped (systemctl-aware) | left running |
|
|
283
317
|
| `stop --pane` | paused | disarmed | stopped | stopped + recovery pinned off |
|
|
284
318
|
|
|
285
|
-
`resume` clears pause **and** any `stop --pane` recovery pin, and **never re-arms**. `arm` is proof-gated: it sends a Telegram challenge and writes the arm marker only after your reply
|
|
319
|
+
`resume` clears pause **and** any `stop --pane` recovery pin, and **never re-arms**. `arm` is proof-gated: it sends a Telegram challenge and writes the arm marker only after the orchestrator's inbound adapter has acknowledged your reply in conductor's own state — no transcript is read, so where (or whether) a session file lives does not matter (#614). `stop --pane` targets the configured conductor agent only — it does **not** run `systemctl stop herdr-fleet`. To bounce the daemon without stopping the fleet, use `restart`.
|
|
286
320
|
|
|
287
321
|
**Removed in 0.15.0**, each exiting `2` with a pointer: `halt` (now `stop`), `pause` (use `hold`), `release-pane` (now part of `resume`), and `graph-setup` (now [`setup graph`](https://github.com/TerrifiedBug/conductor/blob/main/omp/REFERENCE.md#code-graph-discovery)). Dropping `pause` cost one real capability, "stop claiming but keep ticking", which is now `hold --keep-ticks` rather than a fifth verb. It matters because disarming is the expensive half of a hold: re-arming sends a Telegram challenge and blocks until you answer it in the chat, so stopping claims for ten minutes otherwise costs a manual round trip to get the heartbeat back — while the workers a hold deliberately leaves running have nothing shepherding them.
|
|
288
322
|
|
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
|
|
@@ -1420,7 +1454,7 @@ conditions, each one something this package can check without asking you:
|
|
|
1420
1454
|
| Condition | Met when |
|
|
1421
1455
|
| --- | --- |
|
|
1422
1456
|
| `pr-merged:<https url>` | `gh` reports that pull request merged. |
|
|
1423
|
-
| `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). |
|
|
1424
1458
|
| `pr-mergeable:<https url>` | The pull request is mergeable (`clean`, not `unknown` or conflicting). |
|
|
1425
1459
|
| `issue-closed:<number>` | That issue is closed on the tracker. |
|
|
1426
1460
|
| `npm-version:<pkg>@<version>` | `npm view <pkg>@<version> version` succeeds — the version is published. |
|
|
@@ -1434,10 +1468,11 @@ arm/channel/pending single-flight) instead of waiting a full interval. The poke
|
|
|
1434
1468
|
reason and the digest flag `[CONDITION MET — act on this now]` both surface the
|
|
1435
1469
|
wake so the session acts when the answer becomes actionable. Repeated sweeps
|
|
1436
1470
|
while the condition stays true do nothing further — the store marks the
|
|
1437
|
-
transition once.
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
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.
|
|
1441
1476
|
|
|
1442
1477
|
Anything else exits `2` and lists the six forms. An unparseable condition on an
|
|
1443
1478
|
existing row is *listed and never treated as met*: a grammar a future release
|
|
@@ -1944,10 +1979,19 @@ injected tick the extension refuses it and mounts its own bounded surface,
|
|
|
1944
1979
|
|
|
1945
1980
|
- The call carries `question`, `on-timeout` (`auto-proceed` or `park`, required),
|
|
1946
1981
|
and optionally `timeoutSeconds`, `blocks`, `recommended`, `options` and
|
|
1947
|
-
`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.
|
|
1948
1985
|
- The tool records a decision row first (durable, seven-day expiry), then
|
|
1949
|
-
delivers the question
|
|
1950
|
-
|
|
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).
|
|
1951
1995
|
- It waits at most the ceiling: `timeoutSeconds` if the ask names one, else the
|
|
1952
1996
|
tick config's `askTimeoutSeconds`, else 300 seconds — always capped at the
|
|
1953
1997
|
turn budget, so the ask can never outlive the turn it runs in. An ask issued
|
|
@@ -2227,6 +2271,9 @@ omp-conductor doctor [--project NAME] [--json] [--probe-telegram]
|
|
|
2227
2271
|
omp-conductor ledger [--issue N] [--limit N] [--project NAME] [--json]
|
|
2228
2272
|
omp-conductor board [--project NAME] [--json]
|
|
2229
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]
|
|
2230
2277
|
omp-conductor stop [--pane] [--project NAME]
|
|
2231
2278
|
omp-conductor arm [--project NAME]
|
|
2232
2279
|
omp-conductor disarm [--project NAME]
|
|
@@ -2268,15 +2315,16 @@ omp-conductor help
|
|
|
2268
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`. |
|
|
2269
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. |
|
|
2270
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. |
|
|
2271
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`. |
|
|
2272
|
-
| `arm [--project NAME]` | fleet | Proof-gated: send a Telegram challenge and write this project's arm marker only after your reply
|
|
2320
|
+
| `arm [--project NAME]` | fleet | Proof-gated: send a Telegram challenge and write this project's arm marker only after the orchestrator's inbound adapter has acknowledged your reply in conductor's own state — no transcript is read, so where (or whether) a session file lives does not matter (#614). The challenge names the project, so a host running two fleets is not ambiguous. Never auto-armed by `resume` / `hold`. |
|
|
2273
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. |
|
|
2274
2322
|
| `tail <issue>` | project | 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>`. |
|
|
2275
2323
|
| `extend <issue> --turns N [--project NAME]` | project | Raise a live worker's effective turn ceiling through its owning daemon without restarting its session. If the latest run is failed, killed, orphaned, or blocked and has no live controller, store a one-shot ceiling for that issue's next claimed attempt instead. A next-attempt value must exceed the project base, every extension must stay at or below `workerMaxTurnsCeiling`, and live extensions remain monotonic. The pending value appears in `status`, is recorded in `ledger`, and is consumed atomically by one claim. |
|
|
2276
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. |
|
|
2277
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. |
|
|
2278
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. |
|
|
2279
|
-
| `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…
|
|
2280
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. |
|
|
2281
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. |
|
|
2282
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). |
|
|
@@ -2511,10 +2559,12 @@ daemon, across a process boundary, not in a prompt the model can rewrite.
|
|
|
2511
2559
|
| --- | --- | --- |
|
|
2512
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. |
|
|
2513
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. |
|
|
2514
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). |
|
|
2515
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. |
|
|
2516
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. |
|
|
2517
|
-
| `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. |
|
|
2518
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. |
|
|
2519
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. |
|
|
2520
2570
|
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
- `evidence` — the files/symbols that prove the verdict. Required for
|
|
54
|
+
`ALREADY DONE`: name the symbol/file that already does the work, never a
|
|
55
|
+
title match. Welcome on every other verdict.
|
|
56
|
+
- `laterWorkInvalidates` — boolean: did later work (an epic committed after
|
|
57
|
+
this candidate was filed) retire its premise?
|
|
58
|
+
- `laterWorkNote` — what you searched for that check and what you found. Even
|
|
59
|
+
when false this must name the search, so "false" cannot be written without
|
|
60
|
+
looking.
|
|
61
|
+
- `entryPoints` — 3–6 files to change or read first, the discovery a worker's
|
|
62
|
+
budget dies on when absent.
|
|
63
|
+
- `existingTests` — tests that already exercise the behaviour, by path; `[]`
|
|
64
|
+
when you found none.
|
|
65
|
+
- `likelySilentFake` — the one thing most likely to be silently faked while
|
|
66
|
+
implementing, and how to prove it is not.
|
|
67
|
+
- `proofCommands` — the focused commands that prove the work, each with its
|
|
68
|
+
`cwd` when it matters.
|
|
69
|
+
- `fileLane` — the files and directories this slice writes.
|
|
70
|
+
- `dependencies` — open prerequisite issue numbers, each a bare number
|
|
71
|
+
(`875`) or a string (`"875"`); `[]` when none.
|
|
72
|
+
- `proposedBrief` — required iff `verdict` is `PROMOTABLE`: the brief a worker
|
|
73
|
+
would be dispatched with, including the silent fake and the proof commands.
|
|
74
|
+
- `reasonNotToPromote` — required for every other verdict: why this must not
|
|
75
|
+
be promoted.
|
|
76
|
+
|
|
77
|
+
## Three traps, each of which produces a confidently wrong verdict
|
|
78
|
+
|
|
79
|
+
- **Prose is not evidence.** A verdict without the source-backed contract is
|
|
80
|
+
refused as malformed: every field above is required, and `source` must name
|
|
81
|
+
the ref you read and when.
|
|
82
|
+
- **Stale source reads like good source.** Judge the candidate against the
|
|
83
|
+
stated ref as it is now; a verdict drawn from memory of a different clone is
|
|
84
|
+
stale and will be refused.
|
|
85
|
+
- **A later epic retires the premise.** On anything old, check whether later
|
|
86
|
+
open work invalidated the candidate before concluding anything else. That
|
|
87
|
+
check is mechanical, read-only, and exactly what you are cheap at —
|
|
88
|
+
`laterWorkNote` must name what you searched.
|
|
89
|
+
|
|
90
|
+
## Answer
|
|
91
|
+
|
|
92
|
+
One fenced JSON block, nothing else after it. Anything unparseable or
|
|
93
|
+
off-schema is persisted as `blocked(malformed)` and the candidate counts as
|
|
94
|
+
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.18.
|
|
3
|
+
"version": "0.18.2",
|
|
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",
|
|
@@ -358,10 +387,15 @@
|
|
|
358
387
|
"type": "integer",
|
|
359
388
|
"minimum": 1,
|
|
360
389
|
"maximum": 6
|
|
390
|
+
},
|
|
391
|
+
"adjudicator": {
|
|
392
|
+
"default": "task",
|
|
393
|
+
"type": "string",
|
|
394
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"
|
|
361
395
|
}
|
|
362
396
|
},
|
|
363
397
|
"additionalProperties": false,
|
|
364
|
-
"description": "Review strictness
|
|
398
|
+
"description": "Review strictness, round ceiling and adjudicator role for green PRs"
|
|
365
399
|
},
|
|
366
400
|
"authority": {
|
|
367
401
|
"type": "object",
|