omp-conductor
v0.18.2
Published
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.
Readme
omp-conductor
A 24/7 dispatcher that takes ready-for-agent GitHub issues to green, mergeable
PRs using omp coding sessions. When a run cannot finish on its own it escalates in
tiers: first to an orchestrator session that can re-brief the worker, then to you.
What it is
You label an issue. Within one tick the conductor claims it on the tracker, cuts a worktree, hands one omp worker a self-contained brief, and watches it to a green PR. The worker then stops: it does not merge, tag, publish or deploy.
Scope
One issue, one green PR. That is the worker's whole remit. A change that needs a
merge or release is reported to the orchestrator, whose merge and release
authority are chosen during setup and default to human. Granting either action
to the orchestrator never grants it to a worker or the dispatch daemon. Releases
remain batched from coherent groups of merged work, never cut one per worker PR.
Code counts every limit that decides whether work starts: concurrency, dollars per day, turns and wall clock per worker, failed implementation attempts, and operational continuations per issue. None is left to the model. The dispatcher enforces these budgets before anything is claimed and kills workers that exceed their per-run limits.
When a run does get stuck, the first responder is not you. A tier-1 escalation is
injected into a long-lived orchestrator session that can read the issue and the
run's transcript and then either re-brief the worker or decide the problem genuinely
needs a human. It never edits product code and never pushes a branch; whether it
may merge or release is a setup answer (authority), and both default to no. Only
tier 2 pages you directly.
The package ships two deployables:
| Deployable | Entry | What it is for |
| --- | --- | --- |
| Everything an operator does | omp-conductor binary | The only operator surface: setup, inspect, control, and the dispatch loop itself as a background process (start / stop / restart) with a /healthz endpoint for a supervisor. |
| Orchestrator heartbeat | omp extension, activated by .conductor-tick.json | Prompts a 24/7 orchestrator session on a fixed interval so its standing loop actually runs, and marks the session stalled when its prompts stop being consumed. Inert in every other session — including a second session opened in the fleet's own directory. See Orchestrator tick. |
There is no slash command and no skill. Earlier releases shipped a
/conductor command and a skill://conductor-onboarding; both are gone. The
interview they wrapped now lives in the binary as omp-conductor setup, which is
the documented path for every operator and agent — see Onboarding.
An omp chat session that wants conductor state or wants to change it shells out
to omp-conductor, which is why the verb ledger records who asked: a session
that ran a verb is indistinguishable from an operator who ran the same verb, and
that is the point.
Claiming is gated by the pause flag; tick sends are gated by the arm marker — they
are not the same switch. Prefer hold when you want both quiet.
Your workflow vs. the package
This package stops at green PRs. The boundary is in the worker brief and in the loop itself: no worker merges, tags, pins, deploys or publishes, and neither does the orchestrator. Everything past a green PR — when to merge, what to batch into a release, what to deploy — is your workflow, and the package deliberately holds no opinion about it that it could act on.
Your opinion goes in ORCHESTRATOR.md, the standing prompt for the long-lived
session that supervises the fleet. That file is yours: the conductor renders it
once, on request, and then never reads it back, never rewrites it and never
enforces a word of it. What you write there binds your orchestrator session and
nothing else in this package.
omp-conductor setup offers to render the shipped template
(src/briefs/orchestrator.md) to <workspaceRoot>/ORCHESTRATOR.md with your
project's coordinates filled in, and never replaces an existing file without a
second, explicit confirmation. It is a starting point rather than a contract:
| Section | Whose |
| --- | --- |
| Duties (drain, groom, report), escalation tiers, hard boundaries | Fixed — they describe how this package already behaves. |
| Releases | Yours. Its opening paragraph is rendered from the authority you answered in setup — by default "humans release, and you do not merge". Everything under it is the procedure, and a delegated session is told not to cut a release until you have written one: what, when, on what proof, and what stays permanently forbidden. |
| Reporting | Yours, seeded from the scope you chose in setup. |
Reporting is the one half of that the config also knows about, because the wizard has to ask something in order to seed the brief, and it is the one half the runtime acts on:
| Setup choice | What may interrupt | What waits |
| --- | --- | --- |
| material | Every configured material event, when operator availability permits. | Non-bypass events outside configured hours wait for the next configured digest or opening. |
| escalations | Tier-2 escalations, when operator availability permits. | Everything else waits for the configured digest. |
| decisions (recommended) | Tier-2 decisions and fleet-stopping conditions, when operator availability permits. | Every other material event is recorded durably and ships with the configured digest as one message. |
| quiet | Tier-2 escalations, fleet stops, and confirmed failures, when operator availability permits. | Everything else waits for one daily rollup. |
Install
omp plugin install omp-conductorThat installs the omp-conductor binary and the orchestrator heartbeat extension.
It registers no slash command and no skills — everything an operator does is a
verb on the binary, starting with omp-conductor setup.
After install — and after every omp-conductor upgrade — run
omp-conductor doctor once: it checks the deployment faults that have
previously cost debugging sessions (gh auth/scopes, exact-case labels, systemd
unit drift and dir ownership, config + backup freshness, sqlite integrity,
spend telemetry, reporting timezones, Telegram health) and exits 0 only when
nothing failed.
From a checkout of the monorepo, ./setup.sh checks both plugins. It preserves
an existing npm-managed omp-conductor and links only the Herdr half, so running
setup on a release-based fleet cannot silently switch omp to mutable source.
./setup.sh install --force-link is the explicit opt-in to link both checkout
directories.
For what the host needs before anything runs — bun, an authenticated gh,
git, a model credential the daemon's own account can reach, and
omp-telegram for the escalation channel — see the
Install prerequisites in the reference.
Shell completions
omp-conductor complete generates completions for zsh, bash, fish, and
PowerShell. Load them for the current shell:
source <(omp-conductor complete zsh)
# or
source <(omp-conductor complete bash)For a persistent zsh install:
mkdir -p ~/.omp/conductor
omp-conductor complete zsh > ~/.omp/conductor/completions.zsh
echo '[ -f ~/.omp/conductor/completions.zsh ] && source ~/.omp/conductor/completions.zsh' >> ~/.zshrcUse bash and ~/.bashrc for the equivalent bash install. In fish, run
omp-conductor complete fish | source; in PowerShell, run
omp-conductor complete powershell | Out-String | Invoke-Expression.
After a successful interactive setup, the wizard offers to install zsh or bash
completions this way and adds the rc source line only when it is absent.
Quick start
Install
omp-conductorandomp-telegram. Pair the Telegram bot and enable its bridge.Open the long-lived omp session in the fleet workspace.
The default workspace is
~/.omp/conductor/worktrees. The wizard shows the actual path before it writes files.Run the wizard:
omp-conductor setupThe wizard reads the tracker with the same routing code as the daemon. It shows every issue that the next tick can route.
On an interactive TTY, setup uses the styled Clack interface. Piped setup keeps the stable line protocol for automation. Set
OMP_CONDUCTOR_PLAIN_UI=1to use the readline interface on a TTY and replace Clack progress in other verbs with their stable plain messages.To make an interactive run replayable, save every accepted answer by its stable key:
omp-conductor setup --save-answers ./setup-answers.json omp-conductor setup --answers ./setup-answers.jsonAn answers file is a JSON object whose values are strings or booleans. Confirmations require booleans, text prompts require strings, and select prompts require the exact displayed label.
--answersnever falls back to an interactive prompt: a missing key or wrong value fails with the key and question name. Piped line input remains supported, but an exhausted pipe fails with the--answersalternative instead of waiting indefinitely.Nothing changes before the consent step. After consent, setup does these actions:
- creates the required labels;
- writes
config.jsonand the selected brief; - writes
.conductor-tick.jsonfor external orchestration; - stages
omp-conductor.serviceunder the conductor state directory; - runs
daemon --oncewhile dispatch is paused; - starts a temporary daemon and proves
/healthzand stored status; - stops the temporary daemon;
- proves the current inbound Telegram path before it arms or resumes an external heartbeat;
- clears the dispatch pause only after all required gates pass.
If the arm proof cannot complete, setup keeps dispatch paused. The result shows the exact recovery commands.
On a systemd host, run the install commands that setup prints.
The commands install the staged unit, reload systemd, enable the unit, and restart it. The generated unit contains the current user, paths, project, port, and memory ceiling.
On a host without systemd, start the daemon directly:
omp-conductor startRead the layered status:
omp-conductor statusThe result must show a running daemon, a healthy
/healthz, armed ticks for external orchestration, and the configured project.Verify the deployment mechanically before trusting it:
omp-conductor doctordoctorruns read-only and exits 0 only when nothing failed: gh auth and scopes, exact-case labels, installed-systemd-unit drift, runtime-dir ownership, config + backup freshness, sqlite integrity, spend telemetry, reporting timezones, and Telegram health. Run it again after everyomp-conductor upgrade— every one of these failures has cost a debugging session silently, and each has a one-line fix in its finding.
First worker drill
Use a disposable target repository for this drill. Replace the values below with labels that the wizard showed.
TRACKER=acme/planning
QUEUE=ready-for-agent
ROUTE=repo:api
gh issue create --repo \"$TRACKER\" \
--title \"Conductor setup drill: add a marker file\" \
--body $'Add `conductor-smoke.txt` to the target repository.\\n\\nAcceptance: the file contains `setup path verified` and the pull request checks pass.' \
--label \"$ROUTE\" \
--label \"$QUEUE\"The daemon claims the issue, creates a worktree, runs the configured gates, and opens a pull request. Follow it with:
omp-conductor status
omp-conductor tail <issue-number>Merge the green pull request. Then make sure that status moves the run to merged and frees its worker slot.
The loop ticks every 5 minutes while workers remain active. Merge settlement and capacity checks do not wait for the longest worker.
omp-conductor stop drains active workers before it stops. On systemd, it uses systemctl stop to prevent an automatic restart.
The package also ships a generic unit at
systemd/omp-conductor.service.example.
Operating the fleet
The fleet is a long-lived orchestrator session plus the dispatch daemon behind it. These are the verbs you reach for day to day.
The fleet host
Both halves point at the same 24/7 omp session on your always-on host. The
session's working directory is configured as FLEET_CWD; it contains
.conductor-tick.json, whose relative armedFile also resolves from that
directory. Conductor state defaults to ~/.omp/conductor, while workspaceRoot
defaults to its worktrees/ directory. Setup composes the package floor and the
fleet-owned POLICY.md into ORCHESTRATOR.md there. No /root layout is built
into a new deployment.
The Herdr half owns recovery, not dispatch or policy: it restores the exact session identity, requests an immediate heartbeat, or reports through Telegram and a Herdr notification that the fleet is down.
The worker identity and its harness binding
Worker sessions do not run as you. setup host creates a dedicated
unprivileged account, omp-worker (home /var/lib/omp-worker), and the daemon
launches every worker session under it through setpriv. The account is granted
search access to the paths a session needs and read access to your agent config
files; its writable world is the worktree and session directory dispatch hands
it per run, and nothing else.
That boundary means the account cannot list your home — which is also where
omp-conductor and its @oh-my-pi/pi-coding-agent peer are installed, and
module resolution needs to list a directory to find the node_modules inside
it. Left there, a worker silently resolved a different harness version out of
its own package cache. So setup host also installs one mount unit,
var-lib-omp\x2dworker\x2dharness-node_modules.mount, binding your install
read-only at /var/lib/omp-worker-harness/node_modules, and launches worker
children from that path. It is a bind, not a copy: upgrade the harness and every
worker picks it up with nothing to re-materialise. The mount's parent directory
is root:omp-worker 0750, so the bound tree is reachable by root and the
worker account and by no other local account.
Three consequences worth knowing:
omp-conductormust be installed, not run from a source checkout, for worker dispatch to work — a checkout has no install root to bind.- If the binding is missing or no longer resolves to your install, the daemon
refuses to launch workers and says so (
setup hostreports the same thing as pending work). Re-runomp-conductor setup host; no attempt is charged to the issue, because no session ever started. - The check runs at every launch, not once at daemon startup. The mount unit is ordered before the daemon, so a reboot brings them up in the right order — and if the binding ever arrives late anyway, the next dispatch picks it up with no restart needed.
Stop the conductor (hold / stop)
Two words, and one of them takes a flag:
| Verb | Claiming | Tick sends | Dispatch daemon | Conductor pane |
| --- | --- | --- | --- | --- |
| hold | paused | disarmed | left running | left running |
| hold --keep-ticks | paused | left armed | left running | left running |
| stop | paused | disarmed | stopped (systemctl-aware) | left running |
| stop --pane | paused | disarmed | stopped | stopped + recovery pinned off |
resume clears pause and any stop --pane recovery pin, and never re-arms. arm is proof-gated: it sends a Telegram challenge and writes the arm marker only after the orchestrator's inbound adapter has acknowledged your reply in conductor's own state — no transcript is read, so where (or whether) a session file lives does not matter (#614). stop --pane targets the configured conductor agent only — it does not run systemctl stop herdr-fleet. To bounce the daemon without stopping the fleet, use restart.
Removed in 0.15.0, each exiting 2 with a pointer: halt (now stop), pause (use hold), release-pane (now part of resume), and graph-setup (now setup graph). Dropping pause cost one real capability, "stop claiming but keep ticking", which is now hold --keep-ticks rather than a fifth verb. It matters because disarming is the expensive half of a hold: re-arming sends a Telegram challenge and blocks until you answer it in the chat, so stopping claims for ten minutes otherwise costs a manual round trip to get the heartbeat back — while the workers a hold deliberately leaves running have nothing shepherding them.
status prints a layered header (dispatch / ticks / next tick time / pane / recovery / herdr / telegram / daemon) so a paused fleet cannot hide an armed orchestrator still spending turns. The Telegram line calls the official getMe endpoint to prove the token and API are usable without sending a message, then separately reports whether the inbound bridge is configured.
When the tracker is behind what the store decided, status adds a
labels projection N pending (oldest …) row: those are label
changes the dispatcher committed to and the projector has not been
able to apply yet (a 403, a rate limit). The intended label state is durable, so
nothing is lost and safety is preserved: for an issue the queue read already
returns, a pending state-label removal is overlaid as applied, so a stale
label cannot block redispatch. A pending queue-label add is different — the
queue read asks GitHub for issues carrying the queue label, so that issue stays
invisible to dispatch until the add actually lands. The row is the lag to
watch: it should drain, and while it does not, work waiting on a queue-label
add waits with it.
omp-conductor board [--project NAME] opens the same facts as a live terminal
kanban instead of a scrolling wall of status text. Its columns are Queue,
Claimed, Running, Green, Blocked, Failed, Orphaned, Merged, Settled, and
Parked.
Every column is a claim about an issue's state now, and each issue appears in exactly one of them. Claimed, Running, and Green come from the store, because a live worker or an open pull request is the most current fact there is. Every stopped column comes from the labels the tracker carries right now: Queue is the queue label plus the latest tick's admission holds, Failed is the failed state label, Blocked is the blocked state label, and Orphaned is an issue the in-progress label still claims with no worker running it — a protected orphan whose worktree is kept because it may hold uncommitted work. So a requeued issue is queue work even when its last attempt failed, and a closed issue is not active work at all. Merged and Settled are the two backward-looking columns, both bounded to the last 24 hours and neither one an action list: Settled holds recently finished runs that no current label claims, so the board stays operational rather than becoming an analytics archive. Parked is the issue's last word when the tracker has not confirmed it closed — a terminal run whose issue wears no state or queue label (still open, or a label read that failed), so nothing dispatches it until a human labels it. Unlike Merged and Settled it is not bounded to 24 hours, because parked work does not stop being the issue's only remaining record; Settled alone asserts the issue is closed, so an unreadable probe parks rather than letting a run masquerade as finished work.
The board refreshes run, spend, turn, and dispatch values from SQLite every
second. It refreshes the slower daemon, Herdr, Telegram, code-graph, and tracker
label layer every ten seconds or immediately with r; the header reports
tracker ok, or how stale the label read is and why, so an empty Queue is never
mistaken for an empty queue. Use arrow keys or h/j/k/l to select a card,
Enter to inspect and follow its worker transcript, u to run the normal
unblock workflow on a Blocked, Failed, or Orphaned card, i / p to open the
issue / pull request, ? for help, and q, Esc, or Ctrl-C to go back and
quit. Narrow terminals show a sliding subset of columns around the selection;
terminals below 50×20 get a single resize instruction instead of a broken
layout. The board is read-only except for the explicit u action: it never
claims work or changes stages.
halt --pane is fail-closed: it exits 0 only when the conductor agent is
proven gone. It writes the recovery pin first, so a failed stop still cannot be
undone by herdr-conductor respawning the agent, and then refuses (nonzero exit,
message on stderr) on every uncertainty:
- no tick config exists at all —
recover.shreads only$FLEET_CWD/.conductor-pane-halted, and without that file the pane's own directory is unknown, so the pin would land somewhere recovery never looks and the agent would be respawned seconds later.resumerefuses for the same reason, andstatusshowsrecovery unpinnablerather thanclear - the tick config does not parse — the agent name would be a guess
herdr agent listis unreachable, prints nothing, or prints output with no explicitagentsarray; only a realagents: []means "no agents"- an agent row is unreadable — a missing
name/pane_id, or anagentfield present with a non-string value. An absent ornullagentis the sticky claim herdr reports after the agent exits, and stays a normal answer - the configured agent name is not unique, or the claimed pane runs some other agent
pane process-infofails, or the claim is liveompbut names no recognizable omp foreground PID — "cannot see it" is never reported as "it is stopped"- a signal cannot be delivered, or liveness cannot be probed — only
ESRCH("no such process") proves death, soEPERMreads as "exists, not ours", never as "stopped" - the process is still alive after
SIGTERMthenSIGKILL
The pin is written to the pane's own directory (the one holding
.conductor-tick.json, which is FLEET_CWD — the only place recover.sh looks),
including when that tick config is the thing that failed to parse.
Clear the pin with omp-conductor resume when you want recovery again.
See the fleet in a browser: dashboard
omp-conductor dashboard serves the same fleet facts status and board
render, in a browser: a static UI plus one read endpoint, GET /api/projects,
which answers with every configured project annotated with its daemon state
(the same livingDaemon + /healthz classification status uses), the
daemon's port, and the raw /healthz body when it answers. It is a separate
process from the dispatch daemon and only ever reads fleet state — nothing on
the daemon port changes.
Binds 127.0.0.1:8788 by default. First start mints a bearer token at
<stateDir()>/dashboard-token (mode 0600, next to config.json under
$OMP_CONDUCTOR_HOME) and later starts reuse it; the page asks for the token
once and keeps it in its own localStorage. Every /api/* request must carry
Authorization: Bearer <token>; static assets are unauthenticated by design.
Tailnet posture. Loopback is the default because it is the safe one: any
local user could otherwise read your project list. To look at the dashboard
from another machine on your tailnet, run
omp-conductor dashboard --host <tailnet-ip>; the server still binds, but
prints a one-line warning naming the token file — the token is then the only
thing between anyone on that network and the fleet.
See the fleet: status and board
omp-conductor status and omp-conductor board are the two windows onto the
fleet, and both are covered above under
Stop the conductor because that is where the
paused-fleet failure hides. tail, extend, unblock and the worker
controls are the day-to-day levers:
For automation, status --json emits the same project, layer, daemon,
Telegram, code-graph, run, cap, report, and sibling data as the text report.
The other read-only ledgers have stable JSON forms too:
omp-conductor ledger --json
omp-conductor decision list --json
omp-conductor watch list --jsonJSON output is never styled. Empty ledgers return their normal top-level object
with an empty entries, decisions, or watches array.
Outcomes and cost: stats
omp-conductor stats [--since 7d | 30d | YYYY-MM-DD] [--project NAME] [--json]
answers the operator's basic product question — is this fleet earning its keep
— from the local store only, with zero GitHub calls. Over a bounded window
(default: the last 7 days) it reports, per repo and in total: issues merged,
issues settled (merged or terminally failed/blocked), merge rate, queue→merge
lead time (median and p90), attempts per merged issue, metered spend per merged
issue, the failure-class breakdown of everything that did not merge, and the
tracked GitHub API calls consumed over the window's days.
Continuation chains collapse into one journey: an issue that took six attempts is one merged outcome with six runs and one lead time, measured from its first queue-label claim to the tick's merge settlement. A chain that began before the window still counts its whole lead time and all its attempts.
Runs whose spend reads $0.00 — harness telemetry absent, see
Limitations
— are counted separately as unmetered and never averaged into cost as if
they were free; a merged issue the harness never metered reports its cost as
unknown rather than zero.
--json prints the stable report shape. It is a contract: keys never move, and
the human rendering shows the same numbers:
{
"project": "demo",
"window": { "sinceDay": "2026-08-09", "untilDay": "2026-08-16",
"sinceEpochMs": 1754697600000, "untilEpochMs": 1755302400000 },
"ghCalls": 41,
"empty": false,
"total": {
"repo": "(all)",
"merged": 3, "settled": 4, "mergeRate": 0.75,
"runsPerMerged": 4, "leadTimeMedianMs": 7560000, "leadTimeP90Ms": 34200000,
"spendUsd": 1.23, "spendPerMerged": 0.41,
"unmeteredRuns": 2, "unmeteredMerged": 1,
"failureClasses": { "unknown": 3 }
},
"repos": [ { "repo": "acme/api", "merged": 3, "settled": 3, "mergeRate": 1,
"runsPerMerged": 2, "leadTimeMedianMs": 7560000,
"leadTimeP90Ms": 34200000, "spendUsd": 1.23,
"spendPerMerged": 0.41, "unmeteredRuns": 2, "unmeteredMerged": 1,
"failureClasses": {} } ]
}empty: true means nothing settled in the window — a fresh store or an idle
fleet, not zero measurements. mergeRate, the lead-time fields,
runsPerMerged and spendPerMerged are null when there is nothing to
measure them over, never a fabricated 0. Lead times are settlement
timestamps (the tick that confirmed the merge), so they carry up to one tick
of sweep latency.
Watch a run: tail
omp-conductor tail <issue> follows the newest run for an issue — its
assistant: text and every tool: call as they land — from the top of the
transcript, and prints run ended: <state> when it finishes. Workers are omp
sessions inside the daemon, not terminals, so this is the only way to watch one
live. See tail.
Intervene: extend, unblock, worker
omp-conductor extend <issue> --turns Nraises a live worker's effective turn ceiling, or stores a one-shot ceiling for the issue's next attempt when its latest run is terminal.omp-conductor unblock <issue>clears an issue'sblocked/failedlabels — andagent:in-progresswhen the newest run is terminal — so an answered escalation can be claimed again. It is the way back after you answer a tier-1.omp-conductor worker pause/resume/stop <issue>parks or ends one live worker without touching the fleet;worker stop --reason TEXTterminates, salvages and gives up its in-progress label.
Each is a verb on the binary and takes --project NAME. Full semantics are in
the CLI reference.
Answering a decision
A question the orchestrator put to you — an amendment, a tier-2 decision, "do I
ship this tonight?" — arrives on your phone and is written to the decision
ledger, where it survives compaction, restarts and long ticks instead of living
only in a session's memory. You answer with the omp-conductor decision verbs
(open, list, resolve, withdraw), and a --resolves-when condition lets a
parked question wake up by itself when a PR merges, an issue closes or a check
goes green. See the decision ledger.
Capture an idea: intake
An idea that is not an issue yet has nowhere to live. omp-conductor intake
gives it one, durably — the sqlite store, not a session, so a thought captured
at 02:00 is still there after a restart:
omp-conductor intake "ship the intake command" # prints an id
omp-conductor intake list # id, age, text — oldest first
omp-conductor intake dismiss 3f9c2a1b7e04 # drops one by idstatus shows an intake N pending idea(s) row only while the backlog is
nonzero, so an empty intake stays invisible instead of becoming noise; grooming
a pending idea into an actual issue happens later, not on this surface.
Multi-project operation
One daemon serves every configured project on a host. Each project keeps its own
queue, caps, reporting scope, tick config and arm marker under
~/.omp/conductor/projects/<name>/. The lifecycle verbs below take
--project NAME, and a few take --all for when you mean the whole host at once.
Adding another project
To put a second fleet on the same host without touching the first, run the wizard for the new name only:
omp-conductor setup --project second
# or pick "Add another project" from the re-run chooserThat is a full interview for the new name only. Defaults land under
~/.omp/conductor/projects/<name>/{worktrees,mirrors}, so two fleets never share
a cwd. After apply, setup provisions labels, brief, tick config, topic binding,
smoke and arm for that project only, then prints the two follow-ups:
omp-conductor restart --now— the running daemon picks the new project up only after a reload (printed, not auto-run while workers are live);- a copy-pasteable herdr handoff that starts the new fleet's orchestrator in its own empty pane, never into a live one.
Every other area still amends only that project (setup gates --project second),
and a workspaceRoot that collides with another project is refused. The full
walkthrough — what --project means for each verb, amendment rules and the
--all caveats — is in Onboarding.
Hold, resume, arm — per project, and --all
The lifecycle verbs are per project, so one fleet can be quiet while another keeps dispatching:
| Verb | --project NAME | --all |
| --- | --- | --- |
| hold [--keep-ticks] | pause claiming + disarm that project's ticks | every project |
| stop [--pane] | stop that project (daemon, optionally the pane) | every project |
| arm / disarm | that project's arm marker | every project's marker |
| resume | clear pause + any stop --pane recovery pin | every project |
setup host writes one arm marker per project (armed-<project>), so arming one
fleet never arms another; hold --project A writes paused-<name> for that
project only, while a bare paused sentinel pauses every project. Per-project
tick identity and the shared-marker upgrade are in
Orchestrator tick.
CLI at a glance
Every verb hangs off the single omp-conductor binary; there is no in-session
command. Each takes an optional --project NAME, and
hold / stop / arm / disarm / resume also take --all. The full
reference — usage strings, flags and per-command behaviour — is in the
CLI reference.
| Verb | What it does |
| --- | --- |
| setup [area] | The wizard: interview + probes. --no-ai, --answers FILE, --save-answers FILE, per-area amend, host, graph. |
| start / stop / restart | Run the dispatch daemon. restart drains first; stop --pane also halts the pane. |
| upgrade [--to VERSION] | Pin one published npm release across CLI, omp plugin, Herdr plugin and brief. |
| status [--json] | Layered fleet report: dispatch, ticks, pane, recovery, telegram, daemon, then the project body. |
| stats [--since 7d|30d|YYYY-MM-DD] [--json] | What the fleet accomplished and at what cost, from the local store only: merges, merge rate, lead time, attempts and metered cost per merged issue, failure classes, gh calls. |
| doctor [--json] [--probe-telegram] | Read-only deployment health — gh auth, exact-case labels, systemd drift, config backup, sqlite integrity, spend telemetry, timezones, Telegram. Run after install and after every upgrade; exit 0 only when nothing failed. |
| ledger [--json] | The action audit — every mediated verb, refusal and turn budget. |
| board | Live terminal kanban from Queue to Settled. |
| dashboard [--port N] [--host ADDR] | Browser UI plus bearer-authenticated /api/projects: every project's daemon state, port and healthz. Loopback by default; the token lives at <state>/dashboard-token. |
| hold [--keep-ticks] | Pause claims and disarm ticks — the soft stop. |
| arm / disarm | Gate / clear the orchestrator tick's arm marker. |
| tail <issue> | Follow a live worker's transcript. |
| extend <issue> --turns N | Raise one run's, or one next attempt's, turn ceiling. |
| worker pause/resume/stop <issue> | Park or end one live worker. |
| unblock <issue> | Clear blocked/failed/in-progress so a settled issue can be re-claimed. |
| verb <conductor_*> | Run a mediated verb from the CLI (external orchestration). |
| friction <kind> --detail TEXT | Record a bounded operator judgment — an escalation that belonged in a digest, or a report that was noise/surprising — feeding the learning loop. |
| event record / report / message | Record and deliver reports and messages through the durable outbox. |
| decision open/resolve/withdraw/list [--json] | Read and answer questions in the decision ledger; JSON applies to list. |
| watch add/list [--json] | Record or list orchestrator-only conditions and carry notes; JSON applies to list. |
| intake "<text>" / intake list / intake dismiss <id> | Capture a raw idea durably (it lives in the store and survives restarts), list what is still pending, dismiss what turned out to be nothing. |
| daemon [--once] | Run the loop in the foreground — systemd's entry point. |
| resume | Clear pause and any stop --pane recovery pin; never re-arms. |
| brief-upgrade | Report / migrate / retrofit the ORCHESTRATOR.md ↔ POLICY.md overlay. |
Where issues come from
GitHub Issues is the only supported tracker in v1. tracker.kind accepts
exactly one value, "github", and every tracker operation shells out to your
already-authenticated gh CLI — the conductor never stores a token of its own.
Gitea, Jira, and file-based trackers are not supported yet; the seam for them is
src/tracker/github.ts, which implements the whole nine-method Tracker
interface in src/types.ts (listReady, addLabel, removeLabel, comment,
close, linkParent, parentOf, openCloserFor, prState) that a future
backend would swap in.
You tell the conductor where to look with three keys, all in
~/.omp/conductor/config.json (the Configuration section has
the full annotated example, and omp-conductor setup will interview you for these
and create any missing labels):
| Key | Meaning |
| --- | --- |
| tracker.repo | The one owner/repo whose issue list is the queue. This is your planning repo — it does not have to contain any code. |
| queueLabel | Open issues in tracker.repo carrying this label are the work queue. Nothing else is ever read. Required: the wizard pre-fills ready-for-agent, but a config that omits the key is rejected, not defaulted. |
| routing.repos + repo:<name> labels | Each queued issue must also carry exactly one routing label naming which code repo the work lands in. The conductor cuts the worktree and PR there, from routing.repos[name].cloneUrl. An issue with zero or two routing labels is reported as unroutable and skipped — never guessed. |
So: one tracker repo supplies the queue, routing labels fan issues out to any number of code repos, and both label names are yours to configure.
The reference
The guide stops at "operate the fleet". Everything deeper moves unchanged to
REFERENCE.md:
| Subject | In REFERENCE.md |
| --- | --- |
| The upgrade lifecycle (omp-conductor upgrade) | Updating |
| The wizard internals, editing one setting, the brief overlay | Onboarding |
| What one dispatch tick actually does | How one tick works |
| Routing rules, and why unroutable never guesses | Routing |
| Host sizing and memory | Host sizing and memory |
| Caps and the plan allowance | Caps |
| The worker model | Worker model |
| Code-graph discovery | Code-graph discovery |
| Escalation tiers | Escalation tiers |
| Report delivery (the outbox) | Report delivery |
| The decision ledger | The decision ledger |
| Failure classes and recovery | Failure classes |
| Configuration (config.json) key by key | Configuration |
| The orchestrator tick internals | Orchestrator tick |
| The full CLI reference and health endpoint | CLI reference |
| The mediated verbs | The mediated verbs |
| Known limitations | Limitations |
| The licence | License |
