omp-conductor 0.10.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 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` | Startup ceiling for each new worker. Catches a session looping without converging; use `omp-conductor extend` to raise one live run without changing this default. |
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,12 +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. Extension is monotonic: equal or lower
1009
- values are refused, as are runs whose live controller has already settled. The
1010
- effective value is persisted and shown beside that active run in `status`; editing
1011
- `config.json` changes defaults for future daemon starts, not workers already in
1012
- flight. A cap that fires aborts the run, records it as `killed`, and names the
1013
- ceiling in the escalation.
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.
1020
+
1021
+ Pause one live worker cooperatively with
1022
+ `omp-conductor worker pause <issue> [--project NAME]`. The daemon aborts the
1023
+ active turn to an idle harness state, freezes the remaining wall-clock budget,
1024
+ and keeps the run in the Running lane. `omp-conductor worker resume <issue>`
1025
+ continues the same session with a prompt to re-check its last action before
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.
1014
1034
 
1015
1035
  ### Plan allowance (`planUsage`)
1016
1036
 
@@ -1587,7 +1607,8 @@ A complete, valid config for one project with two target repos:
1587
1607
  { "cmd": "bun run lint", "cwd": "." },
1588
1608
  { "cmd": "bun test", "cwd": "." }
1589
1609
  ],
1590
- "graphProject": "~/.cache/conductor-graph/acme/api"
1610
+ "graphProject": "~/.cache/conductor-graph/acme/api",
1611
+ "migrations": { "dir": "backend/alembic/versions" }
1591
1612
  },
1592
1613
  "worker": {
1593
1614
  "name": "worker",
@@ -1659,12 +1680,13 @@ Field notes:
1659
1680
  | `routing.repos` | At least one entry, or nothing can be routed. `name` defaults to the map key, `defaultBranch` to `main`. |
1660
1681
  | `gates` | The exact cheap commands CI also runs, each with the `cwd` it runs from (`cwd` defaults to `.`). Running the real gate locally is what makes an unattended push safe — a subset lets an error outside the source dir reach the runners. |
1661
1682
  | `graphProject` | Optional, per repo. Absolute path of the **index-only clone** whose code graph this repo's workers query — conductor's own disposable clone, pinned to the repo's default branch, never a checkout you work in and never a worker's worktree. Written by the wizard; `~` is expanded, and a relative path is an error rather than something resolved against whichever cwd happened to read the file. Absent means this repo has no graph and its briefs say nothing about one. See [Code-graph discovery](#code-graph-discovery). |
1683
+ | `migrations` | Optional, per repo: `{ "dir": "backend/alembic/versions" }`. Names the repo-relative directory of an Alembic-style ordered migration chain (`revision` / `down_revision` in `*.py`). When set, `conductor_pr_merge` **refuses** a merge that would corrupt the chain at the base tip: reusing a revision id another file already declares, deleting a published migration, or a merge that would leave the combined graph with more than one head (so a stale parent is refused, and a fork-repair merge migration that unifies the heads passes). Absent means the repo opts out of the chain check entirely. Repo-relative only: a leading `/` or `..` is an error. |
1662
1684
  | `caps` | Per-project overrides; omit it or pin only the fields you want to change. |
1663
1685
  | `escalation.fallbackToIssueComment` | Defaults to `true`. Absent means "yes, still tell me". |
1664
1686
  | `escalation.orchestrator` | Optional; `"embedded"` (default) or `"external"`. `external` means an orchestrator session already runs elsewhere: the daemon starts none, and tier-1 escalations post as issue comments for that session to drain. Any other value is an error. |
1665
1687
  | `authority` | Optional; `{ "merge": …, "release": … }`, each `"human"` (default) or `"orchestrator"`. It grants nothing to the daemon — it words the orchestrator's standing orders and the Releases paragraph of the rendered brief, so the config and the prompt cannot disagree about who holds the merge button. Unknown keys and any other value are errors, never folded to the default. |
1666
1688
  | `releasePolicy` | Optional; `"none"` (default) or `"operator-brief"`. `none` installs a pre-tool-call tripwire in worker, embedded-orchestrator and external-orchestrator sessions. It blocks `git tag`, tag pushes, package publishing, GitHub release creation and recognised deploy commands before execution. `operator-brief` opens that gate only for the procedure in the operator-owned brief. Unknown values are errors. Every rejection is written to `release-policy-blocks.jsonl`; the heartbeat carries that day's count into the daily digest so configured intent and observed behaviour cannot drift silently. This is the mechanical gate; `authority.release` still says who owns the decision. |
1667
- | `reporting.scope` | Optional; `"material"` (default), `"decisions"` or `"escalations"`. Every orchestrator tick appends the matching constraint line to its prompt, re-read from this file each tick see [Your workflow vs. the package](#your-workflow-vs-the-package). It constrains what the session is told to report; it is not an outbound filter. A config written without the key keeps reporting material events a release never turns an existing fleet's volume down on its own. Any other value is an error, never folded to the default. |
1689
+ | `reporting` | Optional; a **legacy scope preset** (`reporting.scope` — `"material"` default, `"decisions"`, `"escalations"`) or the **explicit form** `{ "interruptOn": [...], "digest": { ... } }`. The preset writes which categories may page the operator (`interruptOn`) and when the rollup happens (`digest.cadence`); the explicit form sets both directly and the two forms are mutually exclusive in one config. See [Reporting policy](#reporting-policy-reporting). |
1668
1690
  | `orchestratorReadPaths` | **Retired in 0.4.3.** Still accepted in a config and ignored, so a fleet carrying it upgrades without an edit. It widened the orchestrator's file-tool allowlist; there is no allowlist any more — the orchestrator is [unconfined by design](#the-orchestrator-is-unconfined-deliberately). |
1669
1691
  | `policy` | Optional; the gating conditions a merge or a release must satisfy, in two sections — `policy.merge` and `policy.release`. Any member may be omitted and the loader fills it from the strict default; an unknown key in either section, or a value outside its vocabulary, is an error naming the field, never a silent downgrade. See [Merge and release preconditions](#merge-and-release-preconditions-policy). |
1670
1692
  | `workspaceRoot` / `mirrorRoot` | Optional; default to `worktrees/` and `mirrors/` under the state directory. `~` is expanded. |
@@ -1673,6 +1695,27 @@ Prefer an SSH `cloneUrl`, or an https URL backed by a credential helper. A clone
1673
1695
  with credentials embedded is persisted into the mirror's git config, exactly as it
1674
1696
  would be for a hand-run clone.
1675
1697
 
1698
+ ### Reporting policy (`reporting`)
1699
+
1700
+ What may interrupt the operator's phone, and when the daily rollup happens. Two
1701
+ spellings, mutually exclusive in one config (the loader rejects a `scope` next to
1702
+ `interruptOn`/`digest`):
1703
+
1704
+ - **Preset** — `reporting.scope`, the three legacy values, mapped verbatim:
1705
+ - `material` (default) → `interruptOn: [tier2, decision-needed, fleet-stopped, confirmed-failure, material]`, digest `per-tick`.
1706
+ - `decisions` → `interruptOn: [tier2, decision-needed, fleet-stopped]`, digest `per-tick`.
1707
+ - `escalations` → `interruptOn: [tier2, fleet-stopped]`, digest `daily` (model-timed).
1708
+ - **Explicit** — `reporting: { "interruptOn": ["tier2", "fleet-stopped", ...], "digest": { "cadence": "none" | "per-tick" | "daily" } }`.
1709
+ `interruptOn` must be a non-empty array of known categories (`tier2`, `decision-needed`, `fleet-stopped`, `confirmed-failure`, `material`), each an escalation's tier-2 category. `daily` may add `at` (`HH:MM`, 24h) and `timezone` (a known IANA zone, defaulting to the host zone) — both only valid with `daily`.
1710
+
1711
+ A tier-2 escalation whose category is **not** in `interruptOn` is not dropped: it
1712
+ is held (`held_notices`) and the next accepted digest is its delivery authority.
1713
+ The digest itself is at-most-once per local day (`digest:<YYYY-MM-DD>` in the
1714
+ configured zone), which remains the delivery authority across restarts. A
1715
+ scheduled `daily` digest is only sent on a day it has not already run, once the
1716
+ local clock has passed `at`; a restart after `at` still sends today's (one
1717
+ catch-up), and a fully missed day is skipped, never sent late.
1718
+
1676
1719
  ### Merge and release preconditions (`policy`)
1677
1720
 
1678
1721
  These used to be sentences in your `POLICY.md`: when a PR may be merged, what
@@ -1694,7 +1737,7 @@ policy instead of restating it — no threshold lives in two places.
1694
1737
 
1695
1738
  | Field | Values | Default | Means |
1696
1739
  | --- | --- | --- | --- |
1697
- | `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. |
1698
1741
  | `requiredChecks` | any check names | `[]` | Checks that must be green on the branch being released. Empty means every check it reports. |
