omp-conductor 0.3.12 → 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/package.json +1 -1
- package/src/brief-upgrade.ts +149 -7
- package/src/cli.ts +27 -1
- package/src/plugin.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
package/src/brief-upgrade.ts
CHANGED
|
@@ -40,29 +40,138 @@ export const COMPOSE_BANNER = [
|
|
|
40
40
|
/**
|
|
41
41
|
* A brief split into the package's half and the operator's half.
|
|
42
42
|
*
|
|
43
|
-
* `shipped` runs
|
|
44
|
-
*
|
|
45
|
-
* merge safe to
|
|
43
|
+
* `shipped` runs through the full contiguous HTML-comment banner that contains
|
|
44
|
+
* {@link EDIT_BANNER}; `owned` is everything after that block. Concatenating
|
|
45
|
+
* them reproduces the input byte for byte, which is what makes a merge safe to
|
|
46
|
+
* write back.
|
|
46
47
|
*/
|
|
47
48
|
export interface BriefHalves {
|
|
48
49
|
shipped: string;
|
|
49
50
|
owned: string;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** True when a line is a single HTML comment (the banner's only vocabulary). */
|
|
54
|
+
export function isHtmlCommentLine(line: string): boolean {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
return trimmed.startsWith("<!--") && trimmed.endsWith("-->");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Substrings that identify **package** banner chrome phrases, never operator
|
|
61
|
+
* notes and never a bare decorative separator.
|
|
62
|
+
*
|
|
63
|
+
* Matched case-insensitively inside an HTML comment. Keep this list tight: a
|
|
64
|
+
* false positive would delete fleet-owned POLICY prose.
|
|
65
|
+
*/
|
|
66
|
+
const BANNER_CHROME_MARKERS = [
|
|
67
|
+
EDIT_BANNER,
|
|
68
|
+
"never reads this file back",
|
|
69
|
+
"live copy of POLICY.md",
|
|
70
|
+
"regenerated from package floor",
|
|
71
|
+
"hand-edits above or below",
|
|
72
|
+
] as const;
|
|
73
|
+
|
|
74
|
+
/** `<!-- ====...==== -->` / dash separators that *frame* the banner. */
|
|
75
|
+
const BANNER_SEPARATOR = /^<!--\s*[=-]{3,}\s*-->$/;
|
|
76
|
+
|
|
77
|
+
/** True when a line is a decorative `<!-- === -->` / `<!-- --- -->` separator. */
|
|
78
|
+
export function isBannerSeparatorLine(line: string): boolean {
|
|
79
|
+
return BANNER_SEPARATOR.test(line.trim());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* True when a line is a package banner **phrase** comment (YOURS TO EDIT /
|
|
84
|
+
* never-reads footer / compose-banner wording) — not a bare separator.
|
|
85
|
+
*/
|
|
86
|
+
export function isPhraseChromeLine(line: string): boolean {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!isHtmlCommentLine(trimmed)) return false;
|
|
89
|
+
if (isBannerSeparatorLine(trimmed)) return false;
|
|
90
|
+
const lower = trimmed.toLowerCase();
|
|
91
|
+
return BANNER_CHROME_MARKERS.some((marker) => lower.includes(marker.toLowerCase()));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @deprecated Prefer {@link isPhraseChromeLine}. Separators alone are not chrome.
|
|
96
|
+
*/
|
|
97
|
+
export function isKnownBannerChromeLine(line: string): boolean {
|
|
98
|
+
return isPhraseChromeLine(line);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Drop a leading **known package footer fragment** from an owned half / POLICY.md.
|
|
103
|
+
*
|
|
104
|
+
* Only strips when the leading blank+comment prefix contains at least one
|
|
105
|
+
* package phrase marker (e.g. "never reads this file back"). Separators like
|
|
106
|
+
* `<!-- ==== -->` strip only as companions to that phrase — a POLICY that
|
|
107
|
+
* legitimately starts with a decorative separator alone is left untouched.
|
|
108
|
+
* Stops at the first non-blank line that is neither a phrase nor a separator,
|
|
109
|
+
* so `<!-- operator notes -->` survive.
|
|
110
|
+
*/
|
|
111
|
+
export function stripLeadingBannerCrumbs(text: string): string {
|
|
112
|
+
const lines = text.split("\n");
|
|
113
|
+
|
|
114
|
+
// Peek: does the leading blank/comment prefix carry a package phrase?
|
|
115
|
+
let j = 0;
|
|
116
|
+
let phraseInPrefix = false;
|
|
117
|
+
while (j < lines.length) {
|
|
118
|
+
const line = lines[j];
|
|
119
|
+
if (line === undefined) break;
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
if (trimmed === "") {
|
|
122
|
+
j += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!isHtmlCommentLine(line)) break;
|
|
126
|
+
if (isPhraseChromeLine(line)) phraseInPrefix = true;
|
|
127
|
+
j += 1;
|
|
128
|
+
}
|
|
129
|
+
// Separator-only (or operator-comment-only) prefixes are fleet-owned — keep.
|
|
130
|
+
if (!phraseInPrefix) return text;
|
|
131
|
+
|
|
132
|
+
// Strip blanks, phrase chrome, and separators that framed that phrase.
|
|
133
|
+
let i = 0;
|
|
134
|
+
while (i < lines.length) {
|
|
135
|
+
const line = lines[i];
|
|
136
|
+
if (line === undefined) break;
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
if (trimmed === "" || isPhraseChromeLine(line) || isBannerSeparatorLine(line)) {
|
|
139
|
+
i += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
return lines.slice(i).join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @deprecated Use {@link stripLeadingBannerCrumbs}. Kept for call-site stability. */
|
|
148
|
+
export const stripLeadingHtmlComments = stripLeadingBannerCrumbs;
|
|
149
|
+
|
|
52
150
|
/**
|
|
53
151
|
* Splits on the banner, or returns `undefined` when there is none.
|
|
54
152
|
*
|
|
55
153
|
* `undefined` is a real answer, not a failure: it means this brief cannot be
|
|
56
154
|
* merged mechanically, and every caller is expected to degrade to reporting
|
|
57
155
|
* rather than to assume a boundary.
|
|
156
|
+
*
|
|
157
|
+
* The cut is the end of the **whole contiguous HTML-comment block** that
|
|
158
|
+
* contains `YOURS TO EDIT`, not merely the line with that substring. Legacy and
|
|
159
|
+
* compose banners both trail the marker with more `<!-- … -->` lines; leaving
|
|
160
|
+
* those in `owned` would write package chrome into POLICY.md.
|
|
58
161
|
*/
|
|
59
162
|
export function splitBrief(text: string): BriefHalves | undefined {
|
|
60
163
|
const at = text.indexOf(EDIT_BANNER);
|
|
61
164
|
if (at < 0) return undefined;
|
|
62
|
-
//
|
|
63
|
-
// at the first line they own, so a merge never has to reconstruct the banner.
|
|
165
|
+
// End of the line that carries YOURS TO EDIT…
|
|
64
166
|
const lineEnd = text.indexOf("\n", at);
|
|
65
|
-
|
|
167
|
+
let cut = lineEnd < 0 ? text.length : lineEnd + 1;
|
|
168
|
+
// …then every contiguous HTML-comment line after it (footer / closer).
|
|
169
|
+
while (cut < text.length) {
|
|
170
|
+
const nextNl = text.indexOf("\n", cut);
|
|
171
|
+
const nextLine = nextNl < 0 ? text.slice(cut) : text.slice(cut, nextNl);
|
|
172
|
+
if (!isHtmlCommentLine(nextLine)) break;
|
|
173
|
+
cut = nextNl < 0 ? text.length : nextNl + 1;
|
|
174
|
+
}
|
|
66
175
|
return { shipped: text.slice(0, cut), owned: text.slice(cut) };
|
|
67
176
|
}
|
|
68
177
|
|
|
@@ -296,7 +405,9 @@ export function migrateToPolicy(opts: {
|
|
|
296
405
|
if (owned === undefined) {
|
|
297
406
|
throw new Error(`cannot migrate ${opts.orchestratorPath}: no ${EDIT_BANNER} banner`);
|
|
298
407
|
}
|
|
299
|
-
|
|
408
|
+
// Strip banner footers that an older split may have left in owned — never let
|
|
409
|
+
// package chrome become fleet policy.
|
|
410
|
+
const policyBody = stripLeadingBannerCrumbs(owned).replace(/^\s+/, "");
|
|
300
411
|
const policyBackup = writeWithBackup(opts.policyPath, policyBody.endsWith("\n") ? policyBody : `${policyBody}\n`);
|
|
301
412
|
const composed = composeOrchestrator(opts.floor, readFileSync(opts.policyPath, "utf8"));
|
|
302
413
|
const orchestratorBackup = writeWithBackup(opts.orchestratorPath, composed);
|
|
@@ -309,6 +420,37 @@ export function migrateToPolicy(opts: {
|
|
|
309
420
|
};
|
|
310
421
|
}
|
|
311
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Strip leading banner-comment crumbs from an existing POLICY.md and recompose.
|
|
425
|
+
*
|
|
426
|
+
* For fleets that already migrated under the line-only split: `--migrate --apply`
|
|
427
|
+
* on an active overlay repairs POLICY.md in place rather than no-opping.
|
|
428
|
+
*/
|
|
429
|
+
export function repairPolicyBannerCrumbs(opts: {
|
|
430
|
+
orchestratorPath: string;
|
|
431
|
+
policyPath: string;
|
|
432
|
+
floor: string;
|
|
433
|
+
}): MigrateResult | undefined {
|
|
434
|
+
if (!existsSync(opts.policyPath)) return undefined;
|
|
435
|
+
const before = readFileSync(opts.policyPath, "utf8");
|
|
436
|
+
const cleaned = stripLeadingBannerCrumbs(before);
|
|
437
|
+
if (cleaned === before) {
|
|
438
|
+
// Still recompose so the floor matches this package even when POLICY was clean.
|
|
439
|
+
writeFileSync(opts.orchestratorPath, composeOrchestrator(opts.floor, before));
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
const policyBackup = writeWithBackup(opts.policyPath, cleaned.endsWith("\n") ? cleaned : `${cleaned}\n`);
|
|
443
|
+
const composed = composeOrchestrator(opts.floor, readFileSync(opts.policyPath, "utf8"));
|
|
444
|
+
const orchestratorBackup = writeWithBackup(opts.orchestratorPath, composed);
|
|
445
|
+
return {
|
|
446
|
+
policyPath: opts.policyPath,
|
|
447
|
+
orchestratorPath: opts.orchestratorPath,
|
|
448
|
+
policyBackup,
|
|
449
|
+
orchestratorBackup,
|
|
450
|
+
ownedBytes: cleaned.length,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
312
454
|
/**
|
|
313
455
|
* Refresh composed ORCHESTRATOR.md from rendered floor + existing POLICY.md.
|
|
314
456
|
* Creates nothing when POLICY.md is absent (caller should migrate first).
|
package/src/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
inspectBriefLayout,
|
|
18
18
|
migrateToPolicy,
|
|
19
19
|
proposeRetrofit,
|
|
20
|
+
repairPolicyBannerCrumbs,
|
|
20
21
|
writeMergedBrief,
|
|
21
22
|
} from "./brief-upgrade.ts";
|
|
22
23
|
import { findProject, loadConfig, resolveCaps, stateDir } from "./config.ts";
|
|
@@ -593,7 +594,32 @@ try {
|
|
|
593
594
|
|
|
594
595
|
if (argv.includes("--migrate")) {
|
|
595
596
|
if (layout.kind === "overlay") {
|
|
596
|
-
|
|
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
|
+
}
|
|
597
623
|
break;
|
|
598
624
|
}
|
|
599
625
|
if (layout.kind === "missing") {
|
package/src/plugin.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
formatMigrateResult,
|
|
19
19
|
inspectBriefLayout,
|
|
20
20
|
migrateToPolicy,
|
|
21
|
+
repairPolicyBannerCrumbs,
|
|
21
22
|
writeMergedBrief,
|
|
22
23
|
} from "./brief-upgrade.ts";
|
|
23
24
|
import { configPath, expandHome, findProject, loadConfig, saveConfig } from "./config.ts";
|
|
@@ -957,6 +958,23 @@ export default function conductorPlugin(pi: PluginApi): void {
|
|
|
957
958
|
}),
|
|
958
959
|
"info",
|
|
959
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
|
+
}
|
|
960
978
|
break;
|
|
961
979
|
}
|
|
962
980
|
if (layout.kind === "legacy-bannered") {
|