omp-conductor 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -19
- package/package.json +1 -1
- package/src/board.ts +1 -1
- package/src/briefs/orchestrator.md +32 -2
- package/src/cli.ts +104 -23
- package/src/config.ts +10 -2
- package/src/daemon.ts +818 -131
- package/src/diff-flags.ts +4 -0
- package/src/escalate.ts +5 -5
- package/src/failure-class.ts +7 -5
- package/src/fleet.ts +7 -2
- package/src/omp.ts +8 -4
- package/src/orchestrator-tick.ts +35 -20
- package/src/orchestrator.ts +3 -2
- package/src/plugin.ts +28 -2
- package/src/release-policy.ts +66 -6
- package/src/session-host.ts +4 -3
- package/src/setup.ts +4 -2
- package/src/store.ts +190 -29
- package/src/tracker/github.ts +261 -56
- package/src/types.ts +141 -28
- package/src/verbs/actions.ts +127 -15
- package/src/verbs/protocol.ts +7 -6
- package/src/verbs/server.ts +95 -13
- package/src/worker.ts +33 -5
- package/src/worktree.ts +5 -0
package/README.md
CHANGED
|
@@ -989,7 +989,8 @@ rest. `0` is a real value (a hard stop), not "unset".
|
|
|
989
989
|
| `maxConcurrentWorkersPerRepo` | `1` | Max live workers in the **same repo**. The mirror, branch-protection staleness and shared CI egress are all per-repo collision domains, so extra slots should land on other repos. Raise it only when a repo genuinely needs two workers at once. |
|
|
990
990
|
| `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`. |
|
|
991
991
|
| `planUsage` | `null` (unmetered) | Subscription/plan allowance guard: `{ "windowId": "anthropic:7d", "maxUsedFraction": 0.85 }`, or `null` for no plan gate. Independent of `dailySpendUsd` — see [Plan allowance](#plan-allowance-planusage) below. |
|
|
992
|
-
| `workerMaxTurns` | `120` |
|
|
992
|
+
| `workerMaxTurns` | `120` | Base ceiling for each new worker. Catches a session looping without converging; use `omp-conductor extend` to raise one live run or one issue's next attempt without changing this default. |
|
|
993
|
+
| `workerMaxTurnsCeiling` | `240` (twice the effective `workerMaxTurns` when omitted) | Upper bound for per-issue turn extensions. Prevents the loopback control from granting an unbounded worker budget. |
|
|
993
994
|
| `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. |
|
|
994
995
|
| `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. |
|
|
995
996
|
| `maxContinuationsPerIssue` | `2` | Cap-kill, daemon-orphan and answered-block resumes before escalation. This independently bounds crash/resume loops. |
|
|
@@ -1005,19 +1006,31 @@ Work resumes only after `omp-conductor resume` (or `/conductor resume`).
|
|
|
1005
1006
|
`workerMaxTurns` and `workerWallClockMs` are enforced inside the session driver.
|
|
1006
1007
|
The daemon reads a live run's effective turn ceiling at every turn boundary. Use
|
|
1007
1008
|
`omp-conductor extend <issue> --turns N [--project NAME]` to raise it without
|
|
1008
|
-
restarting or reconstructing the session.
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1009
|
+
restarting or reconstructing the session. For a live worker, extension is
|
|
1010
|
+
monotonic: equal or lower values are refused. If the latest run is failed,
|
|
1011
|
+
killed, orphaned, or blocked, the command instead stores a one-shot ceiling for
|
|
1012
|
+
that issue's next attempt. A next-attempt ceiling must exceed the effective
|
|
1013
|
+
project base, and every extension must stay at or below
|
|
1014
|
+
`workerMaxTurnsCeiling`. `status` shows both active ceilings and pending
|
|
1015
|
+
next-attempt overrides. The store consumes an override atomically when it claims
|
|
1016
|
+
the next run, so later attempts return to the project base. Config edits change
|
|
1017
|
+
that base on the next tick but do not change workers already in flight. A cap
|
|
1018
|
+
that fires aborts the run, records it as `killed`, and names the ceiling in the
|
|
1019
|
+
escalation.
|
|
1014
1020
|
|
|
1015
1021
|
Pause one live worker cooperatively with
|
|
1016
1022
|
`omp-conductor worker pause <issue> [--project NAME]`. The daemon aborts the
|
|
1017
1023
|
active turn to an idle harness state, freezes the remaining wall-clock budget,
|
|
1018
1024
|
and keeps the run in the Running lane. `omp-conductor worker resume <issue>`
|
|
1019
1025
|
continues the same session with a prompt to re-check its last action before
|
|
1020
|
-
proceeding.
|
|
1026
|
+
proceeding. To end that run instead, use
|
|
1027
|
+
`omp-conductor worker stop <issue> --reason TEXT [--project NAME]`. Stop works
|
|
1028
|
+
from running or paused, salvages dirty work before removing the worktree, records
|
|
1029
|
+
the distinct terminal `stopped` state, and removes `agent:in-progress` through
|
|
1030
|
+
the label outbox. A salvage failure keeps the only copy in place and reports its
|
|
1031
|
+
path. Stopped runs consume neither implementation-failure nor continuation
|
|
1032
|
+
budget. Repeating stop reports the already-terminal state. These worker controls
|
|
1033
|
+
are separate from fleet-level `pause`, which stops new claims.
|
|
1021
1034
|
|
|
1022
1035
|
### Plan allowance (`planUsage`)
|
|
1023
1036
|
|
|
@@ -1724,7 +1737,7 @@ policy instead of restating it — no threshold lives in two places.
|
|
|
1724
1737
|
|
|
1725
1738
|
| Field | Values | Default | Means |
|
|
1726
1739
|
| --- | --- | --- | --- |
|
|
1727
|
-
| `requires` | `runs-settled`, `no-open-prs`, `queue-drained`, `epic-children-closed` | `["runs-settled"]` | What must already have landed. Order and duplicates do not matter; the loader canonicalises. |
|
|
1740
|
+
| `requires` | `runs-settled`, `no-open-prs`, `queue-drained`, `base-branch-green`, `epic-children-closed` | `["runs-settled"]` | What must already have landed. `base-branch-green` requires the newest observed post-merge workflow verdict for that routed repository to be green; pending, unknown, red, or no observation refuses release. Order and duplicates do not matter; the loader canonicalises. |
|
|
1728
1741
|
| `requiredChecks` | any check names | `[]` | Checks that must be green on the branch being released. Empty means every check it reports. |
|
|
1729
1742
|
| `artefacts` | any names | `[]` | The packages or images this project releases. **Empty denies**: nothing has been authorised to ship. |
|
|
1730
1743
|
| `environments` | any names | `[]` | Deploy targets. **Empty denies** every environment. |
|
|
@@ -1991,6 +2004,7 @@ omp-conductor tail <issue> [--project NAME]
|
|
|
1991
2004
|
omp-conductor extend <issue> --turns N [--project NAME]
|
|
1992
2005
|
omp-conductor worker pause <issue> [--project NAME]
|
|
1993
2006
|
omp-conductor worker resume <issue> [--project NAME]
|
|
2007
|
+
omp-conductor worker stop <issue> --reason TEXT [--project NAME]
|
|
1994
2008
|
omp-conductor unblock <issue> [--force] [--no-requeue] [--project NAME]
|
|
1995
2009
|
omp-conductor verb <conductor_*> [--project NAME] [--arg k=v ...]
|
|
1996
2010
|
omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
|
|
@@ -2014,7 +2028,7 @@ omp-conductor help
|
|
|
2014
2028
|
| `restart [--now] [--timeout SECONDS] [--port N] [--project NAME]` | Drains the fleet by default: pause new claims, wait until live workers reach `0 / N` (bounded by `--timeout SECONDS`, default 1800 = 30 min), restart, then restore the prior dispatch state. Prefer `systemctl restart` when the unit owns the live pid so the replacement stays supervised; otherwise `stop` then `start`, inheriting the running daemon's port and project unless a flag overrides them. `--now` skips the drain and restarts immediately, orphaning any live runs (old behaviour). A drain that hits `--timeout` restarts nothing and leaves dispatch paused — `omp-conductor resume` lifts it, or re-run `restart` to keep waiting. The new process **salvages dirty live worktrees before orphaning** those rows — see [Deploying a new package onto a busy fleet](#deploying-a-new-package-onto-a-busy-fleet). |
|
|
2015
2029
|
| `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. |
|
|
2016
2030
|
| `status [--project NAME]` | Layered fleet report first: `dispatch` / `ticks` / next scheduled tick / `pane` / `recovery` / `herdr` / `telegram` / `brief` / `decisions` / optional `failure classes` and `code graph` / `daemon`, then the project body. The project body includes the latest completed dispatch timestamp, ready/routed/admitted counts, bounded hold groups, and the GitHub API budget (`graphql` / `core` remaining and reset, in the caps block); API failures are marked `DEGRADED` so queue starvation cannot look idle. The next tick comes from the live heartbeat process, not a guess from log timestamps. Telegram health uses `getMe` to prove API authentication without sending a message and reports inbound bridge configuration separately. Configured graphs report prerequisites, indexed repos, timer state, and refresh freshness without blocking dispatch. A `reports` block lists everything the outbox has not delivered, with its age, and prints `pending` (nobody has it) differently from `SENDING` (outcome unknown, it may already have arrived) — see [Report delivery](#report-delivery-the-outbox). The daemon block includes `rss` from `/healthz`; live workers add a busy-deploy warning. A `.conductor-stalled` marker adds an `orchestrator STALLED since …` line. |
|
|
2017
|
-
| `ledger [--issue N] [--limit N]` |
|
|
2031
|
+
| `ledger [--issue N] [--limit N]` | The action audit: every [mediated-verb](#the-mediated-verbs-126) mutation and every next-attempt turn budget. Verb entries include the arguments, decision, named refusal, and resulting SHA. Turn-budget entries remain after an override is replaced or consumed. Reads (`conductor_pr_status`) are absent so polling cannot bury the signal. `--issue` narrows both histories; `--limit` defaults to 50. Recent verb refusals and pending turn overrides also appear in `status`. |
|
|
2018
2032
|
| `board [--project NAME]` | Live keyboard-driven kanban over the same SQLite and `/healthz` truth as `status`, plus the tracker's current labels: Queue, Claimed, Running, Green, Blocked, Failed, Orphaned, the last 24 hours of Merged and Settled, and Parked (an issue the tracker has not confirmed closed — still open, or a label read that failed — so nothing dispatches it until a human labels it). Columns are mutually exclusive and describe current state, not the newest run row, so a requeued issue is queued rather than failed and a closed issue is neither. Refreshes run/spend/turn values every second, and health plus the label read every ten seconds. `Enter` follows the selected transcript in place; `u` invokes the existing unblock workflow on a Blocked, Failed, or Orphaned card; `i` / `p` open the issue / PR; `r` refreshes health; `?` shows all keys. Requires an interactive terminal of at least 50×20. |
|
|
2019
2033
|
| `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). |
|
|
2020
2034
|
| `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. |
|
|
@@ -2022,8 +2036,9 @@ omp-conductor help
|
|
|
2022
2036
|
| `disarm [--project NAME]` | Remove the arm marker so ticks skip. Processes untouched. |
|
|
2023
2037
|
| `release-pane [--project NAME]` | Clear the `halt --pane` recovery pin so herdr-conductor may resume the fleet agent again. |
|
|
2024
2038
|
| `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>`. |
|
|
2025
|
-
| `extend <issue> --turns N [--project NAME]` |
|
|
2026
|
-
| `worker pause <issue>` / `worker resume <issue>` | Cooperatively park one live worker without changing its run state or lane. Pause aborts the active turn to harness idle and freezes the remaining wall-clock budget; resume continues the same session with a prompt to re-check its last action before repeating it. This is
|
|
2039
|
+
| `extend <issue> --turns N [--project NAME]` | Raise a live worker's effective turn ceiling through its owning daemon without restarting its session. If the latest run is failed, killed, orphaned, or blocked and has no live controller, store a one-shot ceiling for that issue's next claimed attempt instead. A next-attempt value must exceed the project base, every extension must stay at or below `workerMaxTurnsCeiling`, and live extensions remain monotonic. The pending value appears in `status`, is recorded in `ledger`, and is consumed atomically by one claim. |
|
|
2040
|
+
| `worker pause <issue>` / `worker resume <issue>` | Cooperatively park one live worker without changing its run state or lane. Pause aborts the active turn to harness idle and freezes the remaining wall-clock budget; resume continues the same session with a prompt to re-check its last action before repeating it. This is separate from fleet-level `pause`, which refuses new claims and work-starting mutations while allowing pre-pause completion work and releases. |
|
|
2041
|
+
| `worker stop <issue> --reason TEXT [--project NAME]` | Terminally end a running or cooperatively paused worker. The reason is required (1–500 characters) and persisted on the run. The command waits for settlement, records the distinct `stopped` state, salvages and publishes dirty work, removes `agent:in-progress` through the durable label outbox, and consumes neither failed-attempt nor continuation budget. If salvage fails, the tree holding the only copy stays in place and the command names it. Repeating stop is idempotent and reports the run's already-terminal state. |
|
|
2027
2042
|
| `unblock <issue> [--force] [--no-requeue]` | Remove that issue's `blocked` and `failed` labels so an answered escalation can be claimed again, and restore the project queue label by default so the dispatcher actually sees it. `agent:in-progress` comes off too, but only when the newest recorded run is terminal — that row is the proof no worker still owns the issue, so a live run keeps the label (and the queue label stays off until that run settles), and so does an issue with no run row at all. Run history remains intact: blocks consume the independent continuation budget, not failed implementation attempts. The output reports both budgets and warns when either will make the next tick escalate instead of dispatch. The label changes go through the [label projection outbox](#the-tick): they are applied inline before the command returns, but **a tracker that refuses them (403, rate limit) no longer fails the verb** — it exits `0`, the intended label state is durable and the daemon retries it, and the output says `label sync queued (N pending) — the daemon retries` instead of claiming the labels were restored. Safety is preserved, but the issue is only claimable once the queue label itself lands: the queue read asks GitHub for issues carrying that label, so a refused queue-label add keeps the issue out of dispatch until projection succeeds. `--no-requeue` clears the state labels only, leaving the queue label untouched — the case where you are about to close the issue. **Refuses, clearing nothing and exiting `3`, when the newest attempt's work could not be committed and its worktree is the only copy** — re-claiming removes that tree. `--force` records the operator's acceptance on the run row and then clears; the salvage failure stays in history. Exits `2` when the issue number is missing or malformed. |
|
|
2028
2043
|
| `verb <conductor_*> [--arg k=v ...]` | Run one [mediated verb](#the-mediated-verbs-126) as the orchestrator, from the CLI — the external-orchestrator half of the verb surface. Every argument goes in as a `--arg k=v` string; an orchestrator can merge (`conductor_pr_merge`), label (`conductor_label`), release (`conductor_release`), update a branch (`conductor_pr_update_branch`) or title/body (`conductor_pr_update`), or read PR state (`conductor_pr_status`). The daemon applies the same checks and writes the same ledger rows a session's call would; a missing `--arg` is refused exactly as a missing tool argument is, worker-only verbs (`conductor_push`, `conductor_pr_create`) are refused with `role-not-allowed`, and a refusal exits `3`. An unknown verb exits `2`. |
|
|
2029
2044
|
| `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. |
|
|
@@ -2036,7 +2051,7 @@ omp-conductor help
|
|
|
2036
2051
|
| `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. |
|
|
2037
2052
|
| `--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. |
|
|
2038
2053
|
| `--project NAME` | Pick the project to service. One daemon process serves exactly one project; with several configured projects the name is required. |
|
|
2039
|
-
| `pause [--reason TEXT]` | Stop
|
|
2054
|
+
| `pause [--reason TEXT]` | Stop new claims and work-starting mutations only. The running daemon notices on its next tick; runs already in flight finish. The orchestrator may still merge, update, or label runs admitted before the pause, and may release when the release policy's own preconditions hold. The orchestrator heartbeat keeps ticking if armed — its gate is the arm marker, not this flag. Per-worker pause is separate. Prefer `hold` to silence both. `--reason TEXT` is recorded in the pause sentinel, which `status` shows as the pause provenance. |
|
|
2040
2055
|
| `resume` | Clear pause only — does **not** re-arm. Run `arm` after an inbound Telegram proof to resume ticks. |
|
|
2041
2056
|
| `--version`, `-V`, `version` | Print the installed `omp-conductor` package version and exit `0`. Works from the global binary and npm/plugin install because it reads the package metadata beside the shipped CLI. |
|
|
2042
2057
|
| `graph-setup` | Print how to set up the code-graph indexes workers query instead of grepping: a `git clone` for every index-only clone that does not exist yet, the one-shot index command per repo, and a `cbm-reindex.service` + `cbm-reindex.timer` pair generated from the project's own repos and branches. Reads only, so it is safe on a host where you are not root. Exits `1` when no repo in the project has [`graphProject`](#configuration) set, because the fix is a wizard answer rather than a flag. See [Code-graph discovery](#code-graph-discovery). |
|
|
@@ -2049,8 +2064,11 @@ omp-conductor help
|
|
|
2049
2064
|
| `help`, `--help`, `-h` | Print usage. An unknown or missing verb prints it too, and exits `2`. |
|
|
2050
2065
|
|
|
2051
2066
|
Pause is a flag file under the state directory, so it applies to every project and
|
|
2052
|
-
survives a daemon restart.
|
|
2053
|
-
|
|
2067
|
+
survives a daemon restart. It refuses new claims and work-starting mutations,
|
|
2068
|
+
allows completion verbs only for runs admitted before the pause, and leaves
|
|
2069
|
+
`conductor_release` to its normal authority, grant, and precondition checks.
|
|
2070
|
+
Per-worker pause is independent. Hold also removes the arm marker the heartbeat
|
|
2071
|
+
reads, so both brains go quiet without killing processes.
|
|
2054
2072
|
|
|
2055
2073
|
These are available in-session as `/conductor setup`, `/conductor status`,
|
|
2056
2074
|
`/conductor hold`, `/conductor halt [--pane]`, `/conductor arm`, `/conductor disarm`,
|
|
@@ -2210,7 +2228,7 @@ What holds the orchestrator instead:
|
|
|
2210
2228
|
| | |
|
|
2211
2229
|
| --- | --- |
|
|
2212
2230
|
| **The brief** | `ORCHESTRATOR.md`'s hard boundaries — never read or edit a worker's checkout or the mirror cache; when you need a run's code, read its PR. |
|
|
2213
|
-
| **The
|
|
2231
|
+
| **The action ledger** | Every `conductor_*` mutation and operator-selected next-attempt turn budget remains auditable. `omp-conductor ledger` shows both, including refused calls and consumed or replaced budget overrides. |
|
|
2214
2232
|
| **The dispatcher** | Merge, label and release authority are checked in the daemon against the operator's grant, across a process boundary, never in the prompt. |
|
|
2215
2233
|
|
|
2216
2234
|
Unconfined means auditable, not licensed. `orchestratorReadPaths` is retired: it
|
|
@@ -2325,9 +2343,12 @@ no socket it fails closed and says so, rather than reaching for `git push`.
|
|
|
2325
2343
|
|
|
2326
2344
|
### The ledger
|
|
2327
2345
|
|
|
2328
|
-
Every mutating call is recorded with its arguments, the decision, the
|
|
2329
|
-
refusal reason and any resulting SHA. Reads are not: a status poll every
|
|
2330
|
-
seconds would bury the refusals the record exists to surface.
|
|
2346
|
+
Every mutating verb call is recorded with its arguments, the decision, the
|
|
2347
|
+
named refusal reason and any resulting SHA. Reads are not: a status poll every
|
|
2348
|
+
thirty seconds would bury the refusals the record exists to surface.
|
|
2349
|
+
|
|
2350
|
+
Every `extend` that sets a next-attempt budget also appends an audit entry.
|
|
2351
|
+
Replacing or consuming the pending override does not erase that history.
|
|
2331
2352
|
|
|
2332
2353
|
```console
|
|
2333
2354
|
$ omp-conductor ledger --issue 7
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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.",
|
package/src/board.ts
CHANGED
|
@@ -95,7 +95,7 @@ const LIVE_LANES: Partial<Record<RunState, BoardLane>> = {
|
|
|
95
95
|
* that has to stay visible — #173. Differs from {@link LIVE_LANES} in that
|
|
96
96
|
* these are rows, never current work, and from MERGED in that they are not a
|
|
97
97
|
* happy resolution. */
|
|
98
|
-
const PARKED_STATES = new Set<RunState>(["blocked", "failed", "killed", "orphaned"]);
|
|
98
|
+
const PARKED_STATES = new Set<RunState>(["blocked", "failed", "killed", "stopped", "orphaned"]);
|
|
99
99
|
|
|
100
100
|
/** Whether a terminal blocked/failed run is wearing its own state label. When
|
|
101
101
|
* the label is absent the run row is the only record of what happened, so the
|
|
@@ -351,6 +351,7 @@ the same checks and the same ledger rows, through the CLI:
|
|
|
351
351
|
omp-conductor verb conductor_pr_merge --arg prUrl=<url> --arg headSha=<sha> --arg reason=<reason>
|
|
352
352
|
omp-conductor verb conductor_label --arg issueUrl=<url> --arg label=<name> --arg action=add --arg reason=<reason>
|
|
353
353
|
omp-conductor verb conductor_release --arg shape=git-tag --arg repo=<name> --arg reason=<reason> --arg tag=<tag>
|
|
354
|
+
omp-conductor verb conductor_pr_status --arg prUrl=<url>
|
|
354
355
|
omp-conductor verb conductor_pr_update_branch --arg prUrl=<url>
|
|
355
356
|
omp-conductor verb conductor_pr_update --arg prUrl=<url> --arg title=<title>
|
|
356
357
|
```
|
|
@@ -368,8 +369,37 @@ sessions do not.
|
|
|
368
369
|
| `conductor_label` | always | The label is one this project declared. Lifecycle labels (`agent:in-progress`, `agent:blocked`, `agent:failed`) are refused — those stay the dispatcher's, and `omp-conductor unblock` is how you clear them. |
|
|
369
370
|
| `conductor_release` | `authority.release` is yours | You are the configured holder, the shape is granted, the artefact or environment was declared, and the release preconditions hold. |
|
|
370
371
|
|
|
371
|
-
|
|
372
|
-
|
|
372
|
+
### Stopping and pausing
|
|
373
|
+
|
|
374
|
+
Four controls stop different work:
|
|
375
|
+
|
|
376
|
+
- **Pause one worker:** `omp-conductor worker pause <issue>` cooperatively drains
|
|
377
|
+
the active turn to harness idle, freezes its remaining wall clock, and keeps
|
|
378
|
+
the same live run, session, attempt and worktree. Its slot stays occupied and
|
|
379
|
+
its lifecycle labels stay in place. `omp-conductor worker resume <issue>`
|
|
380
|
+
continues that same session with a prompt to re-check its last action before
|
|
381
|
+
proceeding.
|
|
382
|
+
- **End one worker:** `omp-conductor worker stop <issue> --reason TEXT`
|
|
383
|
+
terminally settles a running or paused run as `stopped`. It salvages dirty
|
|
384
|
+
work, releases the worker slot, and removes `agent:in-progress`; it does not
|
|
385
|
+
spend a failure or continuation budget. Use it when the operator explicitly
|
|
386
|
+
ends obsolete or already-delivered work. A salvage failure keeps the tree and
|
|
387
|
+
names the path; recover that copy before any forced unblock.
|
|
388
|
+
- **Fleet dispatch:** `omp-conductor pause` stops new claims and work-starting
|
|
389
|
+
mutations. Work admitted before the pause may still complete, and completion
|
|
390
|
+
verbs and releases remain available.
|
|
391
|
+
- **Orchestrator ticks:** `omp-conductor disarm` removes the operator-owned
|
|
392
|
+
`ARMED` marker so ticks skip. It does not pause workers or stop processes.
|
|
393
|
+
|
|
394
|
+
Per-worker pause is not SIGSTOP/SIGCONT, fleet pause, unblock/requeue, or a
|
|
395
|
+
durable restart boundary. Daemon loss still follows the normal salvage and
|
|
396
|
+
orphan handling. Pause only when the operator asks to park one worker, or when
|
|
397
|
+
one live run must quiesce before resolving a proven shared-state collision.
|
|
398
|
+
Never pause routine work speculatively.
|
|
399
|
+
|
|
400
|
+
**`conductor_pr_merge` wants the SHA you believe you are merging.** Call
|
|
401
|
+
`conductor_pr_status` with the PR URL; its reply includes the current full SHA.
|
|
402
|
+
Pass that SHA to `conductor_pr_merge`. The dispatcher re-reads the live head
|
|
373
403
|
immediately before merging and refuses on any mismatch, naming both SHAs —
|
|
374
404
|
because any push since you looked invalidates the green you saw. A refusal there
|
|
375
405
|
is the mechanism working: re-read, re-check, call again.
|
package/src/cli.ts
CHANGED
|
@@ -106,6 +106,7 @@ usage:
|
|
|
106
106
|
omp-conductor extend <issue> --turns N [--project NAME]
|
|
107
107
|
omp-conductor worker pause <issue> [--project NAME]
|
|
108
108
|
omp-conductor worker resume <issue> [--project NAME]
|
|
109
|
+
omp-conductor worker stop <issue> --reason TEXT [--project NAME]
|
|
109
110
|
omp-conductor unblock <issue> [--force] [--no-requeue] [--project NAME]
|
|
110
111
|
omp-conductor verb <conductor_*> [--project NAME] [--arg k=v ...]
|
|
111
112
|
omp-conductor daemon [--once] [--port N] [--project NAME]
|
|
@@ -172,8 +173,9 @@ usage:
|
|
|
172
173
|
the daemon rather than terminals, so this is the only way to watch
|
|
173
174
|
one live. Runs until Ctrl-C, or until the run has finished and its
|
|
174
175
|
transcript has stopped growing.
|
|
175
|
-
extend
|
|
176
|
-
|
|
176
|
+
extend raise a live run's turn ceiling, or set a bounded one-shot ceiling
|
|
177
|
+
after a failed, killed, orphaned or blocked run. Refuses values outside
|
|
178
|
+
configured bounds.
|
|
177
179
|
worker cooperatively pause one live worker at harness idle, then resume the
|
|
178
180
|
same session with a continuation prompt. Its wall clock is frozen
|
|
179
181
|
while parked. Distinct from fleet-level pause/resume.
|
|
@@ -612,14 +614,11 @@ try {
|
|
|
612
614
|
}
|
|
613
615
|
|
|
614
616
|
/**
|
|
615
|
-
* The action ledger
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* now"; this answers "what did run 3 actually try to do", which is the
|
|
621
|
-
* question an escalation about a run asks, and it needs the whole record
|
|
622
|
-
* rather than the newest five lines of every run at once.
|
|
617
|
+
* The action ledger: mediated conductor verbs plus durable per-issue budget
|
|
618
|
+
* changes. Its own command as well as a block in `status`, because the two
|
|
619
|
+
* questions are different sizes. `status` answers "what is pending or being
|
|
620
|
+
* refused now"; this answers "what did run 3 actually try, and what budget
|
|
621
|
+
* did the operator assign", which needs history rather than only live state.
|
|
623
622
|
*/
|
|
624
623
|
case "ledger": {
|
|
625
624
|
const cfg = loadConfig();
|
|
@@ -641,18 +640,38 @@ try {
|
|
|
641
640
|
...(issue === undefined ? {} : { issue }),
|
|
642
641
|
limit,
|
|
643
642
|
});
|
|
644
|
-
|
|
643
|
+
const overrides = store.turnOverrideLedger(p.name, {
|
|
644
|
+
...(issue === undefined ? {} : { issue }),
|
|
645
|
+
limit,
|
|
646
|
+
});
|
|
647
|
+
if (entries.length === 0 && overrides.length === 0) {
|
|
645
648
|
process.stdout.write(
|
|
646
|
-
`no conductor
|
|
649
|
+
`no conductor actions recorded for ${p.name}` +
|
|
647
650
|
`${issue === undefined ? "" : ` on #${String(issue)}`}\n`,
|
|
648
651
|
);
|
|
649
652
|
break;
|
|
650
653
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
654
|
+
const blocks: string[] = [];
|
|
655
|
+
if (entries.length > 0) {
|
|
656
|
+
const refused = entries.filter((e) => e.decision === "refused").length;
|
|
657
|
+
blocks.push(
|
|
658
|
+
`${p.name} — ${entries.length} verb call(s), ${refused} refused (newest first)\n` +
|
|
659
|
+
entries.flatMap(formatVerbLedgerEntry).join("\n"),
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (overrides.length > 0) {
|
|
663
|
+
blocks.push(
|
|
664
|
+
`${p.name} — ${overrides.length} turn override(s) (newest first)\n` +
|
|
665
|
+
overrides
|
|
666
|
+
.map(
|
|
667
|
+
(entry) =>
|
|
668
|
+
` ${new Date(entry.setAt).toISOString()} #${entry.issue} ` +
|
|
669
|
+
`${entry.maxTurns} turns`,
|
|
670
|
+
)
|
|
671
|
+
.join("\n"),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
process.stdout.write(`${blocks.join("\n\n")}\n`);
|
|
656
675
|
} finally {
|
|
657
676
|
store.close();
|
|
658
677
|
}
|
|
@@ -774,14 +793,30 @@ try {
|
|
|
774
793
|
);
|
|
775
794
|
const payload = (await response.json()) as {
|
|
776
795
|
error?: unknown;
|
|
796
|
+
kind?: unknown;
|
|
777
797
|
runId?: unknown;
|
|
778
798
|
maxTurns?: unknown;
|
|
799
|
+
issue?: unknown;
|
|
800
|
+
nextAttemptMaxTurns?: unknown;
|
|
801
|
+
baseMaxTurns?: unknown;
|
|
779
802
|
};
|
|
780
803
|
if (!response.ok) {
|
|
781
804
|
throw new Error(
|
|
782
805
|
typeof payload.error === "string" ? payload.error : `daemon returned HTTP ${response.status}`,
|
|
783
806
|
);
|
|
784
807
|
}
|
|
808
|
+
if (
|
|
809
|
+
payload.kind === "next-attempt" &&
|
|
810
|
+
payload.issue === issue &&
|
|
811
|
+
typeof payload.nextAttemptMaxTurns === "number" &&
|
|
812
|
+
typeof payload.baseMaxTurns === "number"
|
|
813
|
+
) {
|
|
814
|
+
process.stdout.write(
|
|
815
|
+
`#${issue} next attempt turn ceiling set to ${payload.nextAttemptMaxTurns} ` +
|
|
816
|
+
`(project base ${payload.baseMaxTurns})\n`,
|
|
817
|
+
);
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
785
820
|
if (typeof payload.runId !== "string" || typeof payload.maxTurns !== "number") {
|
|
786
821
|
throw new Error("daemon returned an invalid turn-extension response");
|
|
787
822
|
}
|
|
@@ -793,13 +828,21 @@ try {
|
|
|
793
828
|
|
|
794
829
|
case "worker": {
|
|
795
830
|
const sub = argv[1];
|
|
796
|
-
if (sub !== "pause" && sub !== "resume") {
|
|
831
|
+
if (sub !== "pause" && sub !== "resume" && sub !== "stop") {
|
|
797
832
|
process.stderr.write(
|
|
798
|
-
"omp-conductor: worker needs pause or
|
|
833
|
+
"omp-conductor: worker needs pause, resume, or stop, then an issue number\n",
|
|
799
834
|
);
|
|
800
835
|
process.exit(2);
|
|
801
836
|
}
|
|
802
837
|
const issue = issueArg("worker", argv[2]);
|
|
838
|
+
const rawReason = sub === "stop" ? flag(argv, "reason") : undefined;
|
|
839
|
+
const reason = rawReason?.trim().replace(/\s+/g, " ");
|
|
840
|
+
if (sub === "stop" && (reason === undefined || reason === "" || reason.length > 500)) {
|
|
841
|
+
process.stderr.write(
|
|
842
|
+
"omp-conductor: worker stop needs --reason with 1-500 characters\n",
|
|
843
|
+
);
|
|
844
|
+
process.exit(2);
|
|
845
|
+
}
|
|
803
846
|
const project = findProject(loadConfig(), flag(argv, "project"));
|
|
804
847
|
const daemon = livingDaemon();
|
|
805
848
|
if (daemon === undefined) throw new Error("daemon is not running");
|
|
@@ -813,25 +856,63 @@ try {
|
|
|
813
856
|
{
|
|
814
857
|
method: "PUT",
|
|
815
858
|
headers: { "content-type": "application/json" },
|
|
816
|
-
body: JSON.stringify({
|
|
859
|
+
body: JSON.stringify({
|
|
860
|
+
project: project.name,
|
|
861
|
+
...(reason === undefined ? {} : { reason }),
|
|
862
|
+
}),
|
|
817
863
|
},
|
|
818
864
|
);
|
|
819
865
|
const payload = (await response.json()) as {
|
|
820
866
|
error?: unknown;
|
|
821
867
|
runId?: unknown;
|
|
822
868
|
phase?: unknown;
|
|
869
|
+
outcome?: unknown;
|
|
870
|
+
state?: unknown;
|
|
871
|
+
reason?: unknown;
|
|
872
|
+
salvageSha?: unknown;
|
|
873
|
+
salvageError?: unknown;
|
|
874
|
+
worktree?: unknown;
|
|
823
875
|
};
|
|
824
876
|
if (!response.ok) {
|
|
825
877
|
throw new Error(
|
|
826
878
|
typeof payload.error === "string" ? payload.error : `daemon returned HTTP ${response.status}`,
|
|
827
879
|
);
|
|
828
880
|
}
|
|
881
|
+
if (sub === "stop") {
|
|
882
|
+
if (
|
|
883
|
+
typeof payload.runId !== "string" ||
|
|
884
|
+
typeof payload.state !== "string" ||
|
|
885
|
+
(payload.outcome !== "stopped" && payload.outcome !== "already-terminal")
|
|
886
|
+
) {
|
|
887
|
+
throw new Error("daemon returned an invalid worker-stop response");
|
|
888
|
+
}
|
|
889
|
+
if (payload.outcome === "already-terminal") {
|
|
890
|
+
process.stdout.write(
|
|
891
|
+
`#${issue} worker already terminal: ${payload.state} (run ${payload.runId})\n`,
|
|
892
|
+
);
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
if (typeof payload.reason !== "string") {
|
|
896
|
+
throw new Error("daemon returned an invalid worker-stop response");
|
|
897
|
+
}
|
|
898
|
+
process.stdout.write(
|
|
899
|
+
`#${issue} worker stopped (run ${payload.runId}): ${payload.reason}\n`,
|
|
900
|
+
);
|
|
901
|
+
if (typeof payload.salvageSha === "string") {
|
|
902
|
+
process.stdout.write(`work preserved: ${payload.salvageSha}\n`);
|
|
903
|
+
}
|
|
904
|
+
if (typeof payload.salvageError === "string") {
|
|
905
|
+
process.stdout.write(
|
|
906
|
+
`WIP SALVAGE FAILED: ${payload.salvageError}\n` +
|
|
907
|
+
`worktree kept: ${typeof payload.worktree === "string" ? payload.worktree : "(path unavailable)"}\n`,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
829
912
|
if (typeof payload.runId !== "string" || typeof payload.phase !== "string") {
|
|
830
913
|
throw new Error("daemon returned an invalid worker-control response");
|
|
831
914
|
}
|
|
832
|
-
process.stdout.write(
|
|
833
|
-
`#${issue} worker ${payload.phase} (run ${payload.runId})\n`,
|
|
834
|
-
);
|
|
915
|
+
process.stdout.write(`#${issue} worker ${payload.phase} (run ${payload.runId})\n`);
|
|
835
916
|
break;
|
|
836
917
|
}
|
|
837
918
|
|
package/src/config.ts
CHANGED
|
@@ -227,12 +227,16 @@ export function writeConfigRaw(text: string): void {
|
|
|
227
227
|
*/
|
|
228
228
|
export function resolveCaps(p: ProjectConfig, defaults: Caps): Caps {
|
|
229
229
|
const o: Partial<Caps> = p.caps ?? {};
|
|
230
|
+
const workerMaxTurns = o.workerMaxTurns ?? defaults.workerMaxTurns;
|
|
230
231
|
return {
|
|
231
232
|
maxConcurrentWorkers: o.maxConcurrentWorkers ?? defaults.maxConcurrentWorkers,
|
|
232
233
|
maxConcurrentWorkersPerRepo: o.maxConcurrentWorkersPerRepo ?? defaults.maxConcurrentWorkersPerRepo,
|
|
233
234
|
dailySpendUsd: o.dailySpendUsd !== undefined ? o.dailySpendUsd : defaults.dailySpendUsd,
|
|
234
235
|
planUsage: o.planUsage !== undefined ? o.planUsage : defaults.planUsage,
|
|
235
|
-
workerMaxTurns
|
|
236
|
+
workerMaxTurns,
|
|
237
|
+
workerMaxTurnsCeiling:
|
|
238
|
+
o.workerMaxTurnsCeiling ??
|
|
239
|
+
(o.workerMaxTurns === undefined ? defaults.workerMaxTurnsCeiling : workerMaxTurns * 2),
|
|
236
240
|
workerWallClockMs: o.workerWallClockMs ?? defaults.workerWallClockMs,
|
|
237
241
|
maxAttemptsPerIssue: o.maxAttemptsPerIssue ?? defaults.maxAttemptsPerIssue,
|
|
238
242
|
maxContinuationsPerIssue:
|
|
@@ -366,9 +370,13 @@ function validate(parsed: unknown, path: string): ConductorConfig {
|
|
|
366
370
|
);
|
|
367
371
|
}
|
|
368
372
|
|
|
373
|
+
const configuredDefaults = coerceCaps(root["defaults"], `"defaults"`, problems, legacyCaps);
|
|
374
|
+
const defaultWorkerMaxTurns = configuredDefaults.workerMaxTurns ?? DEFAULT_CAPS.workerMaxTurns;
|
|
369
375
|
const defaults: Caps = {
|
|
370
376
|
...DEFAULT_CAPS,
|
|
371
|
-
...
|
|
377
|
+
...configuredDefaults,
|
|
378
|
+
workerMaxTurnsCeiling:
|
|
379
|
+
configuredDefaults.workerMaxTurnsCeiling ?? defaultWorkerMaxTurns * 2,
|
|
372
380
|
};
|
|
373
381
|
|
|
374
382
|
const rawProjects = root["projects"];
|