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.
@@ -1,19 +1,21 @@
1
1
  # Orchestrator brief — {{PROJECT}}
2
2
 
3
- `/conductor setup` renders this template with your project's real values and
4
- writes it into your workspace. From that moment it is **yours**: the conductor
5
- never reads it back, never rewrites it, and never enforces a word of it. It is
6
- the standing prompt for the one long-lived omp session that supervises the fleet.
7
- The *session* is the exception to "never rewrites it" — you may amend this file
8
- yourself, with your operator's approval, per **Learning loop** below.
9
-
10
- Point the heartbeat at it — a `.conductor-tick.json` in that session's working
11
- directory, whose `message` tells the session to run its loop from this file.
12
-
13
- This is the **floor**, not the finished article: it ships conservative so an
14
- unedited brief is still a safe fleet. To have it tailored to your project —
15
- interviewed release boundary, your own hard boundaries, the reporting scope
16
- written out as the one you chose — ask an omp session to read
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
- below; ungranted, it is none — you do not merge, tag, publish or deploy either.
199
- That grant is a deliberate operator decision, changed by re-running setup rather
200
- than by editing this file. The five boundaries above are not.
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
- This file is yours to amend, and amending it is part of the job. Two things
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 this file, or you will need it again next week.
209
- - **This brief contradicts repo reality.** A duty names a step that no longer
210
- exists, or tells you to do something a repo's own `AGENTS.md` forbids. The repo
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.** Quote the lines as they stand, then the lines
216
- you propose. A diff, not a description of one. This full text is what you
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 the file on yes — the diff stays in your transcript for
229
- anyone who wants it verbatim.
230
- 3. **On yes, apply it** by editing this file yourself. **On explicit no, drop
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 this file: the
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 brief works* (a duty's logic, a protocol, a failure mode
241
- any fleet would hit)? The second kind belongs in the shipped template, or
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 when your operator hand-edits it. And at most one proposal per tick: an
249
- amendment waits for the three duties to finish, it never interrupts them.
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 { checkBrief, formatBriefStatus, writeMergedBrief } from "./brief-upgrade.ts";
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 { briefPathForProject, renderBriefForProject, shippedBriefTemplate } from "./setup.ts";
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
- compare a project's ORCHESTRATOR.md against the brief this version of
83
- the package ships. Reports by default; --apply replaces the half above
84
- the YOURS TO EDIT banner and keeps everything below it, backing the old
85
- file up first. --file checks a brief that is not where the wizard would
86
- have put it, on a host that may have no config at all. Nothing is
87
- written for a brief with no banner, or when no config resolved and the
88
- template still carries its {{PLACEHOLDER}} coordinates.
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
- // With no config there are no coordinates to substitute, so the template is
563
- // compared raw. Headings carry no placeholders, so the section-level report is
564
- // unaffected; the note below keeps the printed text honest.
565
- const status = checkBrief(live, project === undefined ? shippedBriefTemplate() : renderBriefForProject(project));
566
- // Report first, always: --apply on a brief with no banner must not be the
567
- // command that silently discards an operator's hand-written policy.
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 ORCHESTRATOR.md."
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.",
@@ -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(now: Date, briefPath = "ORCHESTRATOR.md"): string {
241
- return `Tick ${now.toISOString()}: re-read ${briefPath} from disk, then run your standing loop from it.`;
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(): { scope: ReportScope; briefPath?: string; fallback?: string } {
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: join(project.workspaceRoot, "ORCHESTRATOR.md"),
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(