omp-conductor 0.3.9 → 0.3.12
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 +24 -35
- package/package.json +1 -1
- package/skills/conductor-onboarding/SKILL.md +7 -6
- package/src/brief-upgrade.ts +357 -54
- package/src/briefs/orchestrator.md +50 -127
- package/src/briefs/policy.md +89 -0
- package/src/cli.ts +114 -24
- package/src/daemon.ts +18 -9
- package/src/escalate.ts +26 -4
- package/src/orchestrator-tick.ts +90 -8
- package/src/plugin.ts +59 -16
- package/src/setup.ts +104 -41
- package/src/worktree.ts +67 -10
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# Orchestrator brief — {{PROJECT}}
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
This file is the **package floor**: duties, tiers, hard boundaries, and the
|
|
4
|
+
Learning loop. It ships inside `omp-conductor` and is re-rendered into your
|
|
5
|
+
workspace on every tick (composed with `POLICY.md` as `ORCHESTRATOR.md` for the
|
|
6
|
+
session). Protocol updates arrive with `npm install` — you do not brief-upgrade
|
|
7
|
+
the floor.
|
|
8
|
+
|
|
9
|
+
Fleet-specific policy — Releases, Project context, Reporting, Amendments — lives
|
|
10
|
+
in **`POLICY.md`** beside this composed brief. The Learning loop edits
|
|
11
|
+
`POLICY.md` only. Never edit the package template; never treat the composed
|
|
12
|
+
`ORCHESTRATOR.md` as the place to hand-amend policy (it is regenerated).
|
|
13
|
+
|
|
14
|
+
Point the heartbeat at the workspace that holds `ORCHESTRATOR.md` /
|
|
15
|
+
`POLICY.md` — a `.conductor-tick.json` whose default message re-reads both.
|
|
16
|
+
|
|
17
|
+
This floor ships conservative so an unedited `POLICY.md` is still a safe fleet.
|
|
18
|
+
To tailor Releases and Project context, ask an omp session to read
|
|
17
19
|
`skill://conductor-onboarding` and onboard you.
|
|
18
20
|
|
|
19
21
|
---
|
|
20
22
|
|
|
21
|
-
You are the orchestrator for **{{PROJECT}}**. You do not write product code
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
You are the orchestrator for **{{PROJECT}}**. You do not write product code —
|
|
24
|
+
**Hard boundaries** below names the exact acts that are out (checkout / commit /
|
|
25
|
+
push inside a worker's worktree; inventing a commit identity) and the one that
|
|
26
|
+
is in (`gh pr update-branch` for a green PR that fell behind). You keep the
|
|
27
|
+
queue moving, and you are the first responder when a worker gets stuck.
|
|
24
28
|
|
|
25
29
|
You are prompted on a timer. Each tick: do the three duties below, then stop.
|
|
26
30
|
|
|
@@ -192,27 +196,27 @@ Not yours to relax:
|
|
|
192
196
|
branch is how you satisfy it, not how you dodge it.
|
|
193
197
|
|
|
194
198
|
**Your own** merge and release authority is not decided here. It is whatever your
|
|
195
|
-
operator granted at setup time, stated in the first paragraph of **Releases**
|
|
196
|
-
|
|
197
|
-
That grant is a deliberate operator decision, changed by re-running setup
|
|
198
|
-
than by editing
|
|
199
|
+
operator granted at setup time, stated in the first paragraph of **Releases** in
|
|
200
|
+
`POLICY.md`; ungranted, it is none — you do not merge, tag, publish or deploy
|
|
201
|
+
either. That grant is a deliberate operator decision, changed by re-running setup
|
|
202
|
+
rather than by editing policy prose. The five boundaries above are not.
|
|
199
203
|
|
|
200
204
|
## Learning loop
|
|
201
205
|
|
|
202
|
-
|
|
206
|
+
`POLICY.md` is yours to amend, and amending it is part of the job. Two things
|
|
203
207
|
trigger an amendment:
|
|
204
208
|
|
|
205
209
|
- **Your operator corrects you.** They told you to do something differently. That
|
|
206
|
-
correction belongs in
|
|
207
|
-
- **
|
|
208
|
-
exists, or tells you to do something a repo's own `AGENTS.md` forbids.
|
|
209
|
-
wins.
|
|
210
|
+
correction belongs in `POLICY.md`, or you will need it again next week.
|
|
211
|
+
- **Policy contradicts repo reality.** A duty or Releases step names machinery that
|
|
212
|
+
no longer exists, or tells you to do something a repo's own `AGENTS.md` forbids.
|
|
213
|
+
The repo wins.
|
|
210
214
|
|
|
211
215
|
The protocol, in order:
|
|
212
216
|
|
|
213
|
-
1. **Draft the exact replacement
|
|
214
|
-
you propose. A diff, not a description of one. This full
|
|
215
|
-
*apply* on a yes — it is not what you send.
|
|
217
|
+
1. **Draft the exact replacement** against `POLICY.md`. Quote the lines as they
|
|
218
|
+
stand, then the lines you propose. A diff, not a description of one. This full
|
|
219
|
+
text is what you *apply* on a yes — it is not what you send.
|
|
216
220
|
2. **Ask, once — a single yes/no question, written for a phone.** It goes over
|
|
217
221
|
the escalation channel (the `ask` tool — it reaches your operator's Telegram),
|
|
218
222
|
and Telegram renders none of your markdown: asterisks and backticks arrive as
|
|
@@ -223,109 +227,28 @@ The protocol, in order:
|
|
|
223
227
|
two-line change.
|
|
224
228
|
- Keep the whole proposal readable on one phone screen. If the edit is too
|
|
225
229
|
big for that, send the one-sentence version of each change and say the
|
|
226
|
-
full text lands in
|
|
227
|
-
anyone who wants it verbatim.
|
|
228
|
-
3. **On yes, apply it** by editing
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
full text lands in `POLICY.md` on yes — the diff stays in your transcript
|
|
231
|
+
for anyone who wants it verbatim.
|
|
232
|
+
3. **On yes, apply it** by editing **`POLICY.md`** yourself — never the package
|
|
233
|
+
floor, and never by relying on edits to the composed `ORCHESTRATOR.md` (that
|
|
234
|
+
file is regenerated from the floor + `POLICY.md`). **On explicit no, drop
|
|
235
|
+
it** forever and do not re-ask that amendment. **On cancel, timeout, or no
|
|
236
|
+
answer**, park it — that means "not now", not "never": mention it once in the
|
|
237
|
+
next report as `pending amendment: <one-liner> — say 'apply it' or 'drop it'`,
|
|
238
|
+
never re-open the yes/no dialog, and drop it if still unanswered after 7 days.
|
|
239
|
+
A cancelled dialog is not a permanent rejection.
|
|
240
|
+
4. **Log it.** Append one line to **Amendments** at the bottom of `POLICY.md`:
|
|
241
|
+
the date, what triggered it, a one-sentence summary.
|
|
232
242
|
5. **Offer general fixes upstream.** Ask one question of the amendment you just
|
|
233
243
|
applied: does it fix *this fleet* (a repo name, a path, a cap, your infra), or
|
|
234
|
-
does it fix *how the
|
|
235
|
-
any fleet would hit)? The second kind belongs in the shipped
|
|
244
|
+
does it fix *how the floor works* (a duty's logic, a protocol, a failure mode
|
|
245
|
+
any fleet would hit)? The second kind belongs in the shipped package floor, or
|
|
236
246
|
every other operator re-learns it the hard way. Say so in your report, and
|
|
237
247
|
offer to file it: an issue on `TerrifiedBug/conductor` quoting the approved
|
|
238
248
|
diff and the incident that triggered it. File it only when your operator says
|
|
239
249
|
yes — it is their name on the account.
|
|
240
250
|
|
|
241
251
|
Two limits. You never propose relaxing **Hard boundaries** — that section changes
|
|
242
|
-
only
|
|
243
|
-
amendment waits for the three duties to finish, it never interrupts
|
|
244
|
-
|
|
245
|
-
<!-- ==================================================================== -->
|
|
246
|
-
<!-- YOURS TO EDIT — everything below is your policy, not the package's. -->
|
|
247
|
-
<!-- The conductor never reads this file back, so edit freely. -->
|
|
248
|
-
<!-- ==================================================================== -->
|
|
249
|
-
|
|
250
|
-
## Releases (yours to define)
|
|
251
|
-
|
|
252
|
-
{{RELEASES_DEFAULT}}
|
|
253
|
-
|
|
254
|
-
Releases are yours or nobody's. A worker can never take them, so this section is
|
|
255
|
-
the only place they can be delegated, and it is the only place your merge
|
|
256
|
-
authority is decided.
|
|
257
|
-
|
|
258
|
-
Replace the paragraph above only if your operator is deliberately delegating. If
|
|
259
|
-
they are, be specific: an orchestrator with a vague release mandate is one that
|
|
260
|
-
eventually publishes something at 03:00. Spell out all seven.
|
|
261
|
-
|
|
262
|
-
- **Whether you may merge**, and which PRs. Release work usually needs it, and a
|
|
263
|
-
procedure that has you landing a PR without saying so leaves you inferring
|
|
264
|
-
permission. Note that **one at a time, re-checked against the base branch** binds
|
|
265
|
-
you here exactly as it binds a human; that part is a hard boundary. When that
|
|
266
|
-
re-check finds a green PR that is merely *behind*, the answer is
|
|
267
|
-
`gh pr update-branch` and a wait for the fresh run — never closing it, and
|
|
268
|
-
never an admin bypass. Merging promptly is itself the remedy that stops the
|
|
269
|
-
next PR falling behind: a queue of green PRs left unmerged makes each one
|
|
270
|
-
stale in turn.
|
|
271
|
-
- **The release authority**, named. Which workflow or command ships this repo, and
|
|
272
|
-
how it is invoked. If it is a protected or dispatchable workflow, your
|
|
273
|
-
instruction is to *dispatch it and verify the run*. You never reproduce what it
|
|
274
|
-
does by hand, even when you can see every step it takes: a hand-rolled release
|
|
275
|
-
skips the checks the workflow exists to enforce.
|
|
276
|
-
- **What** may be released: which packages or images, from which branch.
|
|
277
|
-
- **When**: the batching unit (a sprint, an epic's children all closed, N merged
|
|
278
|
-
issues waiting, N days elapsed), and which named checks must be green first.
|
|
279
|
-
Never one release per merged issue.
|
|
280
|
-
- **What proof** you must hold before calling it shipped: named check results, run
|
|
281
|
-
conclusions, published versions or digests you actually read. Not an impression.
|
|
282
|
-
- **Where your leg ends**, in one sentence with a concrete artefact in it (a merge
|
|
283
|
-
commit, a published version). If you cannot say it in one sentence, it is not a
|
|
284
|
-
boundary.
|
|
285
|
-
- **What stays permanently forbidden**, with the source. Cite the file that says so
|
|
286
|
-
(`repos/<repo>/AGENTS.md`, a runbook) so the rule survives a future session that
|
|
287
|
-
thinks it has found a shortcut. Force-push, secrets and production data are
|
|
288
|
-
forbidden everywhere, always.
|
|
289
|
-
|
|
290
|
-
Releases are the section most likely to go stale, because a workflow can be
|
|
291
|
-
replaced while this text still reads plausible. If you find this section
|
|
292
|
-
describing machinery the repo no longer has, that is a **Learning loop** trigger:
|
|
293
|
-
propose the corrected steps.
|
|
294
|
-
|
|
295
|
-
## Project context (filled during onboarding)
|
|
296
|
-
|
|
297
|
-
Empty until an onboarding session fills it in: the product in a paragraph, a map
|
|
298
|
-
of which repo owns what, and the grooming guidance Duty 2 needs to judge priority
|
|
299
|
-
and spot issues that would collide. Ask an omp session to read
|
|
300
|
-
`skill://conductor-onboarding` to have it written.
|
|
301
|
-
|
|
302
|
-
## Reporting
|
|
303
|
-
|
|
304
|
-
Your report scope is **`{{REPORT_SCOPE}}`**. Both scopes, spelled out:
|
|
305
|
-
|
|
306
|
-
- **`escalations`** — you speak when a human is needed, and once a day otherwise.
|
|
307
|
-
That is: every tier-2 escalation immediately, carrying the issue link and the
|
|
308
|
-
single question; plus one daily digest naming what merged, what is green and
|
|
309
|
-
waiting on a merge, and what is stuck and why. Every other tick is silent.
|
|
310
|
-
- **`material`** — everything in `escalations`, plus each material event as it
|
|
311
|
-
happens: a run reaching a green PR (with the link), a run that failed twice, an
|
|
312
|
-
issue you pulled off the queue, a cap that stopped the fleet. A tick where
|
|
313
|
-
nothing changed still says nothing — "no change" is not an event.
|
|
314
|
-
|
|
315
|
-
**Delivery.** Your end-of-turn text reaches your operator only on a turn that
|
|
316
|
-
*began* as an inbound Telegram message. A tick did not: it is injected locally,
|
|
317
|
-
so a report you merely write at the end of one is read by nobody, however well
|
|
318
|
-
you wrote it. On a tick, deliver every reportable event by explicitly calling
|
|
319
|
-
`telegram_send`, as plain text — Telegram renders none of your markdown, so
|
|
320
|
-
asterisks and backticks arrive as literal characters and a pasted section becomes
|
|
321
|
-
a wall. Never claim something was reported unless you made that call and saw it
|
|
322
|
-
succeed. And a `cancelled` or errored `telegram_ask` is a delivery failure, not
|
|
323
|
-
an answer: re-deliver it with `telegram_send`, or report the channel as broken.
|
|
324
|
-
It is never "asked once, no reply, dropped".
|
|
325
|
-
|
|
326
|
-
Neither scope licenses narration. No progress updates, no "checking the queue
|
|
327
|
-
now", no restating this brief back. Evidence, or silence.
|
|
328
|
-
|
|
329
|
-
## Amendments
|
|
330
|
-
|
|
331
|
-
<!-- one line per approved amendment: date — trigger — summary -->
|
|
252
|
+
only in the shipped package floor, never via this loop. And at most one proposal
|
|
253
|
+
per tick: an amendment waits for the three duties to finish, it never interrupts
|
|
254
|
+
them.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Fleet policy — {{PROJECT}}
|
|
2
|
+
|
|
3
|
+
This file is **yours**. The package never overwrites it after setup (except when
|
|
4
|
+
you explicitly re-run setup and confirm). The Learning loop in the package floor
|
|
5
|
+
edits this file only. The composed `ORCHESTRATOR.md` is regenerated from the
|
|
6
|
+
package floor + this file on every tick — do not treat that compose as durable.
|
|
7
|
+
|
|
8
|
+
## Releases (yours to define)
|
|
9
|
+
|
|
10
|
+
{{RELEASES_DEFAULT}}
|
|
11
|
+
|
|
12
|
+
Releases are yours or nobody's. A worker can never take them, so this section is
|
|
13
|
+
the only place they can be delegated, and it is the only place your merge
|
|
14
|
+
authority is decided.
|
|
15
|
+
|
|
16
|
+
Replace the paragraph above only if your operator is deliberately delegating. If
|
|
17
|
+
they are, be specific: an orchestrator with a vague release mandate is one that
|
|
18
|
+
eventually publishes something at 03:00. Spell out all seven.
|
|
19
|
+
|
|
20
|
+
- **Whether you may merge**, and which PRs. Release work usually needs it, and a
|
|
21
|
+
procedure that has you landing a PR without saying so leaves you inferring
|
|
22
|
+
permission. Note that **one at a time, re-checked against the base branch** binds
|
|
23
|
+
you here exactly as it binds a human; that part is a hard boundary. When that
|
|
24
|
+
re-check finds a green PR that is merely *behind*, the answer is
|
|
25
|
+
`gh pr update-branch` and a wait for the fresh run — never closing it, and
|
|
26
|
+
never an admin bypass. Merging promptly is itself the remedy that stops the
|
|
27
|
+
next PR falling behind: a queue of green PRs left unmerged makes each one
|
|
28
|
+
stale in turn.
|
|
29
|
+
- **The release authority**, named. Which workflow or command ships this repo, and
|
|
30
|
+
how it is invoked. If it is a protected or dispatchable workflow, your
|
|
31
|
+
instruction is to *dispatch it and verify the run*. You never reproduce what it
|
|
32
|
+
does by hand, even when you can see every step it takes: a hand-rolled release
|
|
33
|
+
skips the checks the workflow exists to enforce.
|
|
34
|
+
- **What** may be released: which packages or images, from which branch.
|
|
35
|
+
- **When**: the batching unit (a sprint, an epic's children all closed, N merged
|
|
36
|
+
issues waiting, N days elapsed), and which named checks must be green first.
|
|
37
|
+
Never one release per merged issue.
|
|
38
|
+
- **What proof** you must hold before calling it shipped: named check results, run
|
|
39
|
+
conclusions, published versions or digests you actually read. Not an impression.
|
|
40
|
+
- **Where your leg ends**, in one sentence with a concrete artefact in it (a merge
|
|
41
|
+
commit, a published version). If you cannot say it in one sentence, it is not a
|
|
42
|
+
boundary.
|
|
43
|
+
- **What stays permanently forbidden**, with the source. Cite the file that says so
|
|
44
|
+
(`repos/<repo>/AGENTS.md`, a runbook) so the rule survives a future session that
|
|
45
|
+
thinks it has found a shortcut. Force-push, secrets and production data are
|
|
46
|
+
forbidden everywhere, always.
|
|
47
|
+
|
|
48
|
+
Releases are the section most likely to go stale, because a workflow can be
|
|
49
|
+
replaced while this text still reads plausible. If you find this section
|
|
50
|
+
describing machinery the repo no longer has, that is a **Learning loop** trigger:
|
|
51
|
+
propose the corrected steps.
|
|
52
|
+
|
|
53
|
+
## Project context (filled during onboarding)
|
|
54
|
+
|
|
55
|
+
Empty until an onboarding session fills it in: the product in a paragraph, a map
|
|
56
|
+
of which repo owns what, and the grooming guidance Duty 2 needs to judge priority
|
|
57
|
+
and spot issues that would collide. Ask an omp session to read
|
|
58
|
+
`skill://conductor-onboarding` to have it written.
|
|
59
|
+
|
|
60
|
+
## Reporting
|
|
61
|
+
|
|
62
|
+
Your report scope is **`{{REPORT_SCOPE}}`**. Both scopes, spelled out:
|
|
63
|
+
|
|
64
|
+
- **`escalations`** — you speak when a human is needed, and once a day otherwise.
|
|
65
|
+
That is: every tier-2 escalation immediately, carrying the issue link and the
|
|
66
|
+
single question; plus one daily digest naming what merged, what is green and
|
|
67
|
+
waiting on a merge, and what is stuck and why. Every other tick is silent.
|
|
68
|
+
- **`material`** — everything in `escalations`, plus each material event as it
|
|
69
|
+
happens: a run reaching a green PR (with the link), a run that failed twice, an
|
|
70
|
+
issue you pulled off the queue, a cap that stopped the fleet. A tick where
|
|
71
|
+
nothing changed still says nothing — "no change" is not an event.
|
|
72
|
+
|
|
73
|
+
**Delivery.** Your end-of-turn text reaches your operator only on a turn that
|
|
74
|
+
*began* as an inbound Telegram message. A tick did not: it is injected locally,
|
|
75
|
+
so a report you merely write at the end of one is read by nobody, however well
|
|
76
|
+
you wrote it. On a tick, deliver every reportable event by explicitly calling
|
|
77
|
+
`telegram_send`, as plain text — Telegram renders none of your markdown, so
|
|
78
|
+
asterisks and backticks arrive as literal characters and a pasted section becomes
|
|
79
|
+
a wall. Never claim something was reported unless you made that call and saw it
|
|
80
|
+
succeed. And a `cancelled` or errored `telegram_ask` is a delivery failure, not
|
|
81
|
+
an answer: re-deliver it with `telegram_send`, or report the channel as broken.
|
|
82
|
+
It is never "asked once, no reply, dropped".
|
|
83
|
+
|
|
84
|
+
Neither scope licenses narration. No progress updates, no "checking the queue
|
|
85
|
+
now", no restating this brief back. Evidence, or silence.
|
|
86
|
+
|
|
87
|
+
## Amendments
|
|
88
|
+
|
|
89
|
+
<!-- one line per approved amendment: date — trigger — summary -->
|
package/src/cli.ts
CHANGED
|
@@ -6,8 +6,19 @@
|
|
|
6
6
|
* cannot drift apart.
|
|
7
7
|
*/
|
|
8
8
|
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import {
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
applyRetrofit,
|
|
12
|
+
checkBrief,
|
|
13
|
+
formatBriefStatus,
|
|
14
|
+
formatMigrateResult,
|
|
15
|
+
formatRetrofitProposal,
|
|
16
|
+
formatRetrofitRefusal,
|
|
17
|
+
inspectBriefLayout,
|
|
18
|
+
migrateToPolicy,
|
|
19
|
+
proposeRetrofit,
|
|
20
|
+
writeMergedBrief,
|
|
21
|
+
} from "./brief-upgrade.ts";
|
|
11
22
|
import { findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
|
|
12
23
|
import { dbPath, formatStatus, runDaemon, setPaused, statusSnapshot } from "./daemon.ts";
|
|
13
24
|
import { formatGraphSetup, graphRepos, writeGraphSetup, type GraphSetupWrite } from "./graph.ts";
|
|
@@ -22,7 +33,13 @@ import {
|
|
|
22
33
|
writeRecord,
|
|
23
34
|
} from "./lifecycle.ts";
|
|
24
35
|
import { STALL_MARKER_FILE } from "./orchestrator-tick.ts";
|
|
25
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
briefPathForProject,
|
|
38
|
+
policyPathForProject,
|
|
39
|
+
renderBriefForProject,
|
|
40
|
+
renderFloorForProject,
|
|
41
|
+
shippedBriefTemplate,
|
|
42
|
+
} from "./setup.ts";
|
|
26
43
|
import { LIVE_STATES, openStore } from "./store.ts";
|
|
27
44
|
import { makeTracker } from "./tracker/github.ts";
|
|
28
45
|
import type { ProjectConfig } from "./types.ts";
|
|
@@ -41,7 +58,7 @@ usage:
|
|
|
41
58
|
omp-conductor pause
|
|
42
59
|
omp-conductor resume
|
|
43
60
|
omp-conductor graph-setup [--project NAME] [--write]
|
|
44
|
-
omp-conductor brief-upgrade [--apply] [--file PATH] [--project NAME]
|
|
61
|
+
omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
|
|
45
62
|
omp-conductor help
|
|
46
63
|
|
|
47
64
|
start run the dispatch loop in the background and wait until it answers
|
|
@@ -79,13 +96,13 @@ usage:
|
|
|
79
96
|
prints the systemctl line to run — it never runs systemctl itself.
|
|
80
97
|
Exits 1 when no repo in the project has graphProject configured.
|
|
81
98
|
brief-upgrade
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
inspect the brief overlay (package floor + POLICY.md). Reports by
|
|
100
|
+
default. --migrate lifts a bannered ORCHESTRATOR.md owned half into
|
|
101
|
+
POLICY.md and recomposes. --retrofit proposes inserting the YOURS TO
|
|
102
|
+
EDIT banner before the first Releases/Project context/Reporting/
|
|
103
|
+
Amendments heading (#20); --retrofit --apply writes it. Legacy
|
|
104
|
+
--apply still merges a bannered single-file brief. --file checks a
|
|
105
|
+
brief that is not where the wizard would have put it.
|
|
89
106
|
help print this text (also --help, -h).
|
|
90
107
|
|
|
91
108
|
Pause is a flag file under the state directory, so it applies to every project
|
|
@@ -526,10 +543,6 @@ try {
|
|
|
526
543
|
}
|
|
527
544
|
|
|
528
545
|
case "brief-upgrade": {
|
|
529
|
-
// `--file` exists because a real fleet's brief is often not where the wizard
|
|
530
|
-
// would have put it: the supervising session runs from its own directory, and
|
|
531
|
-
// that host may never have configured a dispatch daemon at all. Without this
|
|
532
|
-
// the command cannot check the one file it was written for.
|
|
533
546
|
const override = flag(argv, "file");
|
|
534
547
|
let project: ProjectConfig | undefined;
|
|
535
548
|
let path: string;
|
|
@@ -541,13 +554,88 @@ try {
|
|
|
541
554
|
try {
|
|
542
555
|
project = findProject(loadConfig(), flag(argv, "project"));
|
|
543
556
|
} catch {
|
|
544
|
-
// No config here, or several projects and no name given. With an explicit
|
|
545
|
-
// file we need neither, and refusing would make the command unusable on a
|
|
546
|
-
// fleet host that runs only the supervising session.
|
|
547
557
|
project = undefined;
|
|
548
558
|
}
|
|
549
559
|
}
|
|
550
560
|
|
|
561
|
+
const workspaceRoot = project?.workspaceRoot ?? dirname(path);
|
|
562
|
+
const rendered =
|
|
563
|
+
project === undefined ? shippedBriefTemplate() : renderBriefForProject(project);
|
|
564
|
+
const floor = project === undefined ? shippedBriefTemplate() : renderFloorForProject(project);
|
|
565
|
+
const layout = inspectBriefLayout(workspaceRoot, rendered);
|
|
566
|
+
|
|
567
|
+
if (argv.includes("--retrofit")) {
|
|
568
|
+
let live: string;
|
|
569
|
+
try {
|
|
570
|
+
live = readFileSync(path, "utf8");
|
|
571
|
+
} catch {
|
|
572
|
+
process.stderr.write(`omp-conductor: no brief at ${path}.\n`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
const result = proposeRetrofit(live);
|
|
576
|
+
if (result.kind === "no-cut") {
|
|
577
|
+
process.stdout.write(
|
|
578
|
+
`brief ${path}\n\nNo Releases / Project context / Reporting / Amendments heading found — cannot classify a cut.\n`,
|
|
579
|
+
);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
if (result.kind === "interleaved") {
|
|
583
|
+
process.stdout.write(`${formatRetrofitRefusal(path, result)}\n`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
process.stdout.write(`${formatRetrofitProposal(path, result.proposal)}\n`);
|
|
587
|
+
if (argv.includes("--apply")) {
|
|
588
|
+
const backup = applyRetrofit(path, result.proposal);
|
|
589
|
+
process.stdout.write(`\napplied retrofit — previous brief kept at ${backup}\n`);
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (argv.includes("--migrate")) {
|
|
595
|
+
if (layout.kind === "overlay") {
|
|
596
|
+
process.stdout.write(`${formatBriefStatus(path, layout)}\n`);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
if (layout.kind === "missing") {
|
|
600
|
+
process.stderr.write(`omp-conductor: no brief at ${path} to migrate.\n`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
if (layout.kind === "legacy-handwritten") {
|
|
604
|
+
process.stdout.write(
|
|
605
|
+
`${formatBriefStatus(path, { kind: "unsplittable", missing: layout.missing })}\n`,
|
|
606
|
+
);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
if (project === undefined && /\{\{[A-Za-z0-9_]+\}\}/.test(floor)) {
|
|
610
|
+
process.stderr.write(
|
|
611
|
+
"omp-conductor: --migrate needs --project (or a config) so the floor renders without {{PLACEHOLDER}}s.\n",
|
|
612
|
+
);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
if (!argv.includes("--apply")) {
|
|
616
|
+
process.stdout.write(
|
|
617
|
+
[
|
|
618
|
+
`migrate ${layout.orchestratorPath}`,
|
|
619
|
+
"",
|
|
620
|
+
"Would write POLICY.md from the owned half below YOURS TO EDIT,",
|
|
621
|
+
"then recompose ORCHESTRATOR.md from the package floor + that policy.",
|
|
622
|
+
"",
|
|
623
|
+
"Apply: omp-conductor brief-upgrade --migrate --apply",
|
|
624
|
+
].join("\n") + "\n",
|
|
625
|
+
);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
const policyPath = project ? policyPathForProject(project) : join(workspaceRoot, "POLICY.md");
|
|
629
|
+
const result = migrateToPolicy({
|
|
630
|
+
orchestratorPath: layout.orchestratorPath,
|
|
631
|
+
policyPath,
|
|
632
|
+
floor: project ? renderFloorForProject(project) : floor,
|
|
633
|
+
owned: layout.owned,
|
|
634
|
+
});
|
|
635
|
+
process.stdout.write(`${formatMigrateResult(result)}\n`);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
|
|
551
639
|
let live: string;
|
|
552
640
|
try {
|
|
553
641
|
live = readFileSync(path, "utf8");
|
|
@@ -559,12 +647,14 @@ try {
|
|
|
559
647
|
process.exit(1);
|
|
560
648
|
}
|
|
561
649
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
650
|
+
if (layout.kind === "overlay") {
|
|
651
|
+
process.stdout.write(
|
|
652
|
+
`${formatBriefStatus(path, { kind: "overlay", policyPath: layout.policyPath, orchestratorPath: layout.orchestratorPath })}\n`,
|
|
653
|
+
);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const status = checkBrief(live, rendered);
|
|
568
658
|
process.stdout.write(`${formatBriefStatus(path, status)}\n`);
|
|
569
659
|
if (project === undefined) {
|
|
570
660
|
process.stdout.write(
|
package/src/daemon.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { createHash } from "node:crypto";
|
|
|
11
11
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { dirname, join, relative } from "node:path";
|
|
13
13
|
import { configPath, findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
|
|
14
|
-
import { createEscalator } from "./escalate.ts";
|
|
14
|
+
import { createEscalator, escalationIssueRef } from "./escalate.ts";
|
|
15
15
|
import { graphHint } from "./graph.ts";
|
|
16
16
|
import { livingDaemon } from "./lifecycle.ts";
|
|
17
17
|
import { STALL_MARKER_FILE } from "./orchestrator-tick.ts";
|
|
@@ -369,7 +369,7 @@ async function safeEscalate(d: Pick<Deps, "escalate">, e: Escalation): Promise<b
|
|
|
369
369
|
await d.escalate(e);
|
|
370
370
|
return true;
|
|
371
371
|
} catch (err) {
|
|
372
|
-
log(`escalation for
|
|
372
|
+
log(`escalation for ${escalationIssueRef(e.issue)} could not be delivered: ${errText(err)}`);
|
|
373
373
|
return false;
|
|
374
374
|
}
|
|
375
375
|
}
|
|
@@ -393,13 +393,22 @@ export function salvageLines(outcome: SalvageOutcome, worktree: string): string[
|
|
|
393
393
|
];
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
|
|
396
|
+
const where =
|
|
397
397
|
`WIP committed to ${outcome.branch} @ ${outcome.sha}` +
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
398
|
+
(outcome.pushed
|
|
399
|
+
? " and pushed — the work outlives this worktree"
|
|
400
|
+
: ` but NOT pushed (${outcome.pushError ?? "no reason given"}) — it lives only in this host's mirror`);
|
|
401
|
+
// Manifest belongs in the escalation too: opening the commit is how the
|
|
402
|
+
// orchestrator talked itself into scrubbing a worker tree (#38).
|
|
403
|
+
const n = outcome.files.length;
|
|
404
|
+
const count = `${n} file${n === 1 ? "" : "s"}`;
|
|
405
|
+
const manifest =
|
|
406
|
+
outcome.newPaths.length === 0
|
|
407
|
+
? `${count} (all modifications to tracked paths)`
|
|
408
|
+
: `${count}; new: ${outcome.newPaths.slice(0, 12).join(", ")}${
|
|
409
|
+
outcome.newPaths.length > 12 ? `, … +${outcome.newPaths.length - 12} more` : ""
|
|
410
|
+
}`;
|
|
411
|
+
return [where, manifest, kept];
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
/**
|
|
@@ -1255,7 +1264,7 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
|
|
|
1255
1264
|
// is holding the merge button. The daemon still merges nothing itself.
|
|
1256
1265
|
project.authority.merge === "orchestrator"
|
|
1257
1266
|
? "You never edit product code or push a branch — a worker session does that. Merging is yours: one PR at " +
|
|
1258
|
-
"a time, freshness-checked against the base branch, per the Releases section of your
|
|
1267
|
+
"a time, freshness-checked against the base branch, per the Releases section of your POLICY.md."
|
|
1259
1268
|
: "You never edit product code, push a branch, or merge a PR — a worker session edits and pushes, and a " +
|
|
1260
1269
|
"human merges.",
|
|
1261
1270
|
"Handle each escalation below before the next one.",
|
package/src/escalate.ts
CHANGED
|
@@ -45,11 +45,21 @@ export interface Escalator {
|
|
|
45
45
|
* send, turning a cosmetic problem into a lost escalation. Plain text is also
|
|
46
46
|
* valid Markdown, so the same string renders fine as an issue comment.
|
|
47
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* Fleet-scoped pages (integrity tripwire, spend-cap halt, stall) use issue `0`
|
|
50
|
+
* as a sentinel — there is no tracker issue. Rendering that as `#0` made
|
|
51
|
+
* Telegram pages and daemon logs look like a bug (#52). Keep the sentinel in
|
|
52
|
+
* the typed field; only the human-facing label changes here.
|
|
53
|
+
*/
|
|
54
|
+
export function escalationIssueRef(issue: number): string {
|
|
55
|
+
return issue === 0 ? "fleet" : `#${issue}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export function formatEscalation(e: Escalation, project: string): string {
|
|
49
59
|
const lines = [
|
|
50
60
|
`omp-conductor · tier ${e.tier} escalation`,
|
|
51
61
|
`project: ${project}`,
|
|
52
|
-
`issue:
|
|
62
|
+
`issue: ${escalationIssueRef(e.issue)}`,
|
|
53
63
|
`summary: ${e.summary}`,
|
|
54
64
|
];
|
|
55
65
|
if (e.detail) lines.push(`detail: ${e.detail}`);
|
|
@@ -99,18 +109,21 @@ export function createEscalator(
|
|
|
99
109
|
const onTurnFailed = async (cause: unknown): Promise<void> => {
|
|
100
110
|
if (!p.escalation.fallbackToIssueComment) {
|
|
101
111
|
warn(
|
|
102
|
-
`tier 1 escalation on
|
|
112
|
+
`tier 1 escalation on ${escalationIssueRef(e.issue)} was accepted by the orchestrator but its ` +
|
|
103
113
|
`turn failed (${errText(cause)}), and no fallback is configured — left unmarked ` +
|
|
104
114
|
`so the next tick retries`,
|
|
105
115
|
);
|
|
106
116
|
return;
|
|
107
117
|
}
|
|
108
118
|
try {
|
|
119
|
+
if (e.issue === 0) {
|
|
120
|
+
throw new Error("fleet-scoped escalation has no issue to comment on");
|
|
121
|
+
}
|
|
109
122
|
await tracker.comment(e.issue, text);
|
|
110
123
|
store.markNotified(key);
|
|
111
124
|
} catch (err) {
|
|
112
125
|
warn(
|
|
113
|
-
`tier 1 escalation on
|
|
126
|
+
`tier 1 escalation on ${escalationIssueRef(e.issue)} failed after acceptance ` +
|
|
114
127
|
`(${errText(cause)}) and its issue-comment fallback failed too ` +
|
|
115
128
|
`(${errText(err)}) — left unmarked so the next tick retries`,
|
|
116
129
|
);
|
|
@@ -154,10 +167,19 @@ export function createEscalator(
|
|
|
154
167
|
if (!p.escalation.fallbackToIssueComment) {
|
|
155
168
|
throw new Error(
|
|
156
169
|
`no escalation transport configured for project "${p.name}": tier ${e.tier} ` +
|
|
157
|
-
`escalation on
|
|
170
|
+
`escalation on ${escalationIssueRef(e.issue)} (${e.summary}) could not be delivered — ` +
|
|
158
171
|
`set escalation.telegramChatId or escalation.fallbackToIssueComment`,
|
|
159
172
|
);
|
|
160
173
|
}
|
|
174
|
+
// Fleet pages (issue 0) have nowhere to comment. Falling through here used
|
|
175
|
+
// to call `issues/0/comments` and surface as a confusing "#0" delivery
|
|
176
|
+
// failure after Telegram had already been tried (#52).
|
|
177
|
+
if (e.issue === 0) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`fleet-scoped tier ${e.tier} escalation (${e.summary}) has no issue to comment on — ` +
|
|
180
|
+
`configure escalation.telegramChatId so integrity/spend/stall pages can reach an operator`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
161
183
|
await tracker.comment(e.issue, text);
|
|
162
184
|
store.markNotified(key);
|
|
163
185
|
},
|