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
package/src/plugin.ts
CHANGED
|
@@ -11,8 +11,16 @@
|
|
|
11
11
|
* worth protecting is on `setup()` below — nothing is written before the confirm.
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
-
import { isAbsolute } from "node:path";
|
|
15
|
-
import {
|
|
14
|
+
import { dirname, isAbsolute } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
checkBrief,
|
|
17
|
+
formatBriefStatus,
|
|
18
|
+
formatMigrateResult,
|
|
19
|
+
inspectBriefLayout,
|
|
20
|
+
migrateToPolicy,
|
|
21
|
+
repairPolicyBannerCrumbs,
|
|
22
|
+
writeMergedBrief,
|
|
23
|
+
} from "./brief-upgrade.ts";
|
|
16
24
|
import { configPath, expandHome, findProject, loadConfig, saveConfig } from "./config.ts";
|
|
17
25
|
import {
|
|
18
26
|
armConductor,
|
|
@@ -27,11 +35,14 @@ import { defaultGraphRoot } from "./graph.ts";
|
|
|
27
35
|
import {
|
|
28
36
|
AMEND_AREAS,
|
|
29
37
|
ORCHESTRATOR_BRIEF_NAME,
|
|
38
|
+
POLICY_BRIEF_NAME,
|
|
30
39
|
REPORT_SCOPE_CHOICES,
|
|
31
40
|
SETUP_DEFAULTS,
|
|
32
41
|
amendChoices,
|
|
33
42
|
answersFromProject,
|
|
34
43
|
briefPathForProject,
|
|
44
|
+
policyPathForProject,
|
|
45
|
+
renderFloorForProject,
|
|
35
46
|
buildConfig,
|
|
36
47
|
checkTokenScopes,
|
|
37
48
|
createMissingLabels,
|
|
@@ -374,17 +385,17 @@ async function askGraphRoot(
|
|
|
374
385
|
async function askOrchestratorBrief(ctx: CommandContext, a: SetupAnswers): Promise<boolean> {
|
|
375
386
|
const path = orchestratorBriefPath(a);
|
|
376
387
|
const wanted = await ctx.ui.confirm(
|
|
377
|
-
`Write
|
|
378
|
-
`
|
|
379
|
-
`
|
|
380
|
-
`The conductor
|
|
388
|
+
`Write ${ORCHESTRATOR_BRIEF_NAME} + ${POLICY_BRIEF_NAME} under ${dirname(path)}?`,
|
|
389
|
+
`Writes composed ${ORCHESTRATOR_BRIEF_NAME} (package floor, refreshed each tick) and ${POLICY_BRIEF_NAME} ` +
|
|
390
|
+
`(Releases, Project context, Reporting, Amendments — yours to edit via the Learning loop). ` +
|
|
391
|
+
`The conductor stops at green PRs either way.`,
|
|
381
392
|
);
|
|
382
393
|
if (!wanted) return false;
|
|
383
394
|
if (!existsSync(path)) return true;
|
|
384
395
|
|
|
385
396
|
return await ctx.ui.confirm(
|
|
386
|
-
`Overwrite
|
|
387
|
-
`${path} already exists. Overwriting replaces
|
|
397
|
+
`Overwrite existing ${ORCHESTRATOR_BRIEF_NAME} / ${POLICY_BRIEF_NAME}?`,
|
|
398
|
+
`${path} already exists. Overwriting replaces the composed brief and POLICY.md scaffold — any policy you wrote is lost.`,
|
|
388
399
|
);
|
|
389
400
|
}
|
|
390
401
|
|
|
@@ -885,7 +896,7 @@ async function setup(ctx: CommandContext, projectArg: string | undefined): Promi
|
|
|
885
896
|
`Wrote ${path} and armed the conductor.`,
|
|
886
897
|
briefPath === undefined
|
|
887
898
|
? "No orchestrator brief written — the conductor stops at green PRs; merges and releases stay human."
|
|
888
|
-
: `Wrote ${briefPath} — edit
|
|
899
|
+
: `Wrote ${briefPath} + POLICY.md — edit POLICY.md (Releases/Reporting); floor refreshes each tick.`,
|
|
889
900
|
"",
|
|
890
901
|
"Dry run against the config just written:",
|
|
891
902
|
...(await tryPreview(answers.projectName)),
|
|
@@ -929,18 +940,68 @@ export default function conductorPlugin(pi: PluginApi): void {
|
|
|
929
940
|
case "brief-upgrade": {
|
|
930
941
|
const p = findProject(loadConfig(), project);
|
|
931
942
|
const path = briefPathForProject(p);
|
|
932
|
-
|
|
943
|
+
const rendered = renderBriefForProject(p);
|
|
944
|
+
const layout = inspectBriefLayout(p.workspaceRoot, rendered);
|
|
945
|
+
if (layout.kind === "missing") {
|
|
946
|
+
ctx.ui.notify(
|
|
947
|
+
`No brief at ${path} — run /conductor setup and say yes to writing ${ORCHESTRATOR_BRIEF_NAME} + ${POLICY_BRIEF_NAME}.`,
|
|
948
|
+
"warning",
|
|
949
|
+
);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
if (layout.kind === "overlay") {
|
|
953
|
+
ctx.ui.notify(
|
|
954
|
+
formatBriefStatus(path, {
|
|
955
|
+
kind: "overlay",
|
|
956
|
+
policyPath: layout.policyPath,
|
|
957
|
+
orchestratorPath: layout.orchestratorPath,
|
|
958
|
+
}),
|
|
959
|
+
"info",
|
|
960
|
+
);
|
|
961
|
+
const repair = await ctx.ui.confirm(
|
|
962
|
+
"Repair POLICY.md banner crumbs and recompose?",
|
|
963
|
+
"Strips any leading HTML-comment leftovers from a pre-fix migrate, then recomposes ORCHESTRATOR.md from the package floor + POLICY.md.",
|
|
964
|
+
);
|
|
965
|
+
if (repair) {
|
|
966
|
+
const repaired = repairPolicyBannerCrumbs({
|
|
967
|
+
orchestratorPath: layout.orchestratorPath,
|
|
968
|
+
policyPath: layout.policyPath,
|
|
969
|
+
floor: renderFloorForProject(p),
|
|
970
|
+
});
|
|
971
|
+
ctx.ui.notify(
|
|
972
|
+
repaired === undefined
|
|
973
|
+
? "Recomposed ORCHESTRATOR.md — POLICY.md needed no crumb strip."
|
|
974
|
+
: formatMigrateResult(repaired),
|
|
975
|
+
"info",
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
if (layout.kind === "legacy-bannered") {
|
|
933
981
|
ctx.ui.notify(
|
|
934
|
-
|
|
982
|
+
[
|
|
983
|
+
`Legacy bannered brief at ${layout.orchestratorPath}.`,
|
|
984
|
+
"Migrate the owned half into POLICY.md so the package floor refreshes each tick.",
|
|
985
|
+
].join("\n"),
|
|
935
986
|
"warning",
|
|
936
987
|
);
|
|
988
|
+
const migrate = await ctx.ui.confirm(
|
|
989
|
+
"Migrate to POLICY.md overlay?",
|
|
990
|
+
"Writes POLICY.md from everything below YOURS TO EDIT, recomposes ORCHESTRATOR.md from the package floor + that policy, and keeps backups.",
|
|
991
|
+
);
|
|
992
|
+
if (migrate) {
|
|
993
|
+
const result = migrateToPolicy({
|
|
994
|
+
orchestratorPath: layout.orchestratorPath,
|
|
995
|
+
policyPath: policyPathForProject(p),
|
|
996
|
+
floor: renderFloorForProject(p),
|
|
997
|
+
owned: layout.owned,
|
|
998
|
+
});
|
|
999
|
+
ctx.ui.notify(formatMigrateResult(result), "info");
|
|
1000
|
+
}
|
|
937
1001
|
break;
|
|
938
1002
|
}
|
|
939
|
-
const status = checkBrief(readFileSync(path, "utf8"),
|
|
940
|
-
ctx.ui.notify(formatBriefStatus(path, status),
|
|
941
|
-
// Confirmed here rather than applied on sight: this file is a standing
|
|
942
|
-
// prompt the operator may have spent an hour on, so the diff they just
|
|
943
|
-
// read is the thing they are agreeing to.
|
|
1003
|
+
const status = checkBrief(readFileSync(path, "utf8"), rendered);
|
|
1004
|
+
ctx.ui.notify(formatBriefStatus(path, status), "warning");
|
|
944
1005
|
if (status.kind === "mergeable") {
|
|
945
1006
|
const apply = await ctx.ui.confirm(
|
|
946
1007
|
"Upgrade the brief?",
|
package/src/setup.ts
CHANGED
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import { dirname, join } from "node:path";
|
|
26
|
+
import {
|
|
27
|
+
COMPOSE_BANNER,
|
|
28
|
+
ORCHESTRATOR_BRIEF_NAME,
|
|
29
|
+
POLICY_BRIEF_NAME,
|
|
30
|
+
composeOrchestrator,
|
|
31
|
+
policyPathForRoot,
|
|
32
|
+
renderBriefTemplate,
|
|
33
|
+
writeWithBackup,
|
|
34
|
+
} from "./brief-upgrade.ts";
|
|
26
35
|
import { configPath, resolveCaps, stateDir } from "./config.ts";
|
|
27
36
|
import { graphProjectPath, graphRepos } from "./graph.ts";
|
|
28
37
|
import {
|
|
@@ -37,7 +46,6 @@ import {
|
|
|
37
46
|
type ReportScope,
|
|
38
47
|
type RepoTarget,
|
|
39
48
|
} from "./types.ts";
|
|
40
|
-
import { renderBrief } from "./worker.ts";
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
51
|
* Every decision the wizard needs, in one plain object. Collected by the UI,
|
|
@@ -197,12 +205,14 @@ export const MERGE_DUTY: { readonly [K in ProjectConfig["authority"]["merge"]]:
|
|
|
197
205
|
" one is a hard boundary, not a preference.",
|
|
198
206
|
};
|
|
199
207
|
|
|
200
|
-
|
|
201
|
-
export const ORCHESTRATOR_BRIEF_NAME = "ORCHESTRATOR.md";
|
|
208
|
+
export { ORCHESTRATOR_BRIEF_NAME, POLICY_BRIEF_NAME };
|
|
202
209
|
|
|
203
|
-
/** Shipped
|
|
210
|
+
/** Shipped floor template — duties, tiers, hard boundaries, Learning loop. */
|
|
204
211
|
const ORCHESTRATOR_TEMPLATE_PATH = join(import.meta.dir, "briefs", "orchestrator.md");
|
|
205
212
|
|
|
213
|
+
/** Shipped POLICY.md scaffold — Releases, Project context, Reporting, Amendments. */
|
|
214
|
+
const POLICY_TEMPLATE_PATH = join(import.meta.dir, "briefs", "policy.md");
|
|
215
|
+
|
|
206
216
|
/**
|
|
207
217
|
* `repo` writes labels and closes issues; `project` moves cards on the board.
|
|
208
218
|
* Both are load-bearing for an unattended loop, so a missing one is reported
|
|
@@ -572,46 +582,69 @@ export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
|
572
582
|
}
|
|
573
583
|
|
|
574
584
|
/**
|
|
575
|
-
* Where a configured project's brief lives: beside its worktrees, under
|
|
576
|
-
* directory, so it is on the same disk the fleet already owns and
|
|
577
|
-
* reinstall of the package. Derived from the project rather than
|
|
578
|
-
* project that ever gains a chosen workspace root keeps its brief
|
|
585
|
+
* Where a configured project's composed brief lives: beside its worktrees, under
|
|
586
|
+
* the state directory, so it is on the same disk the fleet already owns and
|
|
587
|
+
* survives a reinstall of the package. Derived from the project rather than
|
|
588
|
+
* fixed, so a project that ever gains a chosen workspace root keeps its brief
|
|
589
|
+
* with it.
|
|
579
590
|
*/
|
|
580
591
|
export function briefPathForProject(p: ProjectConfig): string {
|
|
581
592
|
return join(p.workspaceRoot, ORCHESTRATOR_BRIEF_NAME);
|
|
582
593
|
}
|
|
583
594
|
|
|
584
|
-
/**
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
* Exported for the upgrade check, which has to be able to read the shipped text
|
|
588
|
-
* on a host that has no config to render it against.
|
|
589
|
-
*/
|
|
590
|
-
export function shippedBriefTemplate(): string {
|
|
591
|
-
return readFileSync(ORCHESTRATOR_TEMPLATE_PATH, "utf8");
|
|
595
|
+
/** Where the fleet-owned POLICY.md overlay lives for a project. */
|
|
596
|
+
export function policyPathForProject(p: ProjectConfig): string {
|
|
597
|
+
return policyPathForRoot(p.workspaceRoot);
|
|
592
598
|
}
|
|
593
599
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
*
|
|
597
|
-
* Only the coordinates, the chosen scope and the authority paragraph are
|
|
598
|
-
* substituted: the rest of the policy text is left exactly as shipped, because
|
|
599
|
-
* from here on the file is the operator's to edit and nothing in this package
|
|
600
|
-
* reads it back.
|
|
601
|
-
*
|
|
602
|
-
* Takes a `ProjectConfig` rather than answers so that a *later* upgrade check can
|
|
603
|
-
* reproduce the same render from what is on disk, months after the wizard's
|
|
604
|
-
* answers are gone.
|
|
605
|
-
*/
|
|
606
|
-
export function renderBriefForProject(p: ProjectConfig): string {
|
|
607
|
-
return renderBrief(readFileSync(ORCHESTRATOR_TEMPLATE_PATH, "utf8"), {
|
|
600
|
+
function briefVarsForProject(p: ProjectConfig): Record<string, string> {
|
|
601
|
+
return {
|
|
608
602
|
PROJECT: p.name,
|
|
609
603
|
TRACKER_REPO: p.tracker.repo,
|
|
610
604
|
QUEUE_LABEL: p.queueLabel,
|
|
611
605
|
RELEASES_DEFAULT: RELEASES_DEFAULTS[`${p.authority.merge}/${p.authority.release}`],
|
|
612
606
|
MERGE_DUTY: MERGE_DUTY[p.authority.merge],
|
|
613
607
|
REPORT_SCOPE: p.reporting?.scope ?? DEFAULT_REPORT_SCOPE,
|
|
614
|
-
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Package floor template, placeholders and all. */
|
|
612
|
+
export function shippedFloorTemplate(): string {
|
|
613
|
+
return readFileSync(ORCHESTRATOR_TEMPLATE_PATH, "utf8");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** POLICY.md scaffold template, placeholders and all. */
|
|
617
|
+
export function shippedPolicyTemplate(): string {
|
|
618
|
+
return readFileSync(POLICY_TEMPLATE_PATH, "utf8");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Floor + policy templates concatenated with the compose banner, placeholders
|
|
623
|
+
* intact. Used when no project config is available to render coordinates.
|
|
624
|
+
*/
|
|
625
|
+
export function shippedBriefTemplate(): string {
|
|
626
|
+
return composeOrchestrator(shippedFloorTemplate(), shippedPolicyTemplate());
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Rendered package floor for a configured project. */
|
|
630
|
+
export function renderFloorForProject(p: ProjectConfig): string {
|
|
631
|
+
return renderBriefTemplate(shippedFloorTemplate(), briefVarsForProject(p));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Rendered POLICY.md scaffold for a configured project. */
|
|
635
|
+
export function renderPolicyForProject(p: ProjectConfig): string {
|
|
636
|
+
return renderBriefTemplate(shippedPolicyTemplate(), briefVarsForProject(p));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Composed session brief: rendered floor + POLICY scaffold (or a caller's policy).
|
|
641
|
+
*
|
|
642
|
+
* Setup writes the policy half to `POLICY.md` and this compose to
|
|
643
|
+
* `ORCHESTRATOR.md`. Later ticks recompose from the live POLICY.md so package
|
|
644
|
+
* floor updates apply without brief-upgrade.
|
|
645
|
+
*/
|
|
646
|
+
export function renderBriefForProject(p: ProjectConfig, policyText?: string): string {
|
|
647
|
+
return composeOrchestrator(renderFloorForProject(p), policyText ?? renderPolicyForProject(p));
|
|
615
648
|
}
|
|
616
649
|
|
|
617
650
|
/** Wizard-time path, via the project the answers describe. */
|
|
@@ -625,7 +658,7 @@ export function renderOrchestratorBrief(a: SetupAnswers): string {
|
|
|
625
658
|
}
|
|
626
659
|
|
|
627
660
|
/**
|
|
628
|
-
* Writes
|
|
661
|
+
* Writes `POLICY.md` + composed `ORCHESTRATOR.md`, and returns the composed path.
|
|
629
662
|
*
|
|
630
663
|
* Unconditional by design: the "do not clobber my edits" decision belongs to the
|
|
631
664
|
* operator, is asked in the wizard, and arrives here as
|
|
@@ -634,10 +667,39 @@ export function renderOrchestratorBrief(a: SetupAnswers): string {
|
|
|
634
667
|
* must get an overwrite.
|
|
635
668
|
*/
|
|
636
669
|
export function writeOrchestratorBrief(a: SetupAnswers): string {
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
670
|
+
const project = buildProject(a);
|
|
671
|
+
const policyPath = policyPathForProject(project);
|
|
672
|
+
const orchestratorPath = briefPathForProject(project);
|
|
673
|
+
mkdirSync(dirname(orchestratorPath), { recursive: true });
|
|
674
|
+
const policy = renderPolicyForProject(project);
|
|
675
|
+
writeFileSync(policyPath, policy);
|
|
676
|
+
writeFileSync(orchestratorPath, composeOrchestrator(renderFloorForProject(project), policy));
|
|
677
|
+
return orchestratorPath;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Recompose `ORCHESTRATOR.md` from the package floor + live `POLICY.md`.
|
|
682
|
+
*
|
|
683
|
+
* Returns false when POLICY.md is missing (caller should migrate or set up).
|
|
684
|
+
*/
|
|
685
|
+
export function refreshComposedBriefForProject(p: ProjectConfig): boolean {
|
|
686
|
+
const policyPath = policyPathForProject(p);
|
|
687
|
+
if (!existsSync(policyPath)) return false;
|
|
688
|
+
const orchestratorPath = briefPathForProject(p);
|
|
689
|
+
mkdirSync(dirname(orchestratorPath), { recursive: true });
|
|
690
|
+
writeFileSync(
|
|
691
|
+
orchestratorPath,
|
|
692
|
+
composeOrchestrator(renderFloorForProject(p), readFileSync(policyPath, "utf8")),
|
|
693
|
+
);
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** @internal test helper — expose compose banner for assertions. */
|
|
698
|
+
export const BRIEF_COMPOSE_BANNER = COMPOSE_BANNER;
|
|
699
|
+
|
|
700
|
+
/** Backup-aware POLICY write used by migrate paths that already computed text. */
|
|
701
|
+
export function writePolicyFile(path: string, content: string): string | undefined {
|
|
702
|
+
return writeWithBackup(path, content);
|
|
641
703
|
}
|
|
642
704
|
|
|
643
705
|
/**
|
|
@@ -861,11 +923,12 @@ export function summarisePlan(
|
|
|
861
923
|
` scope ${a.reportScope} — ${chosen?.description ?? "unknown scope"}`,
|
|
862
924
|
);
|
|
863
925
|
if (a.writeOrchestratorBrief) {
|
|
926
|
+
const policyPath = briefPath.replace(/ORCHESTRATOR\.md$/, "POLICY.md");
|
|
864
927
|
lines.push(
|
|
865
|
-
existsSync(briefPath)
|
|
866
|
-
? ` brief would OVERWRITE ${briefPath}`
|
|
867
|
-
: ` brief would write ${briefPath}`,
|
|
868
|
-
"
|
|
928
|
+
existsSync(briefPath) || existsSync(policyPath)
|
|
929
|
+
? ` brief would OVERWRITE ${briefPath} + POLICY.md`
|
|
930
|
+
: ` brief would write ${briefPath} + POLICY.md`,
|
|
931
|
+
" POLICY.md is yours — Releases/Reporting/Amendments; floor recomposes each tick",
|
|
869
932
|
);
|
|
870
933
|
} else {
|
|
871
934
|
lines.push(
|
|
@@ -1002,7 +1065,7 @@ export const AMEND_AREAS: {
|
|
|
1002
1065
|
},
|
|
1003
1066
|
brief: {
|
|
1004
1067
|
name: "orchestrator brief",
|
|
1005
|
-
asks: `whether to
|
|
1068
|
+
asks: `whether to write ${ORCHESTRATOR_BRIEF_NAME} + ${POLICY_BRIEF_NAME} — the one area that writes no config key`,
|
|
1006
1069
|
describe: (p) => {
|
|
1007
1070
|
const path = briefPathForProject(p);
|
|
1008
1071
|
return existsSync(path) ? `written at ${path}` : `none at ${path}`;
|