omp-conductor 0.2.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/LICENSE +21 -0
- package/README.md +732 -0
- package/package.json +40 -0
- package/skills/conductor-onboarding/SKILL.md +626 -0
- package/src/briefs/orchestrator.md +213 -0
- package/src/briefs/worker.md +146 -0
- package/src/cli.ts +179 -0
- package/src/config.ts +446 -0
- package/src/daemon.ts +689 -0
- package/src/escalate.ts +265 -0
- package/src/lifecycle.ts +367 -0
- package/src/omp.ts +273 -0
- package/src/orchestrator-tick.ts +432 -0
- package/src/orchestrator.ts +267 -0
- package/src/plugin.ts +605 -0
- package/src/routing.ts +160 -0
- package/src/setup.ts +644 -0
- package/src/store.ts +263 -0
- package/src/tracker/github.ts +160 -0
- package/src/types.ts +250 -0
- package/src/worker.ts +292 -0
- package/src/worktree.ts +303 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "omp-conductor",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
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.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TerrifiedBug/conductor.git",
|
|
10
|
+
"directory": "omp"
|
|
11
|
+
},
|
|
12
|
+
"main": "./src/plugin.ts",
|
|
13
|
+
"omp": {
|
|
14
|
+
"extensions": [
|
|
15
|
+
"./src/plugin.ts",
|
|
16
|
+
"./src/orchestrator-tick.ts"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"omp-conductor": "./src/cli.ts"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"!src/**/*.test.ts",
|
|
25
|
+
"skills",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"check": "tsc --noEmit",
|
|
31
|
+
"test": "bun test"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@oh-my-pi/pi-coding-agent": ">=17.1.4"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "^1.2.0",
|
|
38
|
+
"typescript": "^5.7.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
---
|
|
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, then finishes through the deterministic /conductor setup wizard.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Onboarding a conductor fleet
|
|
7
|
+
|
|
8
|
+
Onboarding has two layers, and they are not the same job.
|
|
9
|
+
|
|
10
|
+
- **`/conductor setup` is the mechanical layer.** It is deterministic, tested, and
|
|
11
|
+
the **only** thing that writes `config.json`. It asks closed questions, plans
|
|
12
|
+
labels, prints a dry run, and mutates nothing until the operator confirms.
|
|
13
|
+
- **You are the judgement layer.** The wizard cannot ask "where should this fleet
|
|
14
|
+
stop?" and get a useful answer from a text prompt, and it cannot read a repo to
|
|
15
|
+
find out what CI actually runs. That is the part that decides whether an
|
|
16
|
+
unattended fleet is safe, and it is yours.
|
|
17
|
+
|
|
18
|
+
So: **you interview and investigate; the wizard writes.** Never hand-edit
|
|
19
|
+
`config.json`, never construct it and ask for a blessing, never skip the wizard's
|
|
20
|
+
dry run. Its consent gate is the safety property; going around it removes the one
|
|
21
|
+
step where the operator sees what is about to change.
|
|
22
|
+
|
|
23
|
+
Two more ground rules before you start.
|
|
24
|
+
|
|
25
|
+
**Do not recite this file.** Nothing here is a script to read aloud. Every
|
|
26
|
+
question below exists to extract one decision; ask it in your own words, in the
|
|
27
|
+
operator's vocabulary, and skip the ones the repo already answers. An interview
|
|
28
|
+
where you ask and they answer is worth more than a checklist you both step
|
|
29
|
+
through.
|
|
30
|
+
|
|
31
|
+
**Investigate before you ask.** Anything discoverable — default branches, CI
|
|
32
|
+
jobs, gate commands, whether a repo has a `Makefile` — you read, then propose.
|
|
33
|
+
Asking an operator to recite their own CI config is how gates end up wrong: they
|
|
34
|
+
tell you what they *think* runs, and the fleet pushes on it at 03:00.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Step 0 — orient
|
|
39
|
+
|
|
40
|
+
Before the first question, find out where you are:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
gh auth status # repo + project scopes, and who the token is
|
|
44
|
+
cat ~/.omp/conductor/config.json # honours $OMP_CONDUCTOR_HOME
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **Config already there?** This is a re-run or a second project. The wizard
|
|
48
|
+
pre-fills from the existing project and replaces it in place, keeping its
|
|
49
|
+
neighbours — so the interview is now about *what changed*, not everything.
|
|
50
|
+
Read the existing project out loud and ask what is wrong with it.
|
|
51
|
+
- **`gh` missing `repo` or `project`?** Say so now. The wizard warns at the
|
|
52
|
+
confirm, but a token that cannot label issues means every claim fails, and it
|
|
53
|
+
is cheaper to fix before the interview than after.
|
|
54
|
+
- **An `ORCHESTRATOR.md` already exists** at `<workspaceRoot>/ORCHESTRATOR.md`
|
|
55
|
+
(default `~/.omp/conductor/worktrees/ORCHESTRATOR.md`)? Read it. It is the
|
|
56
|
+
operator's accumulated policy, and it outranks the shipped template. Your job
|
|
57
|
+
becomes amending it, and you must warn that a wizard re-run offers to overwrite
|
|
58
|
+
it.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Step 1 — the interview
|
|
63
|
+
|
|
64
|
+
Ask for decisions, not for values you could look up. Each question below carries
|
|
65
|
+
the reason it matters; when you ask it, lead with that reason. An operator who
|
|
66
|
+
understands why a question is being asked gives a usable answer.
|
|
67
|
+
|
|
68
|
+
### Project and tracker
|
|
69
|
+
|
|
70
|
+
**"What is this fleet called, and which one repo holds the queue?"**
|
|
71
|
+
|
|
72
|
+
*Why:* the project name is the handle for everything afterwards
|
|
73
|
+
(`/conductor status <name>`, `--project <name>`), and the tracker is the **one**
|
|
74
|
+
`owner/repo` whose open-issue list *is* the queue. Nothing else is ever read.
|
|
75
|
+
|
|
76
|
+
Say the part that surprises people: **the tracker repo does not have to contain
|
|
77
|
+
any code.** A planning repo whose only content is issues is the normal shape, and
|
|
78
|
+
it is often the better one — the queue then has its own label namespace and its
|
|
79
|
+
own permissions, separate from the code it dispatches into.
|
|
80
|
+
|
|
81
|
+
Then: **"What label means a human has signed this off?"** Default
|
|
82
|
+
`ready-for-agent`. *Why:* this label is the entire consent mechanism for
|
|
83
|
+
unattended work. The conductor never adds it. Everything the fleet ever does
|
|
84
|
+
starts with a human putting that label on an issue, so it should read like a
|
|
85
|
+
decision, not like a status.
|
|
86
|
+
|
|
87
|
+
The three state labels (`agent:in-progress`, `agent:blocked`, `agent:failed`) are
|
|
88
|
+
written *by* the conductor so the tracker alone shows live state. Only ask about
|
|
89
|
+
renaming them if the repo already has a colliding label convention — the
|
|
90
|
+
namespaced defaults are right almost always, and the wizard asks with one confirm.
|
|
91
|
+
|
|
92
|
+
### Routing — what lands where
|
|
93
|
+
|
|
94
|
+
**"Which code repos does this queue dispatch into, and how would you tell them
|
|
95
|
+
apart on an issue?"**
|
|
96
|
+
|
|
97
|
+
*Why:* an issue must carry **exactly one** `repo:<name>` label naming a repo in
|
|
98
|
+
`routing.repos`. Zero or two is reported unroutable and skipped — never guessed.
|
|
99
|
+
Routing is the fan-out: one tracker, any number of code repos.
|
|
100
|
+
|
|
101
|
+
For each repo you need the clone URL and the default branch, but **look those up**
|
|
102
|
+
rather than asking (Step 2). What you actually need from the operator is the
|
|
103
|
+
routing *key* — the word that goes after the prefix — and it should be the word
|
|
104
|
+
they already use in conversation about that repo, because they are the one who
|
|
105
|
+
will be typing it onto issues at 2am.
|
|
106
|
+
|
|
107
|
+
Prefer an SSH clone URL, or an https URL backed by a credential helper. A URL
|
|
108
|
+
with a credential in it gets persisted into the mirror's git config exactly as it
|
|
109
|
+
would for a hand-run clone; say that out loud if you see one.
|
|
110
|
+
|
|
111
|
+
### Release policy — the question that actually matters
|
|
112
|
+
|
|
113
|
+
This is the decision the shipped brief exists to protect, so give it room.
|
|
114
|
+
|
|
115
|
+
First, settle who is even a candidate, because operators assume it is the workers
|
|
116
|
+
and it never is. A worker is scoped to one issue: it cannot judge whether a release
|
|
117
|
+
is worth cutting, and it stops at a green PR permanently. **The only two candidates
|
|
118
|
+
are the orchestrator and a human.** The orchestrator is the right agent for it if
|
|
119
|
+
any agent is: it is long-lived, it can see everything that merged since the last
|
|
120
|
+
release, and batching is exactly the judgement a per-issue session cannot make.
|
|
121
|
+
|
|
122
|
+
So frame it as **three** options, in this order, and name the default:
|
|
123
|
+
|
|
124
|
+
1. **Humans release.** *(default, and what the package ships)* Work ends at a
|
|
125
|
+
green PR. Merging is a separate human action; releasing is a separate human
|
|
126
|
+
action after that. Neither a worker nor the orchestrator ever tags, pins,
|
|
127
|
+
publishes, or deploys. "This needs releasing" becomes something the
|
|
128
|
+
orchestrator *reports*.
|
|
129
|
+
2. **The orchestrator releases up to a named boundary.** Delegating part of the
|
|
130
|
+
release to the supervising session, with the stopping line written down.
|
|
131
|
+
3. **The orchestrator releases fully.**
|
|
132
|
+
|
|
133
|
+
Then ask the question that makes option 2 real:
|
|
134
|
+
|
|
135
|
+
> **"Where does the orchestrator's leg END?"**
|
|
136
|
+
|
|
137
|
+
Not "can it release" — *where does its leg stop*. A boundary you cannot state in
|
|
138
|
+
one sentence is not a boundary, and an orchestrator with a vague release mandate
|
|
139
|
+
is one that eventually publishes something at 03:00.
|
|
140
|
+
|
|
141
|
+
Give them a worked answer so they can calibrate. The Veltro deployment's answer:
|
|
142
|
+
**the agent's leg ends at the merged suite pin.** It may open and land the PR that
|
|
143
|
+
bumps the module image tags, because that artefact is reviewable, reversible by a
|
|
144
|
+
revert, and its correctness is checkable by a named CI check. Deploying that pin
|
|
145
|
+
to prod is **operator territory** — it needs a person who can watch it, and who
|
|
146
|
+
owns the rollback. That is a leg with an end: a merge commit.
|
|
147
|
+
|
|
148
|
+
If they choose option 2, capture all five of these, because the brief needs them
|
|
149
|
+
and a missing one is a hole:
|
|
150
|
+
|
|
151
|
+
- **what** may be released — which packages or images, from which branch;
|
|
152
|
+
- **when** — batched how, after which *named* checks are green;
|
|
153
|
+
- **what proof** must be held first — check results actually read, not an
|
|
154
|
+
impression;
|
|
155
|
+
- **what must still be asked, every time**;
|
|
156
|
+
- **what stays permanently forbidden** — force-push, secrets, production data.
|
|
157
|
+
|
|
158
|
+
If they choose option 3, do not just write it down. Warn, concretely:
|
|
159
|
+
|
|
160
|
+
- **Credentials.** Full release means the session holds publish tokens, registry
|
|
161
|
+
credentials, or deploy keys. Those live in the session's environment, exposed to
|
|
162
|
+
every turn, including a turn that went wrong. Ask whether they are willing to
|
|
163
|
+
put that credential in a process that runs unattended for weeks.
|
|
164
|
+
- **Rollback ownership.** An agent that can release owns the 03:00 rollback too,
|
|
165
|
+
and a rollback is a judgement call under time pressure with partial
|
|
166
|
+
information — the exact thing agents are worst at. If the answer to "who rolls
|
|
167
|
+
this back" is a person, then that person is already the release owner, and
|
|
168
|
+
option 2 with a named boundary is the honest configuration.
|
|
169
|
+
|
|
170
|
+
Push back once if the answer is option 3 and the reasoning is "it'll be fine".
|
|
171
|
+
Then record what they decide. It is their fleet.
|
|
172
|
+
|
|
173
|
+
**Then stop, and do not write the section yet.** What you have is the operator's
|
|
174
|
+
*intent*: how much they want to delegate, and where they want the line. The
|
|
175
|
+
*steps* on their side of that line come from the repo's own release machinery, not
|
|
176
|
+
from this conversation. Step 4 reads that machinery and turns the intent into a
|
|
177
|
+
procedure. Writing release steps from an interview answer is how a brief ends up
|
|
178
|
+
prescribing a hand-rolled release the repo's own policy forbids.
|
|
179
|
+
|
|
180
|
+
### Escalation taste
|
|
181
|
+
|
|
182
|
+
**"What should wake you up, and what can wait for the digest?"**
|
|
183
|
+
|
|
184
|
+
*Why:* there are two tiers, and the split is not adjustable — tier 1 is a worker
|
|
185
|
+
that stopped and asked a question (the orchestrator answers it), tier 2 is
|
|
186
|
+
"nobody can proceed without a human". What *is* adjustable is how tier 2 reaches
|
|
187
|
+
them and how much else comes along with it. An operator who gets paged for
|
|
188
|
+
everything stops reading the pages, which is functionally the same as having no
|
|
189
|
+
escalation channel at all.
|
|
190
|
+
|
|
191
|
+
Concretely, decide:
|
|
192
|
+
|
|
193
|
+
- **Channel.** If `omp-telegram` is installed and paired, the wizard offers that
|
|
194
|
+
chat. If not, tier 2 degrades to an issue comment — a documented fallback, not
|
|
195
|
+
an error, but say plainly that a comment on an issue nobody is watching is a
|
|
196
|
+
page that lands nowhere.
|
|
197
|
+
- **Belt and braces.** Also comment on the issue when a run escalates?
|
|
198
|
+
Recommended yes: a chat message you miss is a run nobody sees, and the comment
|
|
199
|
+
is the durable copy.
|
|
200
|
+
- **Attempts before escalating** (`maxAttemptsPerIssue`, default 3). *Why:* an
|
|
201
|
+
issue that failed three times is not a retry candidate — it is a diagnosis
|
|
202
|
+
task. Setting this high converts a bad issue into a spend line.
|
|
203
|
+
|
|
204
|
+
### Reporting scope
|
|
205
|
+
|
|
206
|
+
**"Do you want to hear about progress, or only about problems?"**
|
|
207
|
+
|
|
208
|
+
*Why:* this is the one half of the brief that the *config* also knows about,
|
|
209
|
+
because every orchestrator tick appends the matching constraint line to its
|
|
210
|
+
prompt, re-read from the config each tick. It maps onto exactly two values:
|
|
211
|
+
|
|
212
|
+
| Answer | `reporting.scope` | What that means |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| "Tell me when things happen." | `material` *(default)* | Escalations, plus every material event as it happens: a run reaching a green PR (with link), a run that failed twice, an issue pulled off the queue, a cap that stopped the fleet. |
|
|
215
|
+
| "Only bother me when I'm needed." | `escalations` | Every tier-2 escalation immediately, plus one daily digest. Every other tick silent. |
|
|
216
|
+
|
|
217
|
+
Two honesty notes to pass on:
|
|
218
|
+
|
|
219
|
+
- **Neither scope is an outbound filter.** Nothing inspects the orchestrator's
|
|
220
|
+
messages and drops the ones the scope did not ask for. It is a constraint handed
|
|
221
|
+
to the model each turn, not a gate it is held to.
|
|
222
|
+
- **Changing the scope later does not rewrite an existing `ORCHESTRATOR.md`.** The
|
|
223
|
+
tick line changes; the brief does not. Whoever changes it must edit the brief's
|
|
224
|
+
Reporting section too, or the session is carrying two versions of the policy.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Step 2 — read the repos, then propose the gates
|
|
229
|
+
|
|
230
|
+
**Do this before you ask anything about gates.** The wizard's gate prompt is a
|
|
231
|
+
free-text field; whatever goes in it is what an unattended worker runs before
|
|
232
|
+
every push. It has to match what CI actually runs, and the operator is not the
|
|
233
|
+
best source for that. Their CI config is.
|
|
234
|
+
|
|
235
|
+
For each routing repo, mirror it read-only if you do not have it locally, then
|
|
236
|
+
read:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
gh api repos/<owner>/<repo> --jq '.default_branch'
|
|
240
|
+
gh api repos/<owner>/<repo>/contents/.github/workflows --jq '.[].name'
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
and then, in the checkout:
|
|
244
|
+
|
|
245
|
+
- `.github/workflows/*.yml` — the PR-triggered jobs and their exact `run:` lines,
|
|
246
|
+
including their `working-directory`;
|
|
247
|
+
- `package.json` — the `scripts` block (`lint`, `check`, `typecheck`, `test`);
|
|
248
|
+
- `Makefile` / `justfile` / `Taskfile.yml` — many repos put the real gate here and
|
|
249
|
+
have CI call `make check`;
|
|
250
|
+
- `pyproject.toml`, `Cargo.toml`, `go.mod` — for the same reason, in other
|
|
251
|
+
ecosystems;
|
|
252
|
+
- any `CONTRIBUTING.md` / `AGENTS.md` line that names the pre-push command.
|
|
253
|
+
|
|
254
|
+
Then **propose**, do not ask. Show your reading and the exact commands, with the
|
|
255
|
+
`cwd` each runs from, in the format the wizard takes (`cmd`, or `cmd @ cwd`,
|
|
256
|
+
comma-separated; `cwd` defaults to `.`):
|
|
257
|
+
|
|
258
|
+
```text
|
|
259
|
+
repo:api — from .github/workflows/ci.yml (on: pull_request), jobs lint + test:
|
|
260
|
+
bun run lint @ . # CI: `bun run lint` at repo root, whole tree
|
|
261
|
+
bun run check @ . # CI: tsc --noEmit
|
|
262
|
+
bun test @ . # CI: bun test
|
|
263
|
+
|
|
264
|
+
Is that the set, and is anything missing that CI would catch?
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Three warnings to carry, every time:
|
|
268
|
+
|
|
269
|
+
1. **Whole tree, not the subdirectory.** CI lints everything. A gate that lints
|
|
270
|
+
only `src/` is how an error in a migration, a config file, a script or a test
|
|
271
|
+
fixture reaches the runners. If CI runs `bun run lint` at the root, the gate is
|
|
272
|
+
`bun run lint` at the root — not `bun run lint src`.
|
|
273
|
+
2. **Cheap gates only.** Do not put docker builds, image builds, production
|
|
274
|
+
builds, browser/e2e suites, or a full integration suite in here. The host is
|
|
275
|
+
shared; CI owns the heavy gates. If a repo's only meaningful check is heavy,
|
|
276
|
+
say so — that repo's workers will lean on CI, and that is a known cost, not
|
|
277
|
+
something to paper over with a fake gate.
|
|
278
|
+
3. **No gates at all is a real answer, and a loud one.** The wizard warns. Repeat
|
|
279
|
+
the warning in plain terms: a repo with no gates means every unattended push
|
|
280
|
+
is an experiment run on the shared runners.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Step 3 — learn the product
|
|
285
|
+
|
|
286
|
+
The duties assume an orchestrator that understands what it is grooming. Duty 2
|
|
287
|
+
asks it to judge whether acceptance criteria are readable and whether an issue is
|
|
288
|
+
worth claiming at all; neither is answerable by a session that knows only the repo
|
|
289
|
+
names. So learn the product now, and write what you learn into the brief.
|
|
290
|
+
|
|
291
|
+
Read, per routing repo:
|
|
292
|
+
|
|
293
|
+
- the `README` — what the thing is for, and who uses it;
|
|
294
|
+
- whatever top-level architecture doc exists (`docs/`, `ARCHITECTURE.md`, an ADR
|
|
295
|
+
directory), enough to say which repo owns which concern;
|
|
296
|
+
- `AGENTS.md` / `CONTRIBUTING.md` for the repo's own rules, which outrank the
|
|
297
|
+
brief.
|
|
298
|
+
|
|
299
|
+
Then read the tracker as a roadmap rather than as a queue:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
gh issue list --repo <tracker> --state open --limit 100 --json number,title,labels,milestone
|
|
303
|
+
gh api repos/<tracker>/milestones --jq '.[] | "\(.title) — \(.open_issues) open"'
|
|
304
|
+
gh label list --repo <tracker>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Milestones and the label taxonomy are what separate a theme from a one-off. Epics
|
|
308
|
+
usually surface as one or the other.
|
|
309
|
+
|
|
310
|
+
Then ask the operator exactly one question, because it is the one thing none of
|
|
311
|
+
that reading answers:
|
|
312
|
+
|
|
313
|
+
> **"Where does the roadmap live, and what is the current priority?"**
|
|
314
|
+
|
|
315
|
+
*Why:* a tracker shows what is open, never what matters. An orchestrator that
|
|
316
|
+
cannot rank work grooms by recency, and that is how a stale issue outranks the
|
|
317
|
+
thing the operator is actually shipping this month.
|
|
318
|
+
|
|
319
|
+
**Write the findings into the brief** as a `## Project context` section, in the
|
|
320
|
+
editable half beside Releases — the template ships a stub for it. Keep it under 40
|
|
321
|
+
lines: it is read on every tick, and a brief nobody finishes reading is a brief
|
|
322
|
+
that gets skimmed. It needs four things.
|
|
323
|
+
|
|
324
|
+
1. **The product in one paragraph.** What it does, for whom. Not a feature list.
|
|
325
|
+
2. **A repo map** — one line per routing key, naming what that repo owns in the
|
|
326
|
+
operator's vocabulary. This is what turns a routing label into a judgement.
|
|
327
|
+
3. **Grooming guidance for Duty 2.** Which repos ship together, so a change in one
|
|
328
|
+
is known to need a matching PR in the other. And which kinds of issue touch the
|
|
329
|
+
same files: those must not be queued concurrently, because two workers editing
|
|
330
|
+
one file produce two PRs that cannot both merge.
|
|
331
|
+
4. **Where the roadmap lives, and how to judge priority against it** — the pointer
|
|
332
|
+
the operator just gave you, in one line.
|
|
333
|
+
|
|
334
|
+
Same order of operations as Step 5: draft it now, and apply it to the brief after
|
|
335
|
+
the wizard has written the file.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Step 4 — scaffold the release process from the repo, not from memory
|
|
340
|
+
|
|
341
|
+
The interview gave you a boundary. This step turns it into steps the operator's
|
|
342
|
+
repos will actually accept, for the same reason Step 2 reads CI instead of asking
|
|
343
|
+
about gates: an operator describes the release they *remember*, and a release is
|
|
344
|
+
the one procedure where being approximately right is worst.
|
|
345
|
+
|
|
346
|
+
Skip this step only for option 1 (humans release). There is nothing to scaffold:
|
|
347
|
+
the shipped paragraph is already correct and the brief keeps it verbatim.
|
|
348
|
+
|
|
349
|
+
### Find the release authority
|
|
350
|
+
|
|
351
|
+
Per repo that can be released, read:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
gh api repos/<owner>/<repo>/contents/.github/workflows --jq '.[].name'
|
|
355
|
+
gh release list --repo <owner>/<repo> --limit 10
|
|
356
|
+
git tag --list --sort=-v:refname | head
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
and then, in the checkout:
|
|
360
|
+
|
|
361
|
+
- **every workflow that publishes anything** — a release, a tag, an image, a
|
|
362
|
+
package, a deployment. For each, the **trigger** is the fact that matters:
|
|
363
|
+
- `workflow_dispatch` → the release is *dispatched*. That workflow is the
|
|
364
|
+
authority, and the correct instruction is "dispatch it with the planned
|
|
365
|
+
version", never "do what it does".
|
|
366
|
+
- `on: push: tags:` → a pushed tag is the trigger, so tagging *is* releasing.
|
|
367
|
+
- `on: release: published` → the GitHub Release is the trigger.
|
|
368
|
+
- **what the workflow enforces.** Many reject a tag unless several version files
|
|
369
|
+
agree. That constraint belongs in the brief, quoted, because it is the failure
|
|
370
|
+
the agent will otherwise hit at 03:00.
|
|
371
|
+
- **`AGENTS.md`, `CONTRIBUTING.md`, and any release runbook.** An explicit policy
|
|
372
|
+
outranks anything you infer from a workflow, and it is usually where the
|
|
373
|
+
*forbidden* paths are named.
|
|
374
|
+
- **whether a release needs a human by construction.** An `npm publish` behind
|
|
375
|
+
interactive 2FA cannot be delegated to an unattended session at all, whatever the
|
|
376
|
+
operator would like. Say so rather than writing a step that cannot run.
|
|
377
|
+
|
|
378
|
+
### Then write three things, and one of them is the forbidden list
|
|
379
|
+
|
|
380
|
+
Present this back as a proposal, in the repo's own commands, before it goes in the
|
|
381
|
+
brief:
|
|
382
|
+
|
|
383
|
+
1. **The authority, named.** One sentence: which workflow or command ships this
|
|
384
|
+
repo, and how it is invoked. If it is a protected workflow, the brief says
|
|
385
|
+
*dispatch it and verify the run* and stops there.
|
|
386
|
+
2. **The steps on the agent's side of the boundary**, as commands, in order, each
|
|
387
|
+
with the check that proves it worked. A step whose success cannot be read from
|
|
388
|
+
a named check is not a step the agent can own.
|
|
389
|
+
3. **What is forbidden, and why, with the citation.** This is the part that decays
|
|
390
|
+
silently, so it is the part to write down hardest. Cite the file and the line:
|
|
391
|
+
`never mutate the deployment directly or reimplement the release by hand
|
|
392
|
+
(repos/<repo>/AGENTS.md)`. A forbidden path with a source attached survives a
|
|
393
|
+
future session's improvisation; "be careful with releases" does not.
|
|
394
|
+
|
|
395
|
+
### Ask two things the repo cannot tell you
|
|
396
|
+
|
|
397
|
+
- **"What is a release worth cutting?"** The batching unit, in the repo's own
|
|
398
|
+
vocabulary: a sprint, an epic's children all closed, N merged issues waiting, N
|
|
399
|
+
days elapsed, or urgency. Without this the orchestrator either releases per merge
|
|
400
|
+
(a stream of meaningless versions burning shared runners) or never releases at
|
|
401
|
+
all.
|
|
402
|
+
- **"Who owns the rollback?"** If the answer is a person, that person owns the
|
|
403
|
+
release, and the boundary belongs before the irreversible step regardless of
|
|
404
|
+
what option 2 sounded like in the interview.
|
|
405
|
+
|
|
406
|
+
### And make it self-correcting
|
|
407
|
+
|
|
408
|
+
A release process is the section most likely to go stale: workflows get replaced,
|
|
409
|
+
and a brief describing the old one still reads plausible. So tell the operator
|
|
410
|
+
plainly, and make sure the brief's own **Learning loop** covers it: when the
|
|
411
|
+
release workflow changes, the brief contradicts repo reality, which is exactly an
|
|
412
|
+
amendment trigger. The session proposes the corrected steps and they approve with
|
|
413
|
+
a yes.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Step 5 — tailor the brief, and show the diff first
|
|
418
|
+
|
|
419
|
+
`src/briefs/orchestrator.md` in this package is the **floor**, not the deliverable.
|
|
420
|
+
It ships deliberately conservative so that an operator who never edits it still
|
|
421
|
+
has a safe fleet. Your job is to raise it to *this* fleet.
|
|
422
|
+
|
|
423
|
+
The file has a hard line in it — an HTML comment banner reading
|
|
424
|
+
`YOURS TO EDIT`. Respect it in both directions:
|
|
425
|
+
|
|
426
|
+
**Above the banner — leave it alone.** Duties (drain, groom, report), the
|
|
427
|
+
escalation-tier table, and the hard boundaries describe how the package already
|
|
428
|
+
behaves. Rewriting them makes the brief disagree with the code, and the code wins.
|
|
429
|
+
The evidence rule in particular is not negotiable: *every claim cites evidence — a
|
|
430
|
+
PR URL, an issue number, or a named check actually read.* "Should be fine",
|
|
431
|
+
"looks green" and "probably passing" are not evidence.
|
|
432
|
+
|
|
433
|
+
Know what is *not* up there, though, because operators expect it to be: the
|
|
434
|
+
orchestrator's own merge and release authority is **not** a hard boundary. It is
|
|
435
|
+
policy, it lives in Releases, and it defaults to none. What is fixed above the
|
|
436
|
+
banner is that a *worker* never merges or releases, and that PRs land one at a time
|
|
437
|
+
with a freshness re-check. Delegating a release to the orchestrator does not touch
|
|
438
|
+
either of those, so it needs no negotiation with the shipped half.
|
|
439
|
+
|
|
440
|
+
**Below the banner — rewrite from the interview.**
|
|
441
|
+
|
|
442
|
+
- **Releases.** Replace the section with the procedure you scaffolded in Step 4. If
|
|
443
|
+
humans-release, say so in one short paragraph and stop; do not leave the "replace
|
|
444
|
+
this paragraph if you are delegating" instructions in a finished brief, because
|
|
445
|
+
a standing prompt full of alternatives it did not choose is a standing prompt
|
|
446
|
+
the session has to guess its way through. If they chose a boundary, write the
|
|
447
|
+
boundary as a sentence with an end — *"your leg ends at the merged pin PR; you
|
|
448
|
+
never deploy it"* — then the scaffolded steps, then the forbidden list with its
|
|
449
|
+
citations. State the merge authority explicitly either way: a brief that
|
|
450
|
+
prescribes landing a release PR without ever saying the orchestrator may merge is
|
|
451
|
+
a brief the session has to infer permission from, and it will infer wrong in one
|
|
452
|
+
direction or the other.
|
|
453
|
+
- **Reporting.** Rewrite it as the one scope they chose, in the second person,
|
|
454
|
+
concretely. Delete the description of the scope they did not choose: it is
|
|
455
|
+
useful in a template and noise in a live prompt. Keep the closing constraint
|
|
456
|
+
verbatim in substance — no narration, no progress updates, no restating the
|
|
457
|
+
brief back. Evidence, or silence.
|
|
458
|
+
|
|
459
|
+
**Then add what the template cannot know: this fleet's own hard boundaries.** Ask
|
|
460
|
+
for them directly:
|
|
461
|
+
|
|
462
|
+
> **"Is there anywhere in these repos an agent must never go, even when an issue
|
|
463
|
+
> says to?"**
|
|
464
|
+
|
|
465
|
+
Typical answers, worth prompting for by name: infrastructure directories
|
|
466
|
+
(`terraform/`, `puppet/`, `ansible/`, `charts/`, `.github/workflows/`), anything
|
|
467
|
+
holding secrets or environment files, migration directories, a vendored or
|
|
468
|
+
generated tree, and **whole repos** that are in the org but off-limits. Write each
|
|
469
|
+
one as a path or repo name, not as a category — "no infra changes" is advice, and
|
|
470
|
+
`never edit terraform/ or .github/workflows/; escalate instead` is a boundary.
|
|
471
|
+
|
|
472
|
+
**Show the diff before writing anything.** Present the tailored sections against
|
|
473
|
+
the shipped template — the sections you rewrote, the boundaries you added, and an
|
|
474
|
+
explicit statement that everything above the banner is untouched. Get an
|
|
475
|
+
acknowledgement. This file becomes the standing prompt for a session that runs
|
|
476
|
+
unattended for weeks; the operator reading it once, now, is the cheapest review it
|
|
477
|
+
will ever get.
|
|
478
|
+
|
|
479
|
+
**Order of operations matters here.** The wizard writes the *template* (with
|
|
480
|
+
coordinates substituted) at the path it owns. So: draft and agree the tailored
|
|
481
|
+
sections now, let the wizard write the rendered floor in Step 7, and apply the
|
|
482
|
+
agreed edits to that file immediately afterwards. Do not pre-write the file to a
|
|
483
|
+
path you guessed, and do not skip the wizard's brief-writing step — you would
|
|
484
|
+
lose the substituted coordinates and the overwrite confirmation.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Step 6 — check the worker brief's assumptions against reality
|
|
489
|
+
|
|
490
|
+
The worker brief makes concrete claims to a session that has no other context. If
|
|
491
|
+
a claim is wrong, the worker cannot tell — it just fails in a confusing way. Check
|
|
492
|
+
each one against the actual repos, and report what you found.
|
|
493
|
+
|
|
494
|
+
1. **Default branches.** The brief says the worktree is cut from the repo's
|
|
495
|
+
default branch and the PR targets it. Confirm per repo with
|
|
496
|
+
`gh api repos/<owner>/<repo> --jq .default_branch`. A repo on `master`, `develop`
|
|
497
|
+
or `trunk` configured as `main` produces a run that fails at worktree creation.
|
|
498
|
+
2. **Branch naming.** The conductor cuts `<type>/<slug>`, where type is `fix` when
|
|
499
|
+
an issue label's last segment is `bug` and `feat` otherwise. Check that against
|
|
500
|
+
the repo's convention and its branch protection: a ruleset that only permits
|
|
501
|
+
`feature/*`, or requires a ticket prefix, will reject every push the fleet
|
|
502
|
+
makes. `gh api repos/<owner>/<repo>/rulesets` and the branch-protection settings
|
|
503
|
+
are the place to look.
|
|
504
|
+
3. **The gates actually run, and exit 0 on a clean checkout.** This is the one
|
|
505
|
+
worth spending real time on. In a clean mirror or worktree, run each proposed
|
|
506
|
+
gate read-only and record the exit code:
|
|
507
|
+
- Safe to run: lint, typecheck, unit tests, formatter `--check` modes.
|
|
508
|
+
- Do **not** run: anything that writes to the working tree (a formatter without
|
|
509
|
+
`--check`), anything that hits the network beyond a package install, docker
|
|
510
|
+
builds, deploys, or a suite that needs live services. If a gate cannot be
|
|
511
|
+
verified safely, say which one and why, rather than asserting it works.
|
|
512
|
+
- A gate that fails on a *clean* checkout is a fleet that can never push. That
|
|
513
|
+
is a finding to report before arming, not after — the operator either fixes
|
|
514
|
+
the repo or drops the gate, and both are their call.
|
|
515
|
+
4. **Does `pull_request` actually fire?** The whole loop ends with a worker
|
|
516
|
+
watching `gh pr checks --watch` to a verdict. A workflow triggered only on
|
|
517
|
+
`push` to a branch pattern, or gated behind `if: github.actor != ...`, or one
|
|
518
|
+
that requires approval for a first-time contributor, gives a PR with **no
|
|
519
|
+
checks** — and a worker that waits forever on a verdict that never comes. Read
|
|
520
|
+
the `on:` block of each workflow and confirm `pull_request` is there and not
|
|
521
|
+
path-filtered away from the paths this fleet will touch.
|
|
522
|
+
|
|
523
|
+
Report these as findings with evidence, not as reassurance. "I read
|
|
524
|
+
`.github/workflows/ci.yml`; `on: pull_request` is present with no path filter" is a
|
|
525
|
+
finding. "CI should trigger" is not.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Step 7 — finish through the wizard
|
|
530
|
+
|
|
531
|
+
Now hand the collected answers to the deterministic path:
|
|
532
|
+
|
|
533
|
+
```text
|
|
534
|
+
/conductor setup
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
You have the answers ready, so this is fast — and it stays the wizard's decision
|
|
538
|
+
to write, not yours. It asks, in this order: project name; tracker repo; queue
|
|
539
|
+
label; whether to rename the state labels; routing label prefix; then per repo the
|
|
540
|
+
routing key, clone URL, default branch and **pre-push gates** (your Step 2
|
|
541
|
+
proposal, in `cmd @ cwd` form); whether to add another repo; caps; the Telegram
|
|
542
|
+
chat id for tier 2; the escalation fallback; the report scope; and finally whether
|
|
543
|
+
to write `ORCHESTRATOR.md`.
|
|
544
|
+
|
|
545
|
+
Two things about the end of it that you must not smooth over:
|
|
546
|
+
|
|
547
|
+
- **The dry run is the point.** Before the confirm, it reads the tracker through
|
|
548
|
+
the same routing code the loop uses and prints exactly what the next tick would
|
|
549
|
+
pick up, which repo each issue routes to, the branch it would cut, and every
|
|
550
|
+
issue it cannot route. Walk the operator through that output. Unroutable issues
|
|
551
|
+
here are the single most useful signal in the whole onboarding: they mean the
|
|
552
|
+
labels and the routing config disagree, and it is far cheaper to see it now.
|
|
553
|
+
- **Nothing is mutated until they answer.** No label created, no config written,
|
|
554
|
+
no state database, no arm. If they decline, the machine is untouched. Never
|
|
555
|
+
answer that confirm on their behalf.
|
|
556
|
+
|
|
557
|
+
Say yes to writing `ORCHESTRATOR.md`, then **immediately apply the edits you
|
|
558
|
+
drafted in Steps 3, 4 and 5** to the file it wrote, and tell them the path. Note
|
|
559
|
+
the trap for later: a future `/conductor setup` re-run offers to overwrite that
|
|
560
|
+
file, and accepting loses every tailored word. Their brief is now a file worth
|
|
561
|
+
keeping a copy of.
|
|
562
|
+
|
|
563
|
+
### Then walk arm, pause, and disarm — they are three different things
|
|
564
|
+
|
|
565
|
+
Operators conflate these, and the failure modes are not the same.
|
|
566
|
+
|
|
567
|
+
- **Arm** is what that final confirm did, and it is exactly two things: create the
|
|
568
|
+
state database, and clear the pause flag. It does **not** start the daemon —
|
|
569
|
+
`omp-conductor start` does, and it does not report success until the daemon
|
|
570
|
+
answers `GET /healthz`. For a first run, take one tick in the foreground and
|
|
571
|
+
watch it: `omp-conductor daemon --once`.
|
|
572
|
+
- **Pause is maintenance.** `/conductor pause` (or `omp-conductor pause`) writes a
|
|
573
|
+
sentinel in the state directory. The dispatch loop checks it first, so no new
|
|
574
|
+
work is claimed from the next tick; runs already in flight finish rather than
|
|
575
|
+
being killed. The orchestrator heartbeat reads the *same* flag, so pausing the
|
|
576
|
+
fleet also silences its heartbeat — one flag, not two. This is the switch for
|
|
577
|
+
touching a repo, rotating a credential, a release window, or a holiday.
|
|
578
|
+
`resume` undoes it. Nothing is torn down, nothing is forgotten.
|
|
579
|
+
- **Disarm is channel teardown, and it is not a subcommand.** It is removing the
|
|
580
|
+
`armedFile` that `.conductor-tick.json` names — the heartbeat then sends nothing
|
|
581
|
+
and the supervising session simply stops being prompted. Be precise about the
|
|
582
|
+
asymmetry: **disarming stops the heartbeat, not the dispatch loop.** A disarmed
|
|
583
|
+
fleet whose daemon is still up keeps claiming issues with nobody supervising, so
|
|
584
|
+
"stop the fleet" is `pause` (or `omp-conductor stop`) — disarm is "stop waking
|
|
585
|
+
the orchestrator".
|
|
586
|
+
- **And the gate that disarms itself.** If `.conductor-tick.json` names an
|
|
587
|
+
`accessFile`, every tick re-reads the Telegram bridge's `access.json` and
|
|
588
|
+
requires `enabled: true` with exactly **one** paired owner. It fails closed on
|
|
589
|
+
everything else: missing, unreadable, not JSON, disabled, nobody paired, or
|
|
590
|
+
several paired (it refuses to guess which human is on the hook). Unattended
|
|
591
|
+
dispatch is only defensible while a tier-2 escalation can reach a person, so a
|
|
592
|
+
channel that goes away disarms the heartbeat whether or not anyone intended it.
|
|
593
|
+
**A fleet deploy always sets `accessFile`.** Leaving it unset passes the gate —
|
|
594
|
+
that is for ordinary dev sessions, not an off switch.
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Step 8 — hand over the learning loop
|
|
599
|
+
|
|
600
|
+
Finish by telling the operator the truth about what they just wrote:
|
|
601
|
+
|
|
602
|
+
**A brief converges from operation, not from an interview.** What you produced
|
|
603
|
+
today is a good first draft, and it is wrong in ways neither of you can see yet.
|
|
604
|
+
Real deployments diverge from the template exactly where the operator learns
|
|
605
|
+
something: a reporting scope that turned out too loud, a release boundary drawn in
|
|
606
|
+
the wrong place, an escalation that should have been a digest line, a hard
|
|
607
|
+
boundary nobody thought to name until an agent walked into it.
|
|
608
|
+
|
|
609
|
+
The brief closes that gap itself now. Point them at its **Learning loop** section
|
|
610
|
+
and say what it means in practice:
|
|
611
|
+
|
|
612
|
+
- **Their corrections are the trigger.** When they tell the orchestrator to work
|
|
613
|
+
differently — mid-flight, in a reply to an escalation, anywhere — it drafts the
|
|
614
|
+
matching edit to its own brief instead of just complying once.
|
|
615
|
+
- **Approval is a Telegram yes/no.** The proposal arrives as one question carrying
|
|
616
|
+
the exact diff: the current lines, then the replacement. Yes applies it. No, or
|
|
617
|
+
no answer at all, drops it, and it does not raise that amendment again.
|
|
618
|
+
- **Two things it will never do:** propose relaxing **Hard boundaries** (that
|
|
619
|
+
section changes only when they hand-edit it), or interrupt a tick's duties to
|
|
620
|
+
ask.
|
|
621
|
+
- **Every applied amendment is logged** as one line under **Amendments** at the
|
|
622
|
+
bottom of the brief — date, trigger, summary. That list is the honest record of
|
|
623
|
+
where this interview was wrong, and it accumulates without anyone scheduling it.
|
|
624
|
+
|
|
625
|
+
That is the whole handover. Convergence is now the loop's job rather than a habit
|
|
626
|
+
the operator has to keep.
|