1699
1742
  | `artefacts` | any names | `[]` | The packages or images this project releases. **Empty denies**: nothing has been authorised to ship. |
1700
1743
  | `environments` | any names | `[]` | Deploy targets. **Empty denies** every environment. |
@@ -1959,6 +2002,9 @@ omp-conductor disarm [--project NAME]
1959
2002
  omp-conductor release-pane [--project NAME]
1960
2003
  omp-conductor tail <issue> [--project NAME]
1961
2004
  omp-conductor extend <issue> --turns N [--project NAME]
2005
+ omp-conductor worker pause <issue> [--project NAME]
2006
+ omp-conductor worker resume <issue> [--project NAME]
2007
+ omp-conductor worker stop <issue> --reason TEXT [--project NAME]
1962
2008
  omp-conductor unblock <issue> [--force] [--no-requeue] [--project NAME]
1963
2009
  omp-conductor verb <conductor_*> [--project NAME] [--arg k=v ...]
1964
2010
  omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
@@ -1982,7 +2028,7 @@ omp-conductor help
1982
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). |
1983
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. |
1984
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. |
1985
- | `ledger [--issue N] [--limit N]` | Every [mediated-verb](#the-mediated-verbs-126) call and how the daemon decided it: the verb, the arguments it was given, allow or refuse, the named refusal reason, and any resulting SHA. Sessions cannot push, open a PR, merge, label or release except through those verbs, so this is the record of what they *tried* as well as what they did — the thing an escalation cites. Reads (`conductor_pr_status`) are deliberately absent: a poll every thirty seconds would bury the refusals. `--issue` narrows to one issue's run; `--limit` defaults to 50. The newest few also appear in `status`. |
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`. |
1986
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. |
1987
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). |
1988
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. |
@@ -1990,7 +2036,9 @@ omp-conductor help
1990
2036
  | `disarm [--project NAME]` | Remove the arm marker so ticks skip. Processes untouched. |
1991
2037
  | `release-pane [--project NAME]` | Clear the `halt --pane` recovery pin so herdr-conductor may resume the fleet agent again. |
1992
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>`. |
1993
- | `extend <issue> --turns N [--project NAME]` | Monotonically raise that live worker's effective turn ceiling through its owning daemon. The current omp session keeps running; no restart or continuation is created. The daemon persists the new ceiling for `status` and rejects missing, settled, cap-killed, equal, or lower requests instead of implying that an immutable session changed. |
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. |
1994
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. |
1995
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`. |
1996
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. |
@@ -2003,7 +2051,7 @@ omp-conductor help
2003
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. |
2004
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. |
2005
2053
  | `--project NAME` | Pick the project to service. One daemon process serves exactly one project; with several configured projects the name is required. |
2006
- | `pause [--reason TEXT]` | Stop claiming new work only. The running daemon notices on its next tick; runs already in flight finish. The orchestrator heartbeat keeps ticking if armed — its gate is the arm marker, not this flag. Prefer `hold` to silence both. `--reason TEXT` is recorded in the pause sentinel, which `status` shows as the pause provenance. |
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. |
2007
2055
  | `resume` | Clear pause only — does **not** re-arm. Run `arm` after an inbound Telegram proof to resume ticks. |
2008
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. |
2009
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). |
@@ -2016,8 +2064,11 @@ omp-conductor help
2016
2064
  | `help`, `--help`, `-h` | Print usage. An unknown or missing verb prints it too, and exits `2`. |
2017
2065
 
2018
2066
  Pause is a flag file under the state directory, so it applies to every project and
2019
- survives a daemon restart. Hold also removes the arm marker the heartbeat reads,
2020
- so both brains go quiet without killing processes.
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.
2021
2072
 
2022
2073
  These are available in-session as `/conductor setup`, `/conductor status`,
2023
2074
  `/conductor hold`, `/conductor halt [--pane]`, `/conductor arm`, `/conductor disarm`,
@@ -2177,7 +2228,7 @@ What holds the orchestrator instead:
2177
2228
  | | |
2178
2229
  | --- | --- |
2179
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. |
2180
- | **The verb ledger** | Every `conductor_*` call records what was asked for and what the dispatcher decided, refusals included. `omp-conductor ledger` is the audit. |
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. |
2181
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. |
2182
2233
 
2183
2234
  Unconfined means auditable, not licensed. `orchestratorReadPaths` is retired: it
@@ -2292,9 +2343,12 @@ no socket it fails closed and says so, rather than reaching for `git push`.
2292
2343
 
2293
2344
  ### The ledger
2294
2345
 
2295
- Every mutating call is recorded with its arguments, the decision, the named
2296
- refusal reason and any resulting SHA. Reads are not: a status poll every thirty
2297
- 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.
2298
2352
 
2299
2353
  ```console
2300
2354
  $ omp-conductor ledger --issue 7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.10.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
@@ -12,6 +12,7 @@ import {
12
12
  } from "./fleet.ts";
13
13
  import { probeCodeGraph, type CodeGraphHealth } from "./graph-health.ts";
14
14
  import { healthCheck, livingDaemon } from "./lifecycle.ts";
15
+ import type { WorkerPausePhase } from "./worker.ts";
15
16
  import { dbPath, openStore } from "./store.ts";
16
17
  import { formatTranscriptLine } from "./transcript.ts";
17
18
  import { makeTracker } from "./tracker/github.ts";
@@ -94,7 +95,7 @@ const LIVE_LANES: Partial<Record<RunState, BoardLane>> = {
94
95
  * that has to stay visible — #173. Differs from {@link LIVE_LANES} in that
95
96
  * these are rows, never current work, and from MERGED in that they are not a
96
97
  * happy resolution. */
97
- const PARKED_STATES = new Set<RunState>(["blocked", "failed", "killed", "orphaned"]);
98
+ const PARKED_STATES = new Set<RunState>(["blocked", "failed", "killed", "stopped", "orphaned"]);
98
99
 
99
100
  /** Whether a terminal blocked/failed run is wearing its own state label. When
100
101
  * the label is absent the run row is the only record of what happened, so the
@@ -136,6 +137,11 @@ export interface BoardHealth {
136
137
  codeGraph?: CodeGraphHealth;
137
138
  }
138
139
 
140
+ interface BoardHealthProbe {
141
+ health: BoardHealth;
142
+ pausedPhases: ReadonlyMap<number, WorkerPausePhase>;
143
+ }
144
+
139
145
  /**
140
146
  * Which issues currently carry each label the board reasons about, read from
141
147
  * the tracker rather than inferred from run rows.
@@ -183,6 +189,7 @@ export interface BoardSnapshot {
183
189
  project: ProjectConfig;
184
190
  status: StatusSnapshot;
185
191
  health: BoardHealth;
192
+ pausedPhases: ReadonlyMap<number, WorkerPausePhase>;
186
193
  labels: BoardLabels;
187
194
  /** Live re-verification of pushed rows (#173): run id → what the tracker says
188
195
  * the PR looks like now. Rendered as a `now:` suffix on the card. */
@@ -561,13 +568,15 @@ function normalizeCursor(snapshot: BoardSnapshot, cursor: BoardCursor): void {
561
568
  function runCardLines(run: RunRecord, snapshot: BoardSnapshot, lane: BoardLane): string[] {
562
569
  const endedAt = run.endedAt ?? snapshot.now;
563
570
  const duration = humanDuration(endedAt - run.startedAt);
571
+ const phase = lane === "running" ? snapshot.pausedPhases.get(run.issue) : undefined;
572
+ const pausePrefix = phase === "paused" ? "⏸ PAUSED " : phase === "pausing" ? "… pausing " : "";
564
573
  const lines = [
565
574
  // The class, when the sweep has attached one and nothing has recovered it
566
575
  // yet (#132): a row state says "failed", which four completed issues on this
567
576
  // fleet also said. The class says which of the two this is.
568
577
  run.failureClass === undefined || run.recoveredAt !== undefined
569
- ? `#${run.issue} · ${run.repo}`
570
- : `#${run.issue} · ${run.repo} [${run.failureClass}]`,
578
+ ? `${pausePrefix}#${run.issue} · ${run.repo}`
579
+ : `${pausePrefix}#${run.issue} · ${run.repo} [${run.failureClass}]`,
571
580
  `attempt ${run.attempt} · ${run.turns}/${run.maxTurns}t`,
572
581
  `$${run.spendUsd.toFixed(2)} · ${duration}`,
573
582
  ];
@@ -719,7 +728,7 @@ function admissionLine(snapshot: BoardSnapshot): string {
719
728
  const queue =
720
729
  dispatch === undefined
721
730
  ? "dispatch not recorded"
722
- : `${dispatch.degraded ? "DEGRADED · " : ""}${dispatch.ready} ready · ${dispatch.routed} routed · ${dispatch.admitted} admitted`;
731
+ : `${dispatch.degraded ? "DEGRADED · " : ""}${dispatch.ready} ready · ${dispatch.claimed ?? 0} in flight · ${dispatch.routed} spare · ${dispatch.admitted} admitted`;
723
732
  const holdText = dispatch?.holds.map((hold) => `${hold.reason} ${hold.count}`).join(", ");
724
733
  const holds = holdText === undefined || holdText === "" ? "none" : holdText;
725
734
  return (
@@ -820,8 +829,20 @@ function renderDetail(snapshot: BoardSnapshot, cursor: BoardCursor, width: numbe
820
829
  // #173: a blocked/failed run whose label is gone is the reason this card is
821
830
  // parked; the header says so rather than presenting the state as current.
822
831
  const stateShown = isLastRun(run, snapshot.labels) ? `last run: ${run.state}` : run.state;
832
+ const pausePhase = snapshot.pausedPhases.get(run.issue);
823
833
  const metadata = [
824
834
  styledCell(` RUN #${run.issue} ${run.repo} ${stateShown} `, width, `${BOLD}${REVERSE}`),
835
+ ...(pausePhase === undefined
836
+ ? []
837
+ : [
838
+ styledCell(
839
+ pausePhase === "paused"
840
+ ? "worker paused · wall clock frozen"
841
+ : "worker pausing · draining to harness idle",
842
+ width,
843
+ YELLOW,
844
+ ),
845
+ ]),
825
846
  styledCell(`attempt ${run.attempt} · ${run.turns}/${run.maxTurns} turns · $${run.spendUsd.toFixed(2)} · ${humanDuration((run.endedAt ?? snapshot.now) - run.startedAt)}`, width),
826
847
  styledCell(`branch ${run.branch}`, width, DIM),
827
848
  styledCell(`worktree ${run.worktree || "removed"}`, width, DIM),
@@ -844,6 +865,7 @@ function renderHelp(width: number, height: number): string[] {
844
865
  "↑/↓ or k/j select card",
845
866
  "Enter inspect/follow transcript",
846
867
  "u unblock selected blocked, failed or orphaned issue",
868
+ "space pause/resume selected worker",
847
869
  "i open selected issue",
848
870
  "p open selected pull request",
849
871
  "r refresh health now",
@@ -900,7 +922,37 @@ export function renderBoard(
900
922
  ].join("\n");
901
923
  }
902
924
 
903
- async function probeBoardHealth(project: ProjectConfig): Promise<BoardHealth> {
925
+ export function workerPhasesFromHealthz(
926
+ body: string | undefined,
927
+ project: string,
928
+ ): ReadonlyMap<number, WorkerPausePhase> {
929
+ const phases = new Map<number, WorkerPausePhase>();
930
+ if (body === undefined) return phases;
931
+ try {
932
+ const payload = JSON.parse(body) as unknown;
933
+ if (payload === null || typeof payload !== "object") return phases;
934
+ if (Reflect.get(payload, "project") !== project) return phases;
935
+ const workers = Reflect.get(payload, "workers");
936
+ if (!Array.isArray(workers)) return phases;
937
+ for (const worker of workers) {
938
+ if (worker === null || typeof worker !== "object") continue;
939
+ const issue = Reflect.get(worker, "issue");
940
+ const phase = Reflect.get(worker, "phase");
941
+ if (
942
+ Number.isSafeInteger(issue) &&
943
+ (issue as number) > 0 &&
944
+ (phase === "pausing" || phase === "paused")
945
+ ) {
946
+ phases.set(issue as number, phase);
947
+ }
948
+ }
949
+ } catch {
950
+ // An unreadable health body means no trustworthy pause phase.
951
+ }
952
+ return phases;
953
+ }
954
+
955
+ async function probeBoardHealth(project: ProjectConfig): Promise<BoardHealthProbe> {
904
956
  const layers = fleetLayers(project.name);
905
957
  const record = livingDaemon();
906
958
  const wrongRecord = record?.project !== undefined && record.project !== project.name;
@@ -921,7 +973,18 @@ async function probeBoardHealth(project: ProjectConfig): Promise<BoardHealth> {
921
973
  }
922
974
  }
923
975
  const cachedGraph = daemon === "ok" ? codeGraphFromHealthz(health?.body, project.name) : undefined;
924
- return { layers, telegram, daemon, codeGraph: cachedGraph ?? (await probeCodeGraph(project)) };
976
+ return {
977
+ health: {
978
+ layers,
979
+ telegram,
980
+ daemon,
981
+ codeGraph: cachedGraph ?? (await probeCodeGraph(project)),
982
+ },
983
+ pausedPhases:
984
+ daemon === "ok"
985
+ ? workerPhasesFromHealthz(health?.body, project.name)
986
+ : new Map<number, WorkerPausePhase>(),
987
+ };
925
988
  }
926
989
 
927
990
  /**
@@ -1020,6 +1083,41 @@ async function unblock(project: ProjectConfig, issue: number): Promise<string> {
1020
1083
  if (code !== 0) return (stderr || stdout).trim().replace(/\s+/g, " ") || `#${issue}: unblock failed`;
1021
1084
  return summarizeUnblockOutput(issue, stdout);
1022
1085
  }
1086
+
1087
+ async function toggleWorkerPause(
1088
+ project: ProjectConfig,
1089
+ issue: number,
1090
+ phase: WorkerPausePhase | undefined,
1091
+ ): Promise<string> {
1092
+ if (phase === "pausing") return "still pausing — wait";
1093
+ const daemon = livingDaemon();
1094
+ if (daemon === undefined) return "daemon is not running";
1095
+ if (daemon.project !== undefined && daemon.project !== project.name) {
1096
+ return `daemon serves project "${daemon.project}", not requested project "${project.name}"`;
1097
+ }
1098
+ const action = phase === "paused" ? "resume" : "pause";
1099
+ try {
1100
+ const response = await fetch(
1101
+ `http://127.0.0.1:${daemon.port}/runs/${issue}/${action}`,
1102
+ {
1103
+ method: "PUT",
1104
+ headers: { "content-type": "application/json" },
1105
+ body: JSON.stringify({ project: project.name }),
1106
+ },
1107
+ );
1108
+ const payload = (await response.json()) as { error?: unknown; phase?: unknown };
1109
+ if (!response.ok) {
1110
+ return typeof payload.error === "string"
1111
+ ? payload.error
1112
+ : `daemon returned HTTP ${response.status}`;
1113
+ }
1114
+ return typeof payload.phase === "string"
1115
+ ? `#${issue} worker ${payload.phase}`
1116
+ : "daemon returned an invalid worker-control response";
1117
+ } catch (err) {
1118
+ return err instanceof Error ? err.message : String(err);
1119
+ }
1120
+ }
1023
1121
  function enqueue(queue: KeyInput[], key: KeyInput, wake: (() => void) | undefined): void {
1024
1122
  queue.push(key);
1025
1123
  wake?.();
@@ -1051,7 +1149,7 @@ export async function boardSnapshotOnce(projectName?: string): Promise<BoardSnap
1051
1149
  const store: Store = openStore(dbPath());
1052
1150
  try {
1053
1151
  const now = Date.now();
1054
- const [health, labels, planUsage] = await Promise.all([
1152
+ const [healthProbe, labels, planUsage] = await Promise.all([
1055
1153
  probeBoardHealth(project),
1056
1154
  probeBoardLabels(project, store),
1057
1155
  readPlanUsage(caps.planUsage, sharedUsageSource()),
@@ -1059,7 +1157,8 @@ export async function boardSnapshotOnce(projectName?: string): Promise<BoardSnap
1059
1157
  return {
1060
1158
  project,
1061
1159
  status: statusSnapshotFromStore(project, caps, store, planUsage),
1062
- health,
1160
+ health: healthProbe.health,
1161
+ pausedPhases: healthProbe.pausedPhases,
1063
1162
  labels,
1064
1163
  pr: new Map(),
1065
1164
  runs: store.recentRuns(project.name, now - MERGED_HISTORY_MS),
@@ -1137,11 +1236,13 @@ export async function runBoard(projectName?: string): Promise<void> {
1137
1236
  onCall: () => trackerCalls.push(Date.now()),
1138
1237
  onNotModified: () => trackerFree.push(Date.now()),
1139
1238
  });
1140
- let [health, labels, planUsage] = await Promise.all([
1239
+ let [healthProbe, labels, planUsage] = await Promise.all([
1141
1240
  probeBoardHealth(project),
1142
1241
  probeBoardLabels(project, store, undefined, boardTracker),
1143
1242
  readPlanUsage(caps.planUsage, sharedUsageSource()),
1144
1243
  ]);
1244
+ let health = healthProbe.health;
1245
+ let pausedPhases = healthProbe.pausedPhases;
1145
1246
  let healthAt = Date.now();
1146
1247
  let healthRefresh: Promise<void> | undefined;
1147
1248
  // The tracker read is gated separately from health and the plan allowance:
@@ -1183,8 +1284,9 @@ export async function runBoard(projectName?: string): Promise<void> {
1183
1284
  // subprocess, and an allowance does not move at 1 Hz.
1184
1285
  readPlanUsage(caps.planUsage, sharedUsageSource()),
1185
1286
  ])
1186
- .then(([nextHealth, nextPlanUsage]) => {
1187
- health = nextHealth;
1287
+ .then(([nextProbe, nextPlanUsage]) => {
1288
+ health = nextProbe.health;
1289
+ pausedPhases = nextProbe.pausedPhases;
1188
1290
  planUsage = nextPlanUsage;
1189
1291
  enqueue(queue, { name: "refresh" }, wake);
1190
1292
  })
@@ -1238,6 +1340,7 @@ export async function runBoard(projectName?: string): Promise<void> {
1238
1340
  project,
1239
1341
  status: statusSnapshotFromStore(project, caps, store, planUsage),
1240
1342
  health,
1343
+ pausedPhases,
1241
1344
  labels,
1242
1345
  pr: prProbe,
1243
1346
  runs: store.recentRuns(project.name, now - MERGED_HISTORY_MS),
@@ -1329,6 +1432,17 @@ export async function runBoard(projectName?: string): Promise<void> {
1329
1432
  else notice = (await openUrl(url)) ? `opened ${url}` : `failed to open ${url}`;
1330
1433
  continue;
1331
1434
  }
1435
+ if (name === "space") {
1436
+ const lane = COLUMN_DEFS[cursor.column]?.key;
1437
+ if (card === undefined || card.kind !== "run" || lane !== "running") {
1438
+ notice = "pause/resume is available for RUNNING cards";
1439
+ } else {
1440
+ const issue = cardIssue(card);
1441
+ notice = await toggleWorkerPause(project, issue, pausedPhases.get(issue));
1442
+ healthAt = 0;
1443
+ }
1444
+ continue;
1445
+ }
1332
1446
  if (name === "u") {
1333
1447
  // Lane, not run state. `unblock` clears the blocked and failed labels
1334
1448
  // unconditionally and the in-progress label once the newest run row is
@@ -199,7 +199,8 @@ Keep the queue worth draining.
199
199
  When the queue is below the grooming trigger, fan out read-only `scout`
200
200
  subagents over backlog clusters **in one batch** rather than auditing one issue
201
201
  at a time. Scouts do the finding; you still do the deciding and you still write
202
- the brief. The quality bar above does not move.
202
+ the brief. Name the authoritative source in every scout brief and forbid
203
+ unnamed fallbacks. The quality bar above does not move.
203
204
  - **Give every scout the same return contract**, or it comes back with prose
204
205
  nobody can act on:
205
206
  - verdict — `ALREADY DONE` / `PROMOTABLE` / `NEEDS DECOMPOSITION` / `BLOCKED` /
@@ -209,6 +210,10 @@ Keep the queue worth draining.
209
210
  - entry points — the 3-6 files to change or read first
210
211
  - existing tests covering the behaviour, by path
211
212
  - the one thing most likely to be silently faked
213
+ - source — where the code was read: the clone/ref and how fresh it is. A
214
+ scout that cannot reach a source it trusts returns `BLOCKED` and says so;
215
+ silent fallback to an unnamed source is the failure mode of delegated
216
+ research — stale evidence reads exactly like good evidence.
212
217
  - **Disqualifying an issue is a successful grooming outcome.** Measured on this
213
218
  package's own fleet: four scouts over sixteen backlog issues promoted four and
214
219
  *disqualified six* that looked promotable from their titles — four written
@@ -228,12 +233,23 @@ decides whether this tick ends in a message or in silence.
228
233
  *How* a report is delivered is not yours, and is not negotiable: run
229
234
  `omp-conductor report --text "<the whole report>"` (add `--kind digest` for the
230
235
  daily digest). It persists the text before anything is sent and prints a report
231
- id; the daemon retries until it lands and `omp-conductor status` lists whatever
236
+ report id; the daemon retries until it lands and `omp-conductor status` lists whatever
232
237
  has not. Writing a report as end-of-turn text on a tick reaches nobody — that is
233
238
  how a suite release and two tier-2 escalations went missing on 2026-08-06 — and
234
239
  `telegram_send` reaches somebody but leaves no record that it did, so a report
235
240
  sent that way is undetectable when it does not arrive.
236
241
 
242
+ A report is an update. It never contains a request: no "needs you" header, no
243
+ "let me know", no embedded options. Anything needing a decision, approval or
244
+ answer leaves as its own ask (`telegram_ask`) at the moment it is known — the
245
+ question in one sentence, your recommendation, and the options with their
246
+ consequences, the recommended one marked. Batch several questions into one
247
+ ask (the surface takes up to five); never one call per question, and never a
248
+ numbered menu typed into a plain message. Still open a `decision` row for
249
+ anything you ask: the ask is how it reaches a human, the row is what stops it
250
+ being forgotten. In both directions the delivery contract is explicit: a
251
+ message you did not explicitly send is a message that did not arrive.
252
+
237
253
  ## Human messages
238
254
 
239
255
  A human writing to you between ticks is not a tick. Answer with a **single
@@ -335,6 +351,7 @@ the same checks and the same ledger rows, through the CLI:
335
351
  omp-conductor verb conductor_pr_merge --arg prUrl=<url> --arg headSha=<sha> --arg reason=<reason>
336
352
  omp-conductor verb conductor_label --arg issueUrl=<url> --arg label=<name> --arg action=add --arg reason=<reason>
337
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>
338
355
  omp-conductor verb conductor_pr_update_branch --arg prUrl=<url>
339
356
  omp-conductor verb conductor_pr_update --arg prUrl=<url> --arg title=<title>
340
357
  ```
@@ -352,8 +369,37 @@ sessions do not.
352
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. |
353
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. |
354
371
 
355
- **`conductor_pr_merge` wants the SHA you believe you are merging.** Read it with
356
- `conductor_pr_status` and pass it. The dispatcher re-reads the live head
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
357
403
  immediately before merging and refuses on any mismatch, naming both SHAs —
358
404
  because any push since you looked invalidates the green you saw. A refusal there
359
405
  is the mechanism working: re-read, re-check, call again.
@@ -104,9 +104,12 @@ two mistakes. `omp-conductor status` lists anything still undelivered.
104
104
  an answer to their message, or a question of your own. It is not a report: it
105
105
  leaves no record that anything went out. And a `cancelled` or errored
106
106
  `telegram_ask` is a delivery failure, not an answer: re-deliver the question with
107
- `telegram_send`, or report the channel as broken. It is never "asked once, no
107
+ with `telegram_send`, or report the channel as broken. It is never "asked once, no
108
108
  reply, dropped".
109
109
 
110
+ Reports never carry questions: anything needing an answer goes out as its own
111
+ ask, with a recommendation and options.
112
+
110
113
  No scope licenses narration. No progress updates, no "checking the queue
111
114
  now", no restating this brief back. Evidence, or silence.
112
115