omp-conductor 0.3.11 → 0.3.13
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 +505 -60
- package/src/briefs/orchestrator.md +40 -123
- package/src/briefs/policy.md +89 -0
- package/src/cli.ts +140 -24
- package/src/daemon.ts +1 -1
- package/src/orchestrator-tick.ts +48 -6
- package/src/plugin.ts +77 -16
- package/src/setup.ts +104 -41
|
@@ -1,19 +1,21 @@
|
|
|
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
|
---
|
|
@@ -194,27 +196,27 @@ Not yours to relax:
|
|
|
194
196
|
branch is how you satisfy it, not how you dodge it.
|
|
195
197
|
|
|
196
198
|
**Your own** merge and release authority is not decided here. It is whatever your
|
|
197
|
-
operator granted at setup time, stated in the first paragraph of **Releases**
|
|
198
|
-
|
|
199
|
-
That grant is a deliberate operator decision, changed by re-running setup
|
|
200
|
-
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.
|
|
201
203
|
|
|
202
204
|
## Learning loop
|
|
203
205
|
|
|
204
|
-
|
|
206
|
+
`POLICY.md` is yours to amend, and amending it is part of the job. Two things
|
|
205
207
|
trigger an amendment:
|
|
206
208
|
|
|
207
209
|
- **Your operator corrects you.** They told you to do something differently. That
|
|
208
|
-
correction belongs in
|
|
209
|
-
- **
|
|
210
|
-
exists, or tells you to do something a repo's own `AGENTS.md` forbids.
|
|
211
|
-
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.
|
|
212
214
|
|
|
213
215
|
The protocol, in order:
|
|
214
216
|
|
|
215
|
-
1. **Draft the exact replacement
|
|
216
|
-
you propose. A diff, not a description of one. This full
|
|
217
|
-
*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.
|
|
218
220
|
2. **Ask, once — a single yes/no question, written for a phone.** It goes over
|
|
219
221
|
the escalation channel (the `ask` tool — it reaches your operator's Telegram),
|
|
220
222
|
and Telegram renders none of your markdown: asterisks and backticks arrive as
|
|
@@ -225,113 +227,28 @@ The protocol, in order:
|
|
|
225
227
|
two-line change.
|
|
226
228
|
- Keep the whole proposal readable on one phone screen. If the edit is too
|
|
227
229
|
big for that, send the one-sentence version of each change and say the
|
|
228
|
-
full text lands in
|
|
229
|
-
anyone who wants it verbatim.
|
|
230
|
-
3. **On yes, apply it** by editing
|
|
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
|
|
231
235
|
it** forever and do not re-ask that amendment. **On cancel, timeout, or no
|
|
232
236
|
answer**, park it — that means "not now", not "never": mention it once in the
|
|
233
237
|
next report as `pending amendment: <one-liner> — say 'apply it' or 'drop it'`,
|
|
234
238
|
never re-open the yes/no dialog, and drop it if still unanswered after 7 days.
|
|
235
239
|
A cancelled dialog is not a permanent rejection.
|
|
236
|
-
4. **Log it.** Append one line to **Amendments** at the bottom of
|
|
237
|
-
date, what triggered it, a one-sentence summary.
|
|
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.
|
|
238
242
|
5. **Offer general fixes upstream.** Ask one question of the amendment you just
|
|
239
243
|
applied: does it fix *this fleet* (a repo name, a path, a cap, your infra), or
|
|
240
|
-
does it fix *how the
|
|
241
|
-
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
|
|
242
246
|
every other operator re-learns it the hard way. Say so in your report, and
|
|
243
247
|
offer to file it: an issue on `TerrifiedBug/conductor` quoting the approved
|
|
244
248
|
diff and the incident that triggered it. File it only when your operator says
|
|
245
249
|
yes — it is their name on the account.
|
|
246
250
|
|
|
247
251
|
Two limits. You never propose relaxing **Hard boundaries** — that section changes
|
|
248
|
-
only
|
|
249
|
-
amendment waits for the three duties to finish, it never interrupts
|
|
250
|
-
|
|
251
|
-
<!-- ==================================================================== -->
|
|
252
|
-
<!-- YOURS TO EDIT — everything below is your policy, not the package's. -->
|
|
253
|
-
<!-- The conductor never reads this file back, so edit freely. -->
|
|
254
|
-
<!-- ==================================================================== -->
|
|
255
|
-
|
|
256
|
-
## Releases (yours to define)
|
|
257
|
-
|
|
258
|
-
{{RELEASES_DEFAULT}}
|
|
259
|
-
|
|
260
|
-
Releases are yours or nobody's. A worker can never take them, so this section is
|
|
261
|
-
the only place they can be delegated, and it is the only place your merge
|
|
262
|
-
authority is decided.
|
|
263
|
-
|
|
264
|
-
Replace the paragraph above only if your operator is deliberately delegating. If
|
|
265
|
-
they are, be specific: an orchestrator with a vague release mandate is one that
|
|
266
|
-
eventually publishes something at 03:00. Spell out all seven.
|
|
267
|
-
|
|
268
|
-
- **Whether you may merge**, and which PRs. Release work usually needs it, and a
|
|
269
|
-
procedure that has you landing a PR without saying so leaves you inferring
|
|
270
|
-
permission. Note that **one at a time, re-checked against the base branch** binds
|
|
271
|
-
you here exactly as it binds a human; that part is a hard boundary. When that
|
|
272
|
-
re-check finds a green PR that is merely *behind*, the answer is
|
|
273
|
-
`gh pr update-branch` and a wait for the fresh run — never closing it, and
|
|
274
|
-
never an admin bypass. Merging promptly is itself the remedy that stops the
|
|
275
|
-
next PR falling behind: a queue of green PRs left unmerged makes each one
|
|
276
|
-
stale in turn.
|
|
277
|
-
- **The release authority**, named. Which workflow or command ships this repo, and
|
|
278
|
-
how it is invoked. If it is a protected or dispatchable workflow, your
|
|
279
|
-
instruction is to *dispatch it and verify the run*. You never reproduce what it
|
|
280
|
-
does by hand, even when you can see every step it takes: a hand-rolled release
|
|
281
|
-
skips the checks the workflow exists to enforce.
|
|
282
|
-
- **What** may be released: which packages or images, from which branch.
|
|
283
|
-
- **When**: the batching unit (a sprint, an epic's children all closed, N merged
|
|
284
|
-
issues waiting, N days elapsed), and which named checks must be green first.
|
|
285
|
-
Never one release per merged issue.
|
|
286
|
-
- **What proof** you must hold before calling it shipped: named check results, run
|
|
287
|
-
conclusions, published versions or digests you actually read. Not an impression.
|
|
288
|
-
- **Where your leg ends**, in one sentence with a concrete artefact in it (a merge
|
|
289
|
-
commit, a published version). If you cannot say it in one sentence, it is not a
|
|
290
|
-
boundary.
|
|
291
|
-
- **What stays permanently forbidden**, with the source. Cite the file that says so
|
|
292
|
-
(`repos/<repo>/AGENTS.md`, a runbook) so the rule survives a future session that
|
|
293
|
-
thinks it has found a shortcut. Force-push, secrets and production data are
|
|
294
|
-
forbidden everywhere, always.
|
|
295
|
-
|
|
296
|
-
Releases are the section most likely to go stale, because a workflow can be
|
|
297
|
-
replaced while this text still reads plausible. If you find this section
|
|
298
|
-
describing machinery the repo no longer has, that is a **Learning loop** trigger:
|
|
299
|
-
propose the corrected steps.
|
|
300
|
-
|
|
301
|
-
## Project context (filled during onboarding)
|
|
302
|
-
|
|
303
|
-
Empty until an onboarding session fills it in: the product in a paragraph, a map
|
|
304
|
-
of which repo owns what, and the grooming guidance Duty 2 needs to judge priority
|
|
305
|
-
and spot issues that would collide. Ask an omp session to read
|
|
306
|
-
`skill://conductor-onboarding` to have it written.
|
|
307
|
-
|
|
308
|
-
## Reporting
|
|
309
|
-
|
|
310
|
-
Your report scope is **`{{REPORT_SCOPE}}`**. Both scopes, spelled out:
|
|
311
|
-
|
|
312
|
-
- **`escalations`** — you speak when a human is needed, and once a day otherwise.
|
|
313
|
-
That is: every tier-2 escalation immediately, carrying the issue link and the
|
|
314
|
-
single question; plus one daily digest naming what merged, what is green and
|
|
315
|
-
waiting on a merge, and what is stuck and why. Every other tick is silent.
|
|
316
|
-
- **`material`** — everything in `escalations`, plus each material event as it
|
|
317
|
-
happens: a run reaching a green PR (with the link), a run that failed twice, an
|
|
318
|
-
issue you pulled off the queue, a cap that stopped the fleet. A tick where
|
|
319
|
-
nothing changed still says nothing — "no change" is not an event.
|
|
320
|
-
|
|
321
|
-
**Delivery.** Your end-of-turn text reaches your operator only on a turn that
|
|
322
|
-
*began* as an inbound Telegram message. A tick did not: it is injected locally,
|
|
323
|
-
so a report you merely write at the end of one is read by nobody, however well
|
|
324
|
-
you wrote it. On a tick, deliver every reportable event by explicitly calling
|
|
325
|
-
`telegram_send`, as plain text — Telegram renders none of your markdown, so
|
|
326
|
-
asterisks and backticks arrive as literal characters and a pasted section becomes
|
|
327
|
-
a wall. Never claim something was reported unless you made that call and saw it
|
|
328
|
-
succeed. And a `cancelled` or errored `telegram_ask` is a delivery failure, not
|
|
329
|
-
an answer: re-deliver it with `telegram_send`, or report the channel as broken.
|
|
330
|
-
It is never "asked once, no reply, dropped".
|
|
331
|
-
|
|
332
|
-
Neither scope licenses narration. No progress updates, no "checking the queue
|
|
333
|
-
now", no restating this brief back. Evidence, or silence.
|
|
334
|
-
|
|
335
|
-
## Amendments
|
|
336
|
-
|
|
337
|
-
<!-- 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,20 @@
|
|
|
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
|
+
repairPolicyBannerCrumbs,
|
|
21
|
+
writeMergedBrief,
|
|
22
|
+
} from "./brief-upgrade.ts";
|
|
11
23
|
import { findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
|
|
12
24
|
import { dbPath, formatStatus, runDaemon, setPaused, statusSnapshot } from "./daemon.ts";
|
|
13
25
|
import { formatGraphSetup, graphRepos, writeGraphSetup, type GraphSetupWrite } from "./graph.ts";
|
|
@@ -22,7 +34,13 @@ import {
|
|
|
22
34
|
writeRecord,
|
|
23
35
|
} from "./lifecycle.ts";
|
|
24
36
|
import { STALL_MARKER_FILE } from "./orchestrator-tick.ts";
|
|
25
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
briefPathForProject,
|
|
39
|
+
policyPathForProject,
|
|
40
|
+
renderBriefForProject,
|
|
41
|
+
renderFloorForProject,
|
|
42
|
+
shippedBriefTemplate,
|
|
43
|
+
} from "./setup.ts";
|
|
26
44
|
import { LIVE_STATES, openStore } from "./store.ts";
|
|
27
45
|
import { makeTracker } from "./tracker/github.ts";
|
|
28
46
|
import type { ProjectConfig } from "./types.ts";
|
|
@@ -41,7 +59,7 @@ usage:
|
|
|
41
59
|
omp-conductor pause
|
|
42
60
|
omp-conductor resume
|
|
43
61
|
omp-conductor graph-setup [--project NAME] [--write]
|
|
44
|
-
omp-conductor brief-upgrade [--apply] [--file PATH] [--project NAME]
|
|
62
|
+
omp-conductor brief-upgrade [--migrate|--retrofit] [--apply] [--file PATH] [--project NAME]
|
|
45
63
|
omp-conductor help
|
|
46
64
|
|
|
47
65
|
start run the dispatch loop in the background and wait until it answers
|
|
@@ -79,13 +97,13 @@ usage:
|
|
|
79
97
|
prints the systemctl line to run — it never runs systemctl itself.
|
|
80
98
|
Exits 1 when no repo in the project has graphProject configured.
|
|
81
99
|
brief-upgrade
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
inspect the brief overlay (package floor + POLICY.md). Reports by
|
|
101
|
+
default. --migrate lifts a bannered ORCHESTRATOR.md owned half into
|
|
102
|
+
POLICY.md and recomposes. --retrofit proposes inserting the YOURS TO
|
|
103
|
+
EDIT banner before the first Releases/Project context/Reporting/
|
|
104
|
+
Amendments heading (#20); --retrofit --apply writes it. Legacy
|
|
105
|
+
--apply still merges a bannered single-file brief. --file checks a
|
|
106
|
+
brief that is not where the wizard would have put it.
|
|
89
107
|
help print this text (also --help, -h).
|
|
90
108
|
|
|
91
109
|
Pause is a flag file under the state directory, so it applies to every project
|
|
@@ -526,10 +544,6 @@ try {
|
|
|
526
544
|
}
|
|
527
545
|
|
|
528
546
|
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
547
|
const override = flag(argv, "file");
|
|
534
548
|
let project: ProjectConfig | undefined;
|
|
535
549
|
let path: string;
|
|
@@ -541,13 +555,113 @@ try {
|
|
|
541
555
|
try {
|
|
542
556
|
project = findProject(loadConfig(), flag(argv, "project"));
|
|
543
557
|
} 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
558
|
project = undefined;
|
|
548
559
|
}
|
|
549
560
|
}
|
|
550
561
|
|
|
562
|
+
const workspaceRoot = project?.workspaceRoot ?? dirname(path);
|
|
563
|
+
const rendered =
|
|
564
|
+
project === undefined ? shippedBriefTemplate() : renderBriefForProject(project);
|
|
565
|
+
const floor = project === undefined ? shippedBriefTemplate() : renderFloorForProject(project);
|
|
566
|
+
const layout = inspectBriefLayout(workspaceRoot, rendered);
|
|
567
|
+
|
|
568
|
+
if (argv.includes("--retrofit")) {
|
|
569
|
+
let live: string;
|
|
570
|
+
try {
|
|
571
|
+
live = readFileSync(path, "utf8");
|
|
572
|
+
} catch {
|
|
573
|
+
process.stderr.write(`omp-conductor: no brief at ${path}.\n`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
const result = proposeRetrofit(live);
|
|
577
|
+
if (result.kind === "no-cut") {
|
|
578
|
+
process.stdout.write(
|
|
579
|
+
`brief ${path}\n\nNo Releases / Project context / Reporting / Amendments heading found — cannot classify a cut.\n`,
|
|
580
|
+
);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
if (result.kind === "interleaved") {
|
|
584
|
+
process.stdout.write(`${formatRetrofitRefusal(path, result)}\n`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
process.stdout.write(`${formatRetrofitProposal(path, result.proposal)}\n`);
|
|
588
|
+
if (argv.includes("--apply")) {
|
|
589
|
+
const backup = applyRetrofit(path, result.proposal);
|
|
590
|
+
process.stdout.write(`\napplied retrofit — previous brief kept at ${backup}\n`);
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (argv.includes("--migrate")) {
|
|
596
|
+
if (layout.kind === "overlay") {
|
|
597
|
+
if (!argv.includes("--apply")) {
|
|
598
|
+
process.stdout.write(
|
|
599
|
+
`${formatBriefStatus(path, layout)}\n\n` +
|
|
600
|
+
"POLICY.md already present. --migrate --apply will strip any leading\n" +
|
|
601
|
+
"banner-comment crumbs from POLICY.md and recompose ORCHESTRATOR.md.\n",
|
|
602
|
+
);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
if (project === undefined) {
|
|
606
|
+
process.stderr.write(
|
|
607
|
+
"omp-conductor: repairing an overlay needs --project (or a config) so the floor renders.\n",
|
|
608
|
+
);
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
611
|
+
const repaired = repairPolicyBannerCrumbs({
|
|
612
|
+
orchestratorPath: layout.orchestratorPath,
|
|
613
|
+
policyPath: layout.policyPath,
|
|
614
|
+
floor: renderFloorForProject(project),
|
|
615
|
+
});
|
|
616
|
+
if (repaired === undefined) {
|
|
617
|
+
process.stdout.write(
|
|
618
|
+
`${formatBriefStatus(path, layout)}\n\nrecomposed ORCHESTRATOR.md — POLICY.md needed no crumb strip.\n`,
|
|
619
|
+
);
|
|
620
|
+
} else {
|
|
621
|
+
process.stdout.write(`${formatMigrateResult(repaired)}\n`);
|
|
622
|
+
}
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
if (layout.kind === "missing") {
|
|
626
|
+
process.stderr.write(`omp-conductor: no brief at ${path} to migrate.\n`);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
if (layout.kind === "legacy-handwritten") {
|
|
630
|
+
process.stdout.write(
|
|
631
|
+
`${formatBriefStatus(path, { kind: "unsplittable", missing: layout.missing })}\n`,
|
|
632
|
+
);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
if (project === undefined && /\{\{[A-Za-z0-9_]+\}\}/.test(floor)) {
|
|
636
|
+
process.stderr.write(
|
|
637
|
+
"omp-conductor: --migrate needs --project (or a config) so the floor renders without {{PLACEHOLDER}}s.\n",
|
|
638
|
+
);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
if (!argv.includes("--apply")) {
|
|
642
|
+
process.stdout.write(
|
|
643
|
+
[
|
|
644
|
+
`migrate ${layout.orchestratorPath}`,
|
|
645
|
+
"",
|
|
646
|
+
"Would write POLICY.md from the owned half below YOURS TO EDIT,",
|
|
647
|
+
"then recompose ORCHESTRATOR.md from the package floor + that policy.",
|
|
648
|
+
"",
|
|
649
|
+
"Apply: omp-conductor brief-upgrade --migrate --apply",
|
|
650
|
+
].join("\n") + "\n",
|
|
651
|
+
);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
const policyPath = project ? policyPathForProject(project) : join(workspaceRoot, "POLICY.md");
|
|
655
|
+
const result = migrateToPolicy({
|
|
656
|
+
orchestratorPath: layout.orchestratorPath,
|
|
657
|
+
policyPath,
|
|
658
|
+
floor: project ? renderFloorForProject(project) : floor,
|
|
659
|
+
owned: layout.owned,
|
|
660
|
+
});
|
|
661
|
+
process.stdout.write(`${formatMigrateResult(result)}\n`);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
|
|
551
665
|
let live: string;
|
|
552
666
|
try {
|
|
553
667
|
live = readFileSync(path, "utf8");
|
|
@@ -559,12 +673,14 @@ try {
|
|
|
559
673
|
process.exit(1);
|
|
560
674
|
}
|
|
561
675
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
676
|
+
if (layout.kind === "overlay") {
|
|
677
|
+
process.stdout.write(
|
|
678
|
+
`${formatBriefStatus(path, { kind: "overlay", policyPath: layout.policyPath, orchestratorPath: layout.orchestratorPath })}\n`,
|
|
679
|
+
);
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const status = checkBrief(live, rendered);
|
|
568
684
|
process.stdout.write(`${formatBriefStatus(path, status)}\n`);
|
|
569
685
|
if (project === undefined) {
|
|
570
686
|
process.stdout.write(
|
package/src/daemon.ts
CHANGED
|
@@ -1264,7 +1264,7 @@ export async function runDaemon(o: DaemonOpts = {}): Promise<void> {
|
|
|
1264
1264
|
// is holding the merge button. The daemon still merges nothing itself.
|
|
1265
1265
|
project.authority.merge === "orchestrator"
|
|
1266
1266
|
? "You never edit product code or push a branch — a worker session does that. Merging is yours: one PR at " +
|
|
1267
|
-
"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."
|
|
1268
1268
|
: "You never edit product code, push a branch, or merge a PR — a worker session edits and pushes, and a " +
|
|
1269
1269
|
"human merges.",
|
|
1270
1270
|
"Handle each escalation below before the next one.",
|
package/src/orchestrator-tick.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Self-tick for the fleet orchestrator session.
|
|
3
3
|
*
|
|
4
|
-
* The orchestrator is a 24/7 omp session with a standing brief (ORCHESTRATOR.md)
|
|
4
|
+
* The orchestrator is a 24/7 omp session with a standing brief (composed ORCHESTRATOR.md + POLICY.md)
|
|
5
5
|
* and no user typing into it. A session that is never prompted never runs its
|
|
6
6
|
* loop, so this extension is the heartbeat: every `intervalSeconds` it injects
|
|
7
7
|
* one message that starts a turn.
|
|
@@ -44,6 +44,11 @@ import { spawnSync } from "node:child_process";
|
|
|
44
44
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
45
45
|
import { isAbsolute, join, resolve } from "node:path";
|
|
46
46
|
import { findProject, loadConfig } from "./config.ts";
|
|
47
|
+
import {
|
|
48
|
+
briefPathForProject,
|
|
49
|
+
policyPathForProject,
|
|
50
|
+
refreshComposedBriefForProject,
|
|
51
|
+
} from "./setup.ts";
|
|
47
52
|
import { DEFAULT_REPORT_SCOPE, type ReportScope } from "./types.ts";
|
|
48
53
|
|
|
49
54
|
/** The activation file. Absent means "this is not an orchestrator session". */
|
|
@@ -237,8 +242,16 @@ export type TickConfigResult =
|
|
|
237
242
|
* A second spelling of it here would contradict the first inside one prompt the
|
|
238
243
|
* moment a fleet chose `escalations`.
|
|
239
244
|
*/
|
|
240
|
-
export function defaultTickMessage(
|
|
241
|
-
|
|
245
|
+
export function defaultTickMessage(
|
|
246
|
+
now: Date,
|
|
247
|
+
briefPath = "ORCHESTRATOR.md",
|
|
248
|
+
policyPath = "POLICY.md",
|
|
249
|
+
): string {
|
|
250
|
+
return (
|
|
251
|
+
`Tick ${now.toISOString()}: re-read ${briefPath} (composed package floor + policy) and ` +
|
|
252
|
+
`${policyPath} (editable fleet policy) from disk, then run your standing loop from them. ` +
|
|
253
|
+
`Learning-loop amendments edit only ${policyPath} — never the package floor.`
|
|
254
|
+
);
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
/**
|
|
@@ -296,18 +309,43 @@ export const TICK_DELIVERY_RULE =
|
|
|
296
309
|
* `status`. Stopping the heartbeat over either preference would be the worse
|
|
297
310
|
* trade.
|
|
298
311
|
*/
|
|
299
|
-
export function resolveTickScope(): {
|
|
312
|
+
export function resolveTickScope(): {
|
|
313
|
+
scope: ReportScope;
|
|
314
|
+
briefPath?: string;
|
|
315
|
+
policyPath?: string;
|
|
316
|
+
projectName?: string;
|
|
317
|
+
fallback?: string;
|
|
318
|
+
} {
|
|
300
319
|
try {
|
|
301
320
|
const project = findProject(loadConfig());
|
|
302
321
|
return {
|
|
303
322
|
scope: project.reporting?.scope ?? DEFAULT_REPORT_SCOPE,
|
|
304
|
-
briefPath:
|
|
323
|
+
briefPath: briefPathForProject(project),
|
|
324
|
+
policyPath: policyPathForProject(project),
|
|
325
|
+
projectName: project.name,
|
|
305
326
|
};
|
|
306
327
|
} catch (err) {
|
|
307
328
|
return { scope: DEFAULT_REPORT_SCOPE, fallback: err instanceof Error ? err.message : String(err) };
|
|
308
329
|
}
|
|
309
330
|
}
|
|
310
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Best-effort recompose of `ORCHESTRATOR.md` from the installed package floor +
|
|
334
|
+
* live `POLICY.md`.
|
|
335
|
+
*
|
|
336
|
+
* Runs on **every** successful send — including ticks that use a custom
|
|
337
|
+
* `message` — so protocol updates land after `npm install` without waiting for
|
|
338
|
+
* the default prompt path. Failures (no config, no `POLICY.md`, unreadable
|
|
339
|
+
* overlay) are silent: the tick still goes out.
|
|
340
|
+
*/
|
|
341
|
+
export function refreshComposedBriefBestEffort(): boolean {
|
|
342
|
+
try {
|
|
343
|
+
return refreshComposedBriefForProject(findProject(loadConfig()));
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
311
349
|
/**
|
|
312
350
|
* An optional file-path field. Relative paths resolve against the session cwd
|
|
313
351
|
* so `state/armed` means what it looks like; a present-but-unusable value is a
|
|
@@ -903,6 +941,10 @@ function tick(pi: TickApi, ctx: TickContext, config: TickConfig, session: TickSe
|
|
|
903
941
|
return;
|
|
904
942
|
}
|
|
905
943
|
|
|
944
|
+
// Floor refresh is independent of which prompt we send: a custom message still
|
|
945
|
+
// expects ORCHESTRATOR.md / AGENTS.md to track the installed package.
|
|
946
|
+
refreshComposedBriefBestEffort();
|
|
947
|
+
|
|
906
948
|
// A configured message owns the whole contract, reporting and delivery clauses
|
|
907
949
|
// included: an operator who wrote their own prompt did not ask for ours
|
|
908
950
|
// appended to it.
|
|
@@ -913,7 +955,7 @@ function tick(pi: TickApi, ctx: TickContext, config: TickConfig, session: TickSe
|
|
|
913
955
|
session.scopeFallbackLogged = true;
|
|
914
956
|
pi.logger.info(`[omp-conductor] tick reporting scope: using ${DEFAULT_REPORT_SCOPE} — ${scope.fallback}`);
|
|
915
957
|
}
|
|
916
|
-
content = `${defaultTickMessage(new Date(), scope.briefPath)}\n${TICK_SCOPE_CONSTRAINTS[scope.scope]}\n${TICK_DELIVERY_RULE}`;
|
|
958
|
+
content = `${defaultTickMessage(new Date(), scope.briefPath, scope.policyPath)}\n${TICK_SCOPE_CONSTRAINTS[scope.scope]}\n${TICK_DELIVERY_RULE}`;
|
|
917
959
|
}
|
|
918
960
|
|
|
919
961
|
pi.sendMessage(
|