omp-conductor 0.9.1 → 0.12.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
@@ -538,7 +538,8 @@ add waits with it.
538
538
 
539
539
  `omp-conductor board [--project NAME]` opens the same facts as a live terminal
540
540
  kanban instead of a scrolling wall of status text. Its columns are Queue,
541
- Claimed, Running, Green, Blocked, Failed, Orphaned, Merged, and History.
541
+ Claimed, Running, Green, Blocked, Failed, Orphaned, Merged, Settled, and
542
+ Parked.
542
543
 
543
544
  Every column is a claim about an issue's state **now**, and each issue appears
544
545
  in exactly one of them. Claimed, Running, and Green come from the store, because
@@ -549,10 +550,17 @@ label, Blocked is the blocked state label, and Orphaned is an issue the
549
550
  in-progress label still claims with no worker running it — a protected orphan
550
551
  whose worktree is kept because it may hold uncommitted work. So a requeued issue
551
552
  is queue work even when its last attempt failed, and a closed issue is not
552
- active work at all. Merged and History are the two backward-looking columns,
553
- both bounded to the last 24 hours and neither one an action list: History holds
553
+ active work at all. Merged and Settled are the two backward-looking columns,
554
+ both bounded to the last 24 hours and neither one an action list: Settled holds
554
555
  recently finished runs that no current label claims, so the board stays
555
- operational rather than becoming an analytics archive.
556
+ operational rather than becoming an analytics archive. Parked is the issue's
557
+ last word when the tracker has **not** confirmed it closed — a terminal run
558
+ whose issue wears no state or queue label (still open, or a label read that
559
+ failed), so nothing dispatches it until a human labels it. Unlike Merged and
560
+ Settled it is not bounded to 24 hours, because parked work does not stop being
561
+ the issue's only remaining record; Settled alone asserts the issue is closed,
562
+ so an unreadable probe parks rather than letting a run masquerade as finished
563
+ work.
556
564
 
557
565
  The board refreshes run, spend, turn, and dispatch values from SQLite every
558
566
  second. It refreshes the slower daemon, Herdr, Telegram, code-graph, and tracker
@@ -1004,6 +1012,13 @@ effective value is persisted and shown beside that active run in `status`; editi
1004
1012
  flight. A cap that fires aborts the run, records it as `killed`, and names the
1005
1013
  ceiling in the escalation.
1006
1014
 
1015
+ Pause one live worker cooperatively with
1016
+ `omp-conductor worker pause <issue> [--project NAME]`. The daemon aborts the
1017
+ active turn to an idle harness state, freezes the remaining wall-clock budget,
1018
+ and keeps the run in the Running lane. `omp-conductor worker resume <issue>`
1019
+ continues the same session with a prompt to re-check its last action before
1020
+ proceeding. This is separate from fleet-level `pause`, which stops new claims.
1021
+
1007
1022
  ### Plan allowance (`planUsage`)
1008
1023
 
1009
1024
  `dailySpendUsd` meters money, which is the only thing an API-billed account can
@@ -1458,6 +1473,7 @@ persists the class on the row, and performs the one recovery that class names.
1458
1473
  | `admin-kill` | killed *below* its own ceiling — a restart or a drain | requeue | none |
1459
1474
  | `ci-infra` | PR open, every unresolved check cancelled / timed out / stale | re-run the failed jobs | none |
1460
1475
  | `ci-deterministic` | PR open, a check genuinely reports `FAILURE` | escalate with the failing check names and links | failed attempt |
1476
+ | `provider-credit` | the provider refused the run for credit (HTTP 402, or its own out-of-credit text read off the transcript) | pause the fleet and require `omp-conductor resume` once the provider has credit | none |
1461
1477
  | `unknown` | anything unrecognised | escalate | as recorded |
1462
1478
 
1463
1479
  **Unknown escalates; it never silently retries.** A shape this table does not
@@ -1466,11 +1482,14 @@ cause nobody has named — the behaviour this exists to end.
1466
1482
 
1467
1483
  ### The budgets follow the cause
1468
1484
 
1469
- `failuresFor` (implementation attempts) excludes `ci-infra`, `settlement-stuck`
1470
- and `env-start-failure`; `continuationsFor` excludes `admin-kill`,
1471
- `settlement-stuck` and `env-start-failure`. An environment fault charges neither:
1472
- the session never started, so nothing about the issue was attempted. A merge conflict *is* charged as a continuation, because a
1473
- rebase is real work just never as a failed implementation attempt.
1485
+ `failuresFor` (implementation attempts) excludes `ci-infra`, `settlement-stuck`,
1486
+ `env-start-failure` and `provider-credit`; `continuationsFor` excludes
1487
+ `admin-kill`, `settlement-stuck`, `env-start-failure` and `provider-credit`. An
1488
+ environment fault charges neither: the session never started, so nothing about
1489
+ the issue was attempted, and nor does a provider-credit refusal, which is a
1490
+ billing state rather than an implementation failure. A merge conflict *is*
1491
+ charged as a continuation, because a rebase is real work — just never as a
1492
+ failed implementation attempt.
1474
1493
 
1475
1494
  An **unclassified** row (every row written before 0.4.3) counts exactly as it
1476
1495
  did before classification existed. Upgrading therefore changes no existing
