omp-conductor 0.3.4 → 0.3.6
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 +204 -6
- package/package.json +1 -1
- package/skills/conductor-onboarding/SKILL.md +98 -5
- package/src/briefs/orchestrator.md +18 -2
- package/src/briefs/worker.md +16 -5
- package/src/cli.ts +60 -0
- package/src/config.ts +40 -4
- package/src/daemon.ts +15 -1
- package/src/graph.ts +508 -0
- package/src/orchestrator-tick.ts +433 -5
- package/src/plugin.ts +329 -90
- package/src/setup.ts +315 -2
- package/src/types.ts +25 -0
- package/src/worktree.ts +81 -2
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ The package ships three deployables, plus one skill:
|
|
|
38
38
|
| --- | --- | --- |
|
|
39
39
|
| omp plugin | `/conductor` slash command | Inspect and arm the conductor from inside an omp session: dry-run the queue, read status, pause, resume. |
|
|
40
40
|
| Standalone daemon | `omp-conductor` binary | The dispatch loop, managed as a background process (`start` / `stop` / `restart`) with a `/healthz` endpoint for a supervisor. |
|
|
41
|
-
| 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. See [Orchestrator tick](#orchestrator-tick). |
|
|
41
|
+
| 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](#orchestrator-tick). |
|
|
42
42
|
| Onboarding skill | `skill://conductor-onboarding` | Directs an omp session to interview you, read your repos for real CI gates, and tailor `ORCHESTRATOR.md` — then finish through the wizard. Discovered automatically once the plugin is installed. See [Onboarding](#onboarding). |
|
|
43
43
|
|
|
44
44
|
The first two are thin wrappers over the same `daemon.ts`, so the plugin and the
|
|
@@ -170,7 +170,7 @@ Onboarding this package has two layers, and installing it gives you both.
|
|
|
170
170
|
|
|
171
171
|
| Layer | What it is | What it owns |
|
|
172
172
|
| --- | --- | --- |
|
|
173
|
-
| **`/conductor setup`** | The deterministic wizard. Closed questions, a label plan, a dry run, one confirm. | **Mechanical config.** It is the only thing that writes `config.json`, and it mutates nothing before you confirm. |
|
|
173
|
+
| **`/conductor setup`** | The deterministic wizard. Closed questions, a label plan, a dry run, one confirm. On a project it already knows, it offers to amend one area instead of re-asking everything — see [Changing one setting](#changing-one-setting). | **Mechanical config.** It is the only thing that writes `config.json`, and it mutates nothing before you confirm. |
|
|
174
174
|
| **`skill://conductor-onboarding`** | A skill bundled in this package (`skills/conductor-onboarding/SKILL.md`), discovered automatically by any omp session once the plugin is installed. | **Brief authoring.** The judgement the wizard cannot prompt for. |
|
|
175
175
|
|
|
176
176
|
The split exists because the two halves fail differently. A wrong config value is
|
|
@@ -215,6 +215,59 @@ turned on you can also invoke it directly:
|
|
|
215
215
|
Nothing about the wizard changes: `/conductor setup` on its own remains a
|
|
216
216
|
complete, supported path, and the brief it renders is safe unedited.
|
|
217
217
|
|
|
218
|
+
### Changing one setting
|
|
219
|
+
|
|
220
|
+
`config.json` is wizard-written, so changing a value means running the wizard —
|
|
221
|
+
and a wizard that re-asks twenty questions to add one key is a wizard people edit
|
|
222
|
+
the file behind instead. So a re-run against a project that is already configured
|
|
223
|
+
opens with one question:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
"veltro" is already configured — what would you like to do?
|
|
227
|
+
> Change one area
|
|
228
|
+
asks one area's questions; every other answer is carried through from the saved config
|
|
229
|
+
Walk every question again
|
|
230
|
+
the full interview, every prompt pre-filled with what is configured now
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Amending is the default. Pick it and the eight areas are listed with what each one
|
|
234
|
+
says right now, so the row you want is the row you can see:
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
Which area? Each row shows what it says now
|
|
238
|
+
tracker & repos — veltrosecurity/veltro, queue "ready-for-agent", "repo:" → veltro, chad, warden, vectorflow
|
|
239
|
+
gates — veltro: none; chad: ruff check . @ backend, pnpm lint @ frontend; warden: ruff check . @ backen…
|
|
240
|
+
caps & worker model — 2 workers, 120 turns, 90m, $25/day, 2 attempts (all defaults) — harness default model
|
|
241
|
+
code graph — not configured — workers grep
|
|
242
|
+
authority — merge=orchestrator, release=orchestrator
|
|
243
|
+
escalation & triage — tier 2 pages Telegram 8236653927, comments too, triage external
|
|
244
|
+
reporting scope — material — escalations, plus green PRs, second failures, and anything that stops the fleet
|
|
245
|
+
orchestrator brief — none at /root/.omp/conductor/worktrees/ORCHESTRATOR.md
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Only that area's questions are asked. Every other answer is read back out of
|
|
249
|
+
`config.json` and written again unchanged — the same answers, the same builder,
|
|
250
|
+
the same single confirm, so there is still exactly one thing in this package that
|
|
251
|
+
writes a config, and it still writes nothing before you agree. The consent screen
|
|
252
|
+
leads with the delta and then shows the whole project as it would be written:
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
amending code graph — project veltro
|
|
256
|
+
was not configured — workers grep
|
|
257
|
+
now /root/.cache/conductor-graph/veltrosecurity — 4 clone(s): veltro, chad, warden, vectorflow
|
|
258
|
+
carried over tracker & repos, gates, caps & worker model, authority, escalation & triage, reporting scope, orchestrator brief
|
|
259
|
+
read back from /root/.omp/conductor/config.json and rewritten unchanged
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
A first run, or a project name this config has never seen, never sees either
|
|
263
|
+
question: there is nothing to amend, so it is the full interview exactly as
|
|
264
|
+
before. Choosing *Walk every question again* is also unchanged — every prompt
|
|
265
|
+
pre-filled with what is configured, Enter to keep it — with one wrinkle worth
|
|
266
|
+
knowing: the two authority confirms and the orchestrator-session confirm cannot
|
|
267
|
+
start on "yes", so Entering through the full interview **revokes** a delegation
|
|
268
|
+
rather than renewing it. Amending the `authority` area names the current grant in
|
|
269
|
+
the question, which is the safer way to leave one alone.
|
|
270
|
+
|
|
218
271
|
### Keeping a brief current
|
|
219
272
|
|
|
220
273
|
Upgrading the package does not upgrade a brief you are already running, and it is
|
|
@@ -524,6 +577,95 @@ honour the pattern it says so, and the daemon logs that per run:
|
|
|
524
577
|
Worth reading the log for. A run that quietly used a weaker model than you chose
|
|
525
578
|
otherwise looks like a run that was merely unlucky.
|
|
526
579
|
|
|
580
|
+
## Code-graph discovery
|
|
581
|
+
|
|
582
|
+
Optional, off unless you answer yes in the wizard, and worth answering yes to for
|
|
583
|
+
one measured reason: **workers spend most of a run finding code, not changing it.**
|
|
584
|
+
On the dogfood fleet a single run typically spends 30–62 `read` calls and 32–69
|
|
585
|
+
`bash` calls against 9–24 edits — 215–390k characters of tool output, roughly four
|
|
586
|
+
fifths of a 120-turn budget — and the runs that died at the turns cap died with
|
|
587
|
+
the work unfinished. A code graph answers "who calls this" and "where is this
|
|
588
|
+
defined" in one call instead of twenty greps.
|
|
589
|
+
|
|
590
|
+
### Two things this package does not do for you
|
|
591
|
+
|
|
592
|
+
`omp-conductor` never installs, spawns, imports or depends on the indexer — with
|
|
593
|
+
`graphProject` unset, nothing about dispatch, caps or escalation changes. That
|
|
594
|
+
means a fresh host needs both of these before an index is worth anything, and
|
|
595
|
+
`graph-setup` reports them as step 0:
|
|
596
|
+
|
|
597
|
+
1. **`codebase-memory-mcp` on PATH** — a separate project,
|
|
598
|
+
[DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp).
|
|
599
|
+
2. **Mounted as an MCP server** in `~/.omp/agent/mcp.json`, on the account the
|
|
600
|
+
daemon runs as. Miss this and the failure is silent: every index builds
|
|
601
|
+
correctly, no worker session can read any of them, so workers fall back to
|
|
602
|
+
grepping and the feature looks like a no-op. `graph-setup` prints the entry.
|
|
603
|
+
|
|
604
|
+
Say yes and the wizard asks for one root, then derives one clone per routed repo
|
|
605
|
+
underneath it (default `~/.cache/conductor-graph/<org>/<repo>`) and writes it to
|
|
606
|
+
each repo's [`graphProject`](#configuration). Nothing else changes: this package
|
|
607
|
+
never runs an indexer, never imports one, and behaves identically with the graph
|
|
608
|
+
server absent — dispatch, caps and escalation do not know it exists. On a fleet
|
|
609
|
+
that was configured before this key existed, `/conductor setup` and the `code
|
|
610
|
+
graph` area add it in two prompts — see
|
|
611
|
+
[Changing one setting](#changing-one-setting).
|
|
612
|
+
|
|
613
|
+
### Why the clone, and not your checkout or the worktree
|
|
614
|
+
|
|
615
|
+
This is the part that decides whether the feature helps or hurts, so it is worth
|
|
616
|
+
being blunt about all three candidates.
|
|
617
|
+
|
|
618
|
+
| Directory | Why not |
|
|
619
|
+
| --- | --- |
|
|
620
|
+
| **The worker's worktree** | An index is keyed by the realpath of the directory it was built from, and has no git-worktree awareness. A run's `worktrees/<issue>` path is therefore *always* an empty project — a worker that queried its own cwd would get silence, conclude there is no graph, and spend the run grepping. This is why `graphProject` is an absolute path in the config and not something derived at run time. |
|
|
621
|
+
| **Your own checkout** | Refreshing an index means resetting the clone to its default branch. In a directory you work in, that either destroys uncommitted work or — if it is made safe instead — indexes whatever feature branch you left checked out, so the fleet orients against your WIP. |
|
|
622
|
+
| **A conductor mirror** | The daemon's mirrors are bare. There is no working tree to index. |
|
|
623
|
+
|
|
624
|
+
So `graphProject` names a fourth thing: a clone that exists only to be indexed,
|
|
625
|
+
that nothing human ever edits, and that is therefore safe to `git reset --hard`
|
|
626
|
+
every night. The worker brief names that path, tells the session to match it
|
|
627
|
+
against `list_projects`' `root_path` and query by the `name` beside it, and says
|
|
628
|
+
plainly that the graph is a snapshot which does **not** contain the worker's own
|
|
629
|
+
edits — orient with it, then read the real file before changing it.
|
|
630
|
+
|
|
631
|
+
### Creating and refreshing them
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
omp-conductor graph-setup # print the plan: clones, index commands, units
|
|
635
|
+
omp-conductor graph-setup --write # stage the script and the two units (no root)
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
`graph-setup` prints a `git clone` for every clone that does not exist yet, the
|
|
639
|
+
one-shot index command per repo, and a `cbm-reindex.service` + `cbm-reindex.timer`
|
|
640
|
+
pair built from the project's own repos and branches. `--write` stages all three
|
|
641
|
+
in the state directory and prints the two `sudo` lines that install and enable
|
|
642
|
+
them; it never runs `systemctl`.
|
|
643
|
+
|
|
644
|
+
**Run it as the account the fleet runs as, never under `sudo`** — it refuses if
|
|
645
|
+
you try. Everything it derives resolves per-account: the config it loads, the
|
|
646
|
+
state directory it stages into, and the `HOME`/`User=` it bakes into the unit.
|
|
647
|
+
Under root you get a timer that goes green while writing indexes into
|
|
648
|
+
`/root/.cache`, where no worker session looks — silent, and indistinguishable
|
|
649
|
+
from the feature simply not helping. Only installing the units needs root, which
|
|
650
|
+
is why that is two separate printed commands.
|
|
651
|
+
|
|
652
|
+
Two properties of the generated unit are deliberate:
|
|
653
|
+
|
|
654
|
+
- **It is a timer, not the server's own watcher.** That watcher lives inside a
|
|
655
|
+
connected MCP session and dies with it, so an ephemeral worker session keeps
|
|
656
|
+
nothing fresh. The refresh has to come from outside the fleet.
|
|
657
|
+
- **It fails loudly.** The refresh is `set -euo pipefail`, then per repo
|
|
658
|
+
`git fetch --prune origin` and `git reset --hard origin/<its own defaultBranch>`
|
|
659
|
+
before indexing. Nothing is `|| true`-ed, so a fetch that has been broken for a
|
|
660
|
+
week turns the unit red instead of quietly re-indexing a stale tree and exiting
|
|
661
|
+
`0` — a green timer serving a month-old graph is worse than no graph at all.
|
|
662
|
+
|
|
663
|
+
The unit spells out `HOME` and an explicit `PATH`, because systemd supplies
|
|
664
|
+
neither usefully: the indexer resolves its store from `HOME`, systemd's default
|
|
665
|
+
`PATH` has no `~/.local/bin`, and the indexer shells out to `git`. Both are the
|
|
666
|
+
user that ran `graph-setup`; the unit sets no `User=`, so check them if that is
|
|
667
|
+
not the account the timer runs as.
|
|
668
|
+
|
|
527
669
|
## Escalation tiers
|
|
528
670
|
|
|
529
671
|
| Tier | Meaning | Raised by | Delivered to |
|
|
@@ -600,6 +742,10 @@ The file is validated on every read. A malformed config produces one readable er
|
|
|
600
742
|
listing every fault, and the daemon refuses to start rather than running with half
|
|
601
743
|
a project.
|
|
602
744
|
|
|
745
|
+
`/conductor setup` is the only thing here that writes this file, and on a project
|
|
746
|
+
it already knows it can rewrite one area of it without re-asking the rest — see
|
|
747
|
+
[Changing one setting](#changing-one-setting).
|
|
748
|
+
|
|
603
749
|
`version` is `2`. A `version: 1` file still loads: caps it names that this build no
|
|
604
750
|
longer enforces are dropped rather than treated as typos, and the next save writes
|
|
605
751
|
it back as `2`. In a `version: 2` file an unrecognised cap key **is** an error,
|
|
@@ -638,7 +784,8 @@ A complete, valid config for one project with two target repos:
|
|
|
638
784
|
"gates": [
|
|
639
785
|
{ "cmd": "bun run lint", "cwd": "." },
|
|
640
786
|
{ "cmd": "bun test", "cwd": "." }
|
|
641
|
-
]
|
|
787
|
+
],
|
|
788
|
+
"graphProject": "~/.cache/conductor-graph/acme/api"
|
|
642
789
|
},
|
|
643
790
|
"worker": {
|
|
644
791
|
"name": "worker",
|
|
@@ -687,6 +834,7 @@ Field notes:
|
|
|
687
834
|
| `routing.labelPrefix` | Optional; defaults to `repo:`. |
|
|
688
835
|
| `routing.repos` | At least one entry, or nothing can be routed. `name` defaults to the map key, `defaultBranch` to `main`. |
|
|
689
836
|
| `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. |
|
|
837
|
+
| `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). |
|
|
690
838
|
| `caps` | Per-project overrides; omit it or pin only the fields you want to change. |
|
|
691
839
|
| `escalation.fallbackToIssueComment` | Defaults to `true`. Absent means "yes, still tell me". |
|
|
692
840
|
| `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. |
|
|
@@ -723,7 +871,8 @@ in the orchestrator's working directory:
|
|
|
723
871
|
| `intervalSeconds` | yes | — | Whole seconds between ticks, minimum `60`. A tick costs a full turn of a frontier model, so a sub-minute period is refused rather than obeyed. |
|
|
724
872
|
| `armedFile` | no | none — the gate passes | Path to the arm marker. A tick does nothing while the file is missing. Relative paths resolve against the session cwd, so `state/armed` means `<cwd>/state/armed`. |
|
|
725
873
|
| `accessFile` | no | none — the gate passes | Path to the Telegram bridge's `access.json`. Every tick re-reads it and requires `enabled: true` with exactly one entry in `allowFrom`. Relative paths resolve against the session cwd. **Configure this on any fleet deploy** — see below. |
|
|
726
|
-
| `message` | no | `Tick <ISO timestamp>: re-read <workspaceRoot>/ORCHESTRATOR.md from disk, then run your standing loop from it.`, then the `reporting.scope` line, then the delivery rule | Sent verbatim when set — and then it owns the whole contract: neither the scope line nor the delivery rule is appended to a prompt you wrote yourself. Re-read from disk on **every** tick, so rewording it binds the next heartbeat instead of waiting for a session restart; a re-read that fails — caught mid-edit, removed, or invalid — keeps the value read at session start rather than stopping the heartbeat. `intervalSeconds` is *not* re-read: rescheduling a live timer still needs a restart. The default *orders* the session to re-read its brief, naming the path resolved from the
|
|
874
|
+
| `message` | no | `Tick <ISO timestamp>: re-read <workspaceRoot>/ORCHESTRATOR.md from disk, then run your standing loop from it.`, then the `reporting.scope` line, then the delivery rule | Sent verbatim when set — and then it owns the whole contract: neither the scope line nor the delivery rule is appended to a prompt you wrote yourself. Re-read from disk on **every** tick, so rewording it binds the next heartbeat instead of waiting for a session restart; a re-read that fails — caught mid-edit, removed, or invalid — keeps the value read at session start rather than stopping the heartbeat. `intervalSeconds` is *not* re-read: rescheduling a live timer still needs a restart. The default *orders* the session to re-read its brief, naming the path resolved from the project's `workspaceRoot`, because a standing prompt drifts out of a long-lived session's context while the file on disk does not. |
|
|
875
|
+
| `agentName` | no | `fleet` | The herdr agent name the orchestrator's pane is registered under. Under herdr this is the whole of the identity check below, and the default matches `AGENT_NAME=${AGENT_NAME:-fleet}` in the recovery plugin's `recover.sh`, so both halves key on one name. Rename the agent and set this to match. |
|
|
727
876
|
|
|
728
877
|
A tick sends one message (`customType` `omp-conductor.tick`, attributed to the
|
|
729
878
|
user): the standing-loop prompt, the one constraint line the project's
|
|
@@ -737,8 +886,6 @@ watched succeed. The tick starts a turn if the session is idle; while a turn is
|
|
|
737
886
|
streaming it is queued as a follow-up and consumed when that turn ends.
|
|
738
887
|
It sends **nothing** when:
|
|
739
888
|
|
|
740
|
-
- `/conductor pause` (or `omp-conductor pause`) holds the pause flag, the same
|
|
741
|
-
flag the dispatch loop reads, so pausing the fleet pauses its heartbeat;
|
|
742
889
|
- `armedFile` is configured and missing;
|
|
743
890
|
- `accessFile` is configured and the escalation channel is not verifiably up;
|
|
744
891
|
- an earlier tick is still queued. Ticks coalesce rather than stack, so a slow
|
|
@@ -746,6 +893,53 @@ It sends **nothing** when:
|
|
|
746
893
|
in a row are the signal that the session is not slow but wedged, which is
|
|
747
894
|
what the [stall marker](#a-wedged-session-and-the-marker-that-notices) is for.
|
|
748
895
|
|
|
896
|
+
### One session per directory ticks, and it says which
|
|
897
|
+
|
|
898
|
+
Activation is a property of the *directory*, so before it arms anything the
|
|
899
|
+
heartbeat asks whether this session is the orchestrator or merely a session
|
|
900
|
+
standing in its directory. It has to: opening a second omp session in the fleet's
|
|
901
|
+
cwd — a shell to read state, say — used to arm a second heartbeat that prompted
|
|
902
|
+
*that* session with the standing loop, and with
|
|
903
|
+
[`authority`](#configuration) delegated it would consider itself entitled to
|
|
904
|
+
merge PRs and cut releases. Two brains, one queue, and nothing in the log to tell
|
|
905
|
+
them apart.
|
|
906
|
+
|
|
907
|
+
**Under herdr** (`HERDR_ENV=1` with a `HERDR_PANE_ID`), the answer is the pane's
|
|
908
|
+
registered agent name: the heartbeat asks `herdr agent list` for the entry whose
|
|
909
|
+
`pane_id` is this pane's and ticks only when its `name` equals `agentName`. Fleetness
|
|
910
|
+
is the *session* — every pane in it shares `HERDR_SESSION` and the cwd — and
|
|
911
|
+
herdr's `agent` field is the *runtime*, `omp` for the orchestrator and for the
|
|
912
|
+
shell beside it, so neither can tell them apart. The registered name can, it is
|
|
913
|
+
what `herdr agent start fleet --kind omp --pane <id>` sets, and it is the same
|
|
914
|
+
identity the recovery plugin keys on. A pane with a different name, or no name at
|
|
915
|
+
all, stays inert.
|
|
916
|
+
|
|
917
|
+
**Without herdr**, the session claims the directory in a sibling
|
|
918
|
+
`.conductor-tick-owner.json` (pid, session file, claim time) and ticks only while
|
|
919
|
+
it is the live claimant. Liveness is a **pid check, never a timestamp**: a crashed
|
|
920
|
+
orchestrator's claim is reclaimed by the next session rather than wedging the
|
|
921
|
+
fleet until somebody deletes a file, and a slow-but-running orchestrator never
|
|
922
|
+
loses its claim to a lease that expired.
|
|
923
|
+
|
|
924
|
+
Declining is logged once, at session start, naming the holder — which is the whole
|
|
925
|
+
point, because the original failure was that the second ticker was
|
|
926
|
+
indistinguishable from the first:
|
|
927
|
+
|
|
928
|
+
```text
|
|
929
|
+
[omp-conductor] orchestrator tick inactive: pane w1:p1 (agent "fleet") owns the fleet tick here — this session will not tick
|
|
930
|
+
[omp-conductor] orchestrator tick inactive: this pane is agent "scratch", not the fleet agent "fleet" — this session will not tick
|
|
931
|
+
[omp-conductor] orchestrator tick inactive: pid 4147344 (claimed 2026-08-07T07:55:36.001Z, session …/fleet.jsonl) owns the fleet tick in /root/.omp/conductor — this session will not tick
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
A `herdr agent list` that does not answer also declines, for the same reason the
|
|
935
|
+
escalation channel fails closed: under herdr this session is one pane of several
|
|
936
|
+
in that directory, and an unproven identity is exactly the case the check exists
|
|
937
|
+
for. That includes `herdr` not being on the session's `PATH` — worth checking on a
|
|
938
|
+
fleet host, where the orchestrator's environment comes from a unit file rather
|
|
939
|
+
than a login shell — and `HERDR_BIN_PATH` names the binary when it is not, the
|
|
940
|
+
same escape hatch the recovery plugin's `recover.sh` has. On a host with no herdr
|
|
941
|
+
and no prior claimant — the ordinary single-session case — nothing changes.
|
|
942
|
+
|
|
749
943
|
### The escalation channel is a gate, and it fails closed
|
|
750
944
|
|
|
751
945
|
Unattended dispatch is only defensible while a tier-2 escalation can reach a
|
|
@@ -863,6 +1057,7 @@ omp-conductor unblock <issue> [--project NAME]
|
|
|
863
1057
|
omp-conductor daemon [--once] [--port N] [--project NAME]
|
|
864
1058
|
omp-conductor pause
|
|
865
1059
|
omp-conductor resume
|
|
1060
|
+
omp-conductor graph-setup [--project NAME] [--write]
|
|
866
1061
|
omp-conductor brief-upgrade [--apply] [--file PATH] [--project NAME]
|
|
867
1062
|
omp-conductor help
|
|
868
1063
|
```
|
|
@@ -881,6 +1076,8 @@ omp-conductor help
|
|
|
881
1076
|
| `--project NAME` | Pick the project to service. One daemon process serves exactly one project; with several configured projects the name is required. |
|
|
882
1077
|
| `pause` | Stop claiming new work. The running daemon notices on its next tick; runs already in flight finish. The orchestrator heartbeat keeps ticking — its gate is the arm marker, not this flag. |
|
|
883
1078
|
| `resume` | Allow claiming again. |
|
|
1079
|
+
| `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). |
|
|
1080
|
+
| `--write` | Only for `graph-setup`. Writes the refresh script into the state directory and the two units into `/etc/systemd/system`, then prints the exact `systemctl daemon-reload && systemctl enable --now cbm-reindex.timer` to run. It never runs `systemctl` itself and never enables anything: that needs root, and a package that enables system timers behind your back is one you cannot audit by reading its output. |
|
|
884
1081
|
| `brief-upgrade` | Compare a project's `ORCHESTRATOR.md` against the brief this version of the package ships. Reports by default; see [Keeping a brief current](#keeping-a-brief-current). |
|
|
885
1082
|
| `--apply` | Only for `brief-upgrade`. Replaces the half above the `YOURS TO EDIT` banner and keeps everything below it, backing the previous file up first. Ignored when the brief cannot be split or the template is unrendered. |
|
|
886
1083
|
| `--file PATH` | Only for `brief-upgrade`. Check a brief that is not where the wizard would have put it, on a host that may have no config at all. |
|
|
@@ -921,6 +1118,7 @@ dispatcher. The brief is explicit about the boundary:
|
|
|
921
1118
|
| It may | It must not |
|
|
922
1119
|
| --- | --- |
|
|
923
1120
|
| Read the issue and the repo's own guidance (`AGENTS.md`, `CLAUDE.md`, `CONTEXT.md`, relevant ADRs) before writing anything. | Touch any path outside its worktree, or switch branches. |
|
|
1121
|
+
| Query its repo's [code graph](#code-graph-discovery), when one is configured, by the project name whose `root_path` matches the clone its brief names. | Query that graph by its own cwd or worktree path — no index of a worktree exists — or treat what it returns as current. It is a snapshot of the clone's default branch; the real file in the worktree wins. |
|
|
924
1122
|
| Edit code inside its own worktree. | Weaken, skip, delete or loosen **any test it did not write** — that is a design question to escalate, and it is checked by diff review before the push. |
|
|
925
1123
|
| Add or update tests for behaviour it introduced. | Suppress a warning, delete an assertion, or special-case an input to make a check pass. |
|
|
926
1124
|
| Run the repo's configured cheap gates, each from its listed `cwd`, over the whole tree. | Run docker or image builds, production builds, browser/e2e suites, or the full test suite on the shared host — CI owns the heavy gates. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-onboarding
|
|
3
|
-
description: Interview-driven onboarding for omp-conductor. Use when the user wants to set up conductor, onboard a new fleet or project, configure the fleet, asks for conductor setup help, asks what belongs in ORCHESTRATOR.md, or wants an agent's release and merge authority scoped and written down. Interviews the operator on release policy, escalation taste and reporting scope, reads each routing repo's CI to propose the real pre-push gates, learns the product and roadmap the fleet will groom, scaffolds the release procedure from the repo's own release workflows rather than from the operator's memory, tailors ORCHESTRATOR.md from the shipped template, verifies the worker brief's assumptions against the actual repos,
|
|
3
|
+
description: Interview-driven onboarding for omp-conductor. Use when the user wants to set up conductor, onboard a new fleet or project, configure the fleet, asks for conductor setup help, asks what belongs in ORCHESTRATOR.md, or wants an agent's release and merge authority scoped and written down. Interviews the operator on release policy, escalation taste and reporting scope, reads each routing repo's CI to propose the real pre-push gates, learns the product and roadmap the fleet will groom, scaffolds the release procedure from the repo's own release workflows rather than from the operator's memory, tailors ORCHESTRATOR.md from the shipped template, verifies the worker brief's assumptions against the actual repos, finishes through the deterministic /conductor setup wizard, then builds the code-graph indexes workers query instead of grepping.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Onboarding a conductor fleet
|
|
@@ -549,9 +549,11 @@ You have the answers ready, so this is fast — and it stays the wizard's decisi
|
|
|
549
549
|
to write, not yours. It asks, in this order: project name; tracker repo; queue
|
|
550
550
|
label; whether to rename the state labels; routing label prefix; then per repo the
|
|
551
551
|
routing key, clone URL, default branch and **pre-push gates** (your Step 2
|
|
552
|
-
proposal, in `cmd @ cwd` form); whether to add another repo;
|
|
553
|
-
|
|
554
|
-
|
|
552
|
+
proposal, in `cmd @ cwd` form); whether to add another repo; whether to set up
|
|
553
|
+
**code-graph discovery** and the root its clones live under (Step 8); caps; the
|
|
554
|
+
authority confirms; the worker model; the Telegram chat id for tier 2; the
|
|
555
|
+
escalation fallback; whether an orchestrator session already runs elsewhere; the
|
|
556
|
+
report scope; and finally whether to write `ORCHESTRATOR.md`.
|
|
555
557
|
|
|
556
558
|
Two things about the end of it that you must not smooth over:
|
|
557
559
|
|
|
@@ -606,7 +608,98 @@ Operators conflate these, and the failure modes are not the same.
|
|
|
606
608
|
|
|
607
609
|
---
|
|
608
610
|
|
|
609
|
-
## Step 8 —
|
|
611
|
+
## Step 8 — build the code graph, if they said yes to it
|
|
612
|
+
|
|
613
|
+
Only if the wizard's code-graph question was answered yes. It is optional, and a
|
|
614
|
+
fleet without it works exactly as it did before — but it is the cheapest single
|
|
615
|
+
improvement to how far a worker gets, so lead with the number: **workers spend
|
|
616
|
+
most of a run finding code, not changing it.** Measured on the reference fleet, a
|
|
617
|
+
run typically spends 30–62 `read` and 32–69 `bash` calls against 9–24 edits, and
|
|
618
|
+
the runs that hit the turns cap hit it with the work unfinished. A graph answers
|
|
619
|
+
"who calls this" in one call instead of twenty greps.
|
|
620
|
+
|
|
621
|
+
Say the thing operators get wrong before you run anything: **the indexed
|
|
622
|
+
directories are conductor's, not theirs.** Three candidates and only one works.
|
|
623
|
+
|
|
624
|
+
- A worker's **worktree** cannot be indexed usefully — an index is keyed by the
|
|
625
|
+
realpath it was built from, so a throwaway `worktrees/<issue>` path is always an
|
|
626
|
+
empty project. That is why the brief hands the worker an absolute path instead.
|
|
627
|
+
- Their **own checkout** must not be indexed. Refreshing means resetting to the
|
|
628
|
+
default branch, which in a directory they work in either destroys uncommitted
|
|
629
|
+
work or indexes the feature branch they left checked out.
|
|
630
|
+
- Conductor's **mirrors** are bare. No working tree, nothing to index.
|
|
631
|
+
|
|
632
|
+
So each `graphProject` is a fourth thing: a disposable clone that exists only to
|
|
633
|
+
be indexed, pinned to the repo's default branch, never edited by a human. Say that
|
|
634
|
+
out loud, because an operator who points it at `~/projects/<repo>` to "save disk"
|
|
635
|
+
has armed something that will one day `git reset --hard` over their work.
|
|
636
|
+
|
|
637
|
+
Two host prerequisites come before any of that, and neither is conductor's to
|
|
638
|
+
install. `graph-setup` reports both as step 0, so run it first and read that
|
|
639
|
+
block before running anything else.
|
|
640
|
+
|
|
641
|
+
- **The indexer must be on PATH.** `codebase-memory-mcp` is a separate project
|
|
642
|
+
([source](https://github.com/DeusData/codebase-memory-mcp)); the package never
|
|
643
|
+
installs, spawns or depends on it. A host without it gets command-not-found
|
|
644
|
+
partway down the plan.
|
|
645
|
+
- **It must be mounted as an MCP server for sessions**, in `~/.omp/agent/mcp.json`
|
|
646
|
+
on the account the daemon runs as. This is the one that bites, because it fails
|
|
647
|
+
*silently*: indexing succeeds, the databases are real and correct, and worker
|
|
648
|
+
sessions have no graph tools at all — so every worker quietly greps and the
|
|
649
|
+
whole thing looks like it simply did not help. `graph-setup` prints the exact
|
|
650
|
+
entry to paste, pointed at the binary it found.
|
|
651
|
+
|
|
652
|
+
Check the mount on the daemon's account, not yours — a per-user config that is
|
|
653
|
+
present for the operator and absent for the service account looks fine from the
|
|
654
|
+
shell they are typing in.
|
|
655
|
+
|
|
656
|
+
Then, on the host that runs the daemon:
|
|
657
|
+
|
|
658
|
+
```bash
|
|
659
|
+
omp-conductor graph-setup # read-only: prints the whole plan
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Walk them through what it printed rather than pasting it silently. It has three
|
|
663
|
+
parts, and each one is a decision they can still refuse: a `git clone` per missing
|
|
664
|
+
clone, an index command per repo (minutes each — run them now, or the first worker
|
|
665
|
+
queries an empty graph), and a `cbm-reindex.service` + `cbm-reindex.timer` pair
|
|
666
|
+
derived from their own repos and branches. Then:
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
omp-conductor graph-setup --write # stages the script and the two units (no root)
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Have them run this as the account the fleet runs as, not under `sudo`.** The
|
|
673
|
+
command refuses sudo outright, and that refusal is the whole point: config path,
|
|
674
|
+
state directory, `~/.cache` and the unit's own `User=` all resolve per-account,
|
|
675
|
+
so a root run stages a timer that goes green while writing indexes into
|
|
676
|
+
`/root/.cache` where no worker session looks. It is silent, and it looks exactly
|
|
677
|
+
like the feature not helping.
|
|
678
|
+
|
|
679
|
+
`--write` stages all three files in the state directory and prints the two `sudo`
|
|
680
|
+
lines that install and enable them — installing units is the only privileged
|
|
681
|
+
step, and it never runs `systemctl` itself. Have them start the service once by
|
|
682
|
+
hand and read the result: a first real run is where a wrong branch or a missing
|
|
683
|
+
clone shows up, and the unit is written to fail loudly rather than index a stale
|
|
684
|
+
tree.
|
|
685
|
+
|
|
686
|
+
Two things to leave them with:
|
|
687
|
+
|
|
688
|
+
- **A timer, not the server's own watcher.** That watcher lives inside a connected
|
|
689
|
+
MCP session and dies with it, so nothing a worker mounts keeps anything fresh.
|
|
690
|
+
If the timer is not enabled, the graph decays and no one is told.
|
|
691
|
+
- **The graph is a snapshot, and the brief says so.** Workers are told to orient
|
|
692
|
+
with it and then read the real file before editing, because the index is the
|
|
693
|
+
default branch at the last reindex — not their branch, and not their edits.
|
|
694
|
+
|
|
695
|
+
Verify before moving on: `codebase-memory-mcp cli list_projects` must show one
|
|
696
|
+
entry per repo whose `root_path` is exactly the configured `graphProject`. That
|
|
697
|
+
match is the whole contract — the worker brief tells the session to find its
|
|
698
|
+
project by that path, so a mismatch means a silent fallback to grep.
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## Step 9 — hand over the learning loop
|
|
610
703
|
|
|
611
704
|
Finish by telling the operator the truth about what they just wrote:
|
|
612
705
|
|
|
@@ -176,12 +176,23 @@ Not yours to relax:
|
|
|
176
176
|
it, never you. A fleet that patches its own dispatcher is a fleet whose
|
|
177
177
|
behavior nobody can reproduce, and the next install silently reverts the
|
|
178
178
|
patch, which is worse than never having made it.
|
|
179
|
+
- **A worker's branch is theirs; the PR is yours to steer.** Never `git checkout`,
|
|
180
|
+
commit or push inside a worker's worktree, never cut a branch from one, never
|
|
181
|
+
force-push or rewrite history anywhere, and never author a commit under an
|
|
182
|
+
invented identity. But `gh pr update-branch` **is** yours to run and is the
|
|
183
|
+
sanctioned remedy for a green PR that has fallen behind: it is a server-side
|
|
184
|
+
merge of the base into the head, it destroys nothing, it rewrites nothing, and
|
|
185
|
+
under a ruleset that requires branches to be up to date it is the only way a
|
|
186
|
+
correct PR ever merges. Closing a green PR to make a fresh worker redo the
|
|
187
|
+
merge costs a whole attempt to buy what one command does in minutes — do not.
|
|
188
|
+
Bypassing branch protection with admin rights is still forbidden; updating the
|
|
189
|
+
branch is how you satisfy it, not how you dodge it.
|
|
179
190
|
|
|
180
191
|
**Your own** merge and release authority is not decided here. It is whatever your
|
|
181
192
|
operator granted at setup time, stated in the first paragraph of **Releases**
|
|
182
193
|
below; ungranted, it is none — you do not merge, tag, publish or deploy either.
|
|
183
194
|
That grant is a deliberate operator decision, changed by re-running setup rather
|
|
184
|
-
than by editing this file. The
|
|
195
|
+
than by editing this file. The five boundaries above are not.
|
|
185
196
|
|
|
186
197
|
## Learning loop
|
|
187
198
|
|
|
@@ -248,7 +259,12 @@ eventually publishes something at 03:00. Spell out all seven.
|
|
|
248
259
|
- **Whether you may merge**, and which PRs. Release work usually needs it, and a
|
|
249
260
|
procedure that has you landing a PR without saying so leaves you inferring
|
|
250
261
|
permission. Note that **one at a time, re-checked against the base branch** binds
|
|
251
|
-
you here exactly as it binds a human; that part is a hard boundary.
|
|
262
|
+
you here exactly as it binds a human; that part is a hard boundary. When that
|
|
263
|
+
re-check finds a green PR that is merely *behind*, the answer is
|
|
264
|
+
`gh pr update-branch` and a wait for the fresh run — never closing it, and
|
|
265
|
+
never an admin bypass. Merging promptly is itself the remedy that stops the
|
|
266
|
+
next PR falling behind: a queue of green PRs left unmerged makes each one
|
|
267
|
+
stale in turn.
|
|
252
268
|
- **The release authority**, named. Which workflow or command ships this repo, and
|
|
253
269
|
how it is invoked. If it is a protected or dispatchable workflow, your
|
|
254
270
|
instruction is to *dispatch it and verify the run*. You never reproduce what it
|
package/src/briefs/worker.md
CHANGED
|
@@ -39,16 +39,27 @@ files are canonical; your priors are not.
|
|
|
39
39
|
mid-refactor loses the run. If code-graph MCP tools are mounted (a
|
|
40
40
|
`codebase-memory` server or similar), start there: list its indexed projects,
|
|
41
41
|
and query by **project name** — your worktree is a throwaway path the index
|
|
42
|
-
has never seen, so a cwd-based lookup finds nothing while the
|
|
43
|
-
|
|
42
|
+
has never seen, so a cwd-based lookup finds nothing while the clone that was
|
|
43
|
+
actually indexed has the whole call graph. Fall back to grep where the graph
|
|
44
44
|
is silent. Either way, trace the real flow end to end — every file the change
|
|
45
45
|
touches — and check the callers of any function you are about to change; the
|
|
46
46
|
smallest diff in the wrong place is a second bug.
|
|
47
|
-
2. **
|
|
47
|
+
{{GRAPH_HINT}}2. **One read per file, not one per question.** A turn that reads forty lines
|
|
48
|
+
costs exactly what a turn that rewrites a module costs, and you have a fixed
|
|
49
|
+
number of them. So take every range you already know you want in a single
|
|
50
|
+
call — `read path.py:1-40,120-160,300-340` — rather than returning to the
|
|
51
|
+
same file three times as each question occurs to you. When you do not yet
|
|
52
|
+
know the ranges, read the file once and keep what you learned instead of
|
|
53
|
+
re-reading a neighbouring slice later. Measured on this fleet: one run spent
|
|
54
|
+
58 reads, of which 20 were consecutive reads of the *same* file and 28 were
|
|
55
|
+
return visits to a file it had already opened — roughly a sixth of its whole
|
|
56
|
+
budget, on a run that then died with the work unfinished. The same applies to
|
|
57
|
+
`grep`: one pattern that answers the question beats three that narrow it.
|
|
58
|
+
3. **Follow existing patterns.** A second convention beside an existing one is a
|
|
48
59
|
defect. Reuse the helper that already exists rather than writing a sibling.
|
|
49
|
-
|
|
60
|
+
4. **Keep the diff small.** Small PRs merge; large ones conflict. If the issue
|
|
50
61
|
genuinely cannot be done small, stop and escalate rather than ballooning.
|
|
51
|
-
|
|
62
|
+
5. **Fix the root cause, never the symptom.** Do not suppress a warning, delete an
|
|
52
63
|
assertion, or special-case an input to make a check pass.
|
|
53
64
|
|
|
54
65
|
## Tests — read this carefully
|
package/src/cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { checkBrief, formatBriefStatus, writeMergedBrief } from "./brief-upgrade.ts";
|
|
11
11
|
import { findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
|
|
12
12
|
import { dbPath, formatStatus, runDaemon, setPaused, statusSnapshot } from "./daemon.ts";
|
|
13
|
+
import { formatGraphSetup, graphRepos, writeGraphSetup, type GraphSetupWrite } from "./graph.ts";
|
|
13
14
|
import {
|
|
14
15
|
clearRecord,
|
|
15
16
|
DEFAULT_PORT,
|
|
@@ -38,6 +39,7 @@ usage:
|
|
|
38
39
|
omp-conductor daemon [--once] [--port N] [--project NAME]
|
|
39
40
|
omp-conductor pause
|
|
40
41
|
omp-conductor resume
|
|
42
|
+
omp-conductor graph-setup [--project NAME] [--write]
|
|
41
43
|
omp-conductor brief-upgrade [--apply] [--file PATH] [--project NAME]
|
|
42
44
|
omp-conductor help
|
|
43
45
|
|
|
@@ -63,6 +65,13 @@ usage:
|
|
|
63
65
|
and exits. This is what \`start\` launches.
|
|
64
66
|
pause stop claiming new work. The running daemon notices on its next tick.
|
|
65
67
|
resume allow claiming again.
|
|
68
|
+
graph-setup
|
|
69
|
+
print how to set up the code-graph indexes workers query instead of
|
|
70
|
+
grepping: the clone commands for any missing index-only clone, the
|
|
71
|
+
index command per repo, and a systemd service+timer that keeps them
|
|
72
|
+
current. --write writes the two units and the script they run, and
|
|
73
|
+
prints the systemctl line to run — it never runs systemctl itself.
|
|
74
|
+
Exits 1 when no repo in the project has graphProject configured.
|
|
66
75
|
brief-upgrade
|
|
67
76
|
compare a project's ORCHESTRATOR.md against the brief this version of
|
|
68
77
|
the package ships. Reports by default; --apply replaces the half above
|
|
@@ -451,6 +460,57 @@ try {
|
|
|
451
460
|
process.stdout.write("resumed — work will be claimed on the next tick\n");
|
|
452
461
|
break;
|
|
453
462
|
|
|
463
|
+
case "graph-setup": {
|
|
464
|
+
// Refused rather than accommodated. Under sudo every path this command
|
|
465
|
+
// derives — the config it loads, the state directory it writes to, the
|
|
466
|
+
// HOME and User= it bakes into the unit — resolves as root instead of the
|
|
467
|
+
// fleet's account, and the result is a timer that goes green while
|
|
468
|
+
// building indexes in a store no worker session ever reads. Nothing about
|
|
469
|
+
// that announces itself, so the only safe answer is to stop.
|
|
470
|
+
if (process.env["SUDO_USER"] !== undefined) {
|
|
471
|
+
process.stderr.write(
|
|
472
|
+
"omp-conductor: run graph-setup as the account the fleet runs as, not under sudo.\n" +
|
|
473
|
+
`Under sudo the config, ~/.cache and the unit's User= all resolve as root, and the\n` +
|
|
474
|
+
`indexes land where no worker can read them. Only installing the units needs root,\n` +
|
|
475
|
+
"and this command prints those two lines for you at the end.\n",
|
|
476
|
+
);
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const project = findProject(loadConfig(), flag(argv, "project"));
|
|
481
|
+
if (graphRepos(project).length === 0) {
|
|
482
|
+
// Not a warning: with nothing configured there is nothing to print, and
|
|
483
|
+
// the fix is a wizard answer rather than a flag on this command.
|
|
484
|
+
process.stderr.write(
|
|
485
|
+
`omp-conductor: no repo in project "${project.name}" has graphProject set — re-run\n` +
|
|
486
|
+
"/conductor setup and say yes to code-graph discovery.\n",
|
|
487
|
+
);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!argv.includes("--write")) {
|
|
492
|
+
process.stdout.write(`${formatGraphSetup(project)}\n`);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let result: GraphSetupWrite;
|
|
497
|
+
try {
|
|
498
|
+
result = writeGraphSetup(project);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
// No longer the permissions case — all three files go to this account's
|
|
501
|
+
// own state directory — so this is a full disk, a read-only mount or a
|
|
502
|
+
// state directory someone else owns. Say what failed and offer the
|
|
503
|
+
// printed plan, which is a complete substitute for the write.
|
|
504
|
+
process.stderr.write(
|
|
505
|
+
`omp-conductor: could not stage the files (${err instanceof Error ? err.message : String(err)}).\n` +
|
|
506
|
+
"Drop --write and copy the printed text yourself — it is the same content.\n",
|
|
507
|
+
);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
process.stdout.write(`wrote:\n${result.written.map((f) => ` ${f}`).join("\n")}\n\n${result.next}\n`);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
|
|
454
514
|
case "brief-upgrade": {
|
|
455
515
|
// `--file` exists because a real fleet's brief is often not where the wizard
|
|
456
516
|
// would have put it: the supervising session runs from its own directory, and
|