@@ -1575,7 +1594,8 @@ A complete, valid config for one project with two target repos:
1575
1594
  { "cmd": "bun run lint", "cwd": "." },
1576
1595
  { "cmd": "bun test", "cwd": "." }
1577
1596
  ],
1578
- "graphProject": "~/.cache/conductor-graph/acme/api"
1597
+ "graphProject": "~/.cache/conductor-graph/acme/api",
1598
+ "migrations": { "dir": "backend/alembic/versions" }
1579
1599
  },
1580
1600
  "worker": {
1581
1601
  "name": "worker",
@@ -1647,12 +1667,13 @@ Field notes:
1647
1667
  | `routing.repos` | At least one entry, or nothing can be routed. `name` defaults to the map key, `defaultBranch` to `main`. |
1648
1668
  | `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. |
1649
1669
  | `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). |
1670
+ | `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. |
1650
1671
  | `caps` | Per-project overrides; omit it or pin only the fields you want to change. |
1651
1672
  | `escalation.fallbackToIssueComment` | Defaults to `true`. Absent means "yes, still tell me". |
1652
1673
  | `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. |
1653
1674
  | `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. |
1654
1675
  | `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. |
1655
- | `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. |
1676
+ | `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). |
1656
1677
  | `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). |
1657
1678
  | `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). |
1658
1679
  | `workspaceRoot` / `mirrorRoot` | Optional; default to `worktrees/` and `mirrors/` under the state directory. `~` is expanded. |
@@ -1661,6 +1682,27 @@ Prefer an SSH `cloneUrl`, or an https URL backed by a credential helper. A clone
1661
1682
  with credentials embedded is persisted into the mirror's git config, exactly as it
1662
1683
  would be for a hand-run clone.
1663
1684
 
1685
+ ### Reporting policy (`reporting`)
1686
+
1687
+ What may interrupt the operator's phone, and when the daily rollup happens. Two
1688
+ spellings, mutually exclusive in one config (the loader rejects a `scope` next to
1689
+ `interruptOn`/`digest`):
1690
+
1691
+ - **Preset** — `reporting.scope`, the three legacy values, mapped verbatim:
1692
+ - `material` (default) → `interruptOn: [tier2, decision-needed, fleet-stopped, confirmed-failure, material]`, digest `per-tick`.
1693
+ - `decisions` → `interruptOn: [tier2, decision-needed, fleet-stopped]`, digest `per-tick`.
1694
+ - `escalations` → `interruptOn: [tier2, fleet-stopped]`, digest `daily` (model-timed).
1695
+ - **Explicit** — `reporting: { "interruptOn": ["tier2", "fleet-stopped", ...], "digest": { "cadence": "none" | "per-tick" | "daily" } }`.
1696
+ `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`.
1697
+
1698
+ A tier-2 escalation whose category is **not** in `interruptOn` is not dropped: it
1699
+ is held (`held_notices`) and the next accepted digest is its delivery authority.
1700
+ The digest itself is at-most-once per local day (`digest:<YYYY-MM-DD>` in the
1701
+ configured zone), which remains the delivery authority across restarts. A
1702
+ scheduled `daily` digest is only sent on a day it has not already run, once the
1703
+ local clock has passed `at`; a restart after `at` still sends today's (one
1704
+ catch-up), and a fully missed day is skipped, never sent late.
1705
+
1664
1706
  ### Merge and release preconditions (`policy`)
1665
1707
 
1666
1708
  These used to be sentences in your `POLICY.md`: when a PR may be merged, what
@@ -1947,6 +1989,8 @@ omp-conductor disarm [--project NAME]
1947
1989
  omp-conductor release-pane [--project NAME]
1948
1990
  omp-conductor tail <issue> [--project NAME]
1949
1991
  omp-conductor extend <issue> --turns N [--project NAME]
1992
+ omp-conductor worker pause <issue> [--project NAME]
1993
+ omp-conductor worker resume <issue> [--project NAME]
1950
1994
  omp-conductor unblock <issue> [--force] [--no-requeue] [--project NAME]
1951
1995
  omp-conductor verb <conductor_*> [--project NAME] [--arg k=v ...]
1952
1996
  omp-conductor friction <escalation-digest|report-noise|report-surprise> --detail TEXT [--issue N] [--project NAME]
@@ -1971,7 +2015,7 @@ omp-conductor help
1971
2015
  | `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. |
1972
2016
  | `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. |
1973
2017
  | `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`. |
1974
- | `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, and the last 24 hours of Merged and History. 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. |
2018
+ | `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. |
1975
2019
  | `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). |
1976
2020
  | `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. |
1977
2021
  | `arm [--project NAME]` | Proof-gated: send a Telegram challenge and write the arm marker only after your reply appears as a user turn in the orchestrator transcript. Never auto-armed by `resume` / `hold`. |
@@ -1979,6 +2023,7 @@ omp-conductor help
1979
2023
  | `release-pane [--project NAME]` | Clear the `halt --pane` recovery pin so herdr-conductor may resume the fleet agent again. |
1980
2024
  | `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>`. |
1981
2025
  | `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. |
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 distinct from fleet-level `pause`, which stops new claims. |
1982
2027
  | `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. |
1983
2028
  | `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`. |
1984
2029
  | `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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.9.1",
3
+ "version": "0.12.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.",