omp-conductor 0.3.18 → 0.3.20
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 +352 -182
- package/package.json +1 -1
- package/skills/conductor-onboarding/SKILL.md +43 -50
- package/skills/conductor-update/SKILL.md +15 -165
- package/src/board.ts +734 -0
- package/src/brief-upgrade.ts +102 -19
- package/src/briefs/orchestrator.md +32 -16
- package/src/briefs/worker.md +7 -2
- package/src/cli.ts +138 -53
- package/src/config.ts +22 -6
- package/src/daemon.ts +673 -126
- package/src/fleet.ts +64 -6
- package/src/graph-health.ts +296 -0
- package/src/graph.ts +6 -6
- package/src/omp.ts +15 -4
- package/src/orchestrator-tick.ts +157 -10
- package/src/orchestrator.ts +8 -1
- package/src/plugin.ts +173 -45
- package/src/release-policy.ts +202 -0
- package/src/setup-host.ts +285 -0
- package/src/setup.ts +24 -20
- package/src/store.ts +373 -13
- package/src/tracker/github.ts +171 -3
- package/src/transcript.ts +45 -0
- package/src/types.ts +145 -2
- package/src/unblock.ts +26 -14
- package/src/upgrade.ts +537 -0
- package/src/worker.ts +67 -30
- package/src/worktree.ts +66 -0
- package/systemd/omp-conductor.service.example +5 -3
package/src/upgrade.ts
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { inspectBriefLayout, type BriefLayout } from "./brief-upgrade.ts";
|
|
3
|
+
import { setPaused, statusSnapshot } from "./daemon.ts";
|
|
4
|
+
import { fleetLayers, type DispatchLayer, type FleetLayers } from "./fleet.ts";
|
|
5
|
+
import { livingDaemon, restartDaemon } from "./lifecycle.ts";
|
|
6
|
+
import { findProject, loadConfig } from "./config.ts";
|
|
7
|
+
import { renderBriefForProject } from "./setup.ts";
|
|
8
|
+
|
|
9
|
+
const PACKAGE = "omp-conductor";
|
|
10
|
+
const HERDR_PLUGIN = "herdr-conductor";
|
|
11
|
+
const HERDR_SOURCE = "TerrifiedBug/conductor/herdr";
|
|
12
|
+
const HERDR_UNIT = "herdr-fleet.service";
|
|
13
|
+
const DRAIN_POLL_MS = 5_000;
|
|
14
|
+
const RECOVERY_POLL_MS = 2_000;
|
|
15
|
+
const RECOVERY_ATTEMPTS = 30;
|
|
16
|
+
|
|
17
|
+
export interface UpgradeCommandResult {
|
|
18
|
+
code: number;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UpgradeOptions {
|
|
24
|
+
version?: string;
|
|
25
|
+
project?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UpgradeResult {
|
|
29
|
+
previousVersion: string;
|
|
30
|
+
version: string;
|
|
31
|
+
gitHead: string;
|
|
32
|
+
alreadyCurrent: boolean;
|
|
33
|
+
dispatch: DispatchLayer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UpgradeDeps {
|
|
37
|
+
run(command: string, args: readonly string[]): Promise<UpgradeCommandResult>;
|
|
38
|
+
snapshot(project?: string): { liveWorkers: number };
|
|
39
|
+
layers(project?: string): FleetLayers;
|
|
40
|
+
brief(project?: string): { kind: BriefLayout["kind"]; current: boolean };
|
|
41
|
+
daemonIdentity(): { running: boolean; project?: string };
|
|
42
|
+
setPaused(value: boolean): void;
|
|
43
|
+
restartDaemon(): Promise<void>;
|
|
44
|
+
sleep(ms: number): Promise<void>;
|
|
45
|
+
env: NodeJS.ProcessEnv;
|
|
46
|
+
log(message: string): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runCommand(command: string, args: readonly string[]): Promise<UpgradeCommandResult> {
|
|
50
|
+
try {
|
|
51
|
+
const child = Bun.spawn([command, ...args], {
|
|
52
|
+
stdin: "ignore",
|
|
53
|
+
stdout: "pipe",
|
|
54
|
+
stderr: "pipe",
|
|
55
|
+
env: process.env,
|
|
56
|
+
});
|
|
57
|
+
const stdout = new Response(child.stdout).text();
|
|
58
|
+
const stderr = new Response(child.stderr).text();
|
|
59
|
+
const code = await child.exited;
|
|
60
|
+
return { code, stdout: await stdout, stderr: await stderr };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return { code: 127, stdout: "", stderr: err instanceof Error ? err.message : String(err) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const DEFAULT_DEPS: UpgradeDeps = {
|
|
67
|
+
run: runCommand,
|
|
68
|
+
snapshot: statusSnapshot,
|
|
69
|
+
layers: fleetLayers,
|
|
70
|
+
brief: (projectName) => {
|
|
71
|
+
const project = findProject(loadConfig(), projectName);
|
|
72
|
+
const layout = inspectBriefLayout(project.workspaceRoot, renderBriefForProject(project));
|
|
73
|
+
if (layout.kind !== "overlay") return { kind: layout.kind, current: false };
|
|
74
|
+
const policy = readFileSync(layout.policyPath, "utf8");
|
|
75
|
+
const live = readFileSync(layout.orchestratorPath, "utf8");
|
|
76
|
+
return { kind: layout.kind, current: live === renderBriefForProject(project, policy) };
|
|
77
|
+
},
|
|
78
|
+
daemonIdentity: () => {
|
|
79
|
+
const daemon = livingDaemon();
|
|
80
|
+
return daemon === undefined ? { running: false } : { running: true, project: daemon.project };
|
|
81
|
+
},
|
|
82
|
+
setPaused,
|
|
83
|
+
restartDaemon: async () => {
|
|
84
|
+
await restartDaemon({});
|
|
85
|
+
},
|
|
86
|
+
sleep: Bun.sleep,
|
|
87
|
+
env: process.env,
|
|
88
|
+
log: (message) => process.stdout.write(`${message}\n`),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function commandLine(command: string, args: readonly string[]): string {
|
|
92
|
+
return [command, ...args].join(" ");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function mustRun(
|
|
96
|
+
deps: UpgradeDeps,
|
|
97
|
+
command: string,
|
|
98
|
+
args: readonly string[],
|
|
99
|
+
): Promise<UpgradeCommandResult> {
|
|
100
|
+
const ran = await deps.run(command, args);
|
|
101
|
+
if (ran.code === 0) return ran;
|
|
102
|
+
const detail = ran.stderr.trim() || ran.stdout.trim() || `exit ${ran.code}`;
|
|
103
|
+
throw new Error(`${commandLine(command, args)} failed: ${detail}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseRegistry(raw: string): { version: string; gitHead: string } {
|
|
107
|
+
let value: unknown;
|
|
108
|
+
try {
|
|
109
|
+
value = JSON.parse(raw);
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error("npm returned invalid release metadata");
|
|
112
|
+
}
|
|
113
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
114
|
+
throw new Error("npm returned invalid release metadata");
|
|
115
|
+
}
|
|
116
|
+
const version = Reflect.get(value, "version");
|
|
117
|
+
const gitHead = Reflect.get(value, "gitHead");
|
|
118
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
119
|
+
throw new Error("npm release metadata has no version");
|
|
120
|
+
}
|
|
121
|
+
if (typeof gitHead !== "string" || !/^[0-9a-f]{40}$/i.test(gitHead)) {
|
|
122
|
+
throw new Error(`npm release ${version} has no full gitHead`);
|
|
123
|
+
}
|
|
124
|
+
return { version, gitHead };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ompPluginVersion(raw: string): string | undefined {
|
|
128
|
+
let parsed: unknown;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(raw);
|
|
131
|
+
} catch {
|
|
132
|
+
throw new Error("omp plugin list returned invalid JSON");
|
|
133
|
+
}
|
|
134
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
135
|
+
throw new Error("omp plugin list returned an invalid inventory");
|
|
136
|
+
}
|
|
137
|
+
const npm = Reflect.get(parsed, "npm");
|
|
138
|
+
if (!Array.isArray(npm)) throw new Error("omp plugin list returned no npm inventory");
|
|
139
|
+
const plugin = npm.find(
|
|
140
|
+
(entry) => entry !== null && typeof entry === "object" && Reflect.get(entry, "name") === PACKAGE,
|
|
141
|
+
);
|
|
142
|
+
if (plugin === undefined) return undefined;
|
|
143
|
+
const version = Reflect.get(plugin, "version");
|
|
144
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
145
|
+
throw new Error("installed omp-conductor plugin has no version");
|
|
146
|
+
}
|
|
147
|
+
return version;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function herdrPluginSource(raw: string): string | undefined {
|
|
151
|
+
const line = raw.split("\n").find((candidate) => /^- herdr-conductor\b/.test(candidate.trim()));
|
|
152
|
+
if (line === undefined) return undefined;
|
|
153
|
+
const source = line.match(/\[([^\]]+)\]\s*$/)?.[1];
|
|
154
|
+
if (source === undefined) {
|
|
155
|
+
throw new Error("cannot determine installed Herdr plugin source");
|
|
156
|
+
}
|
|
157
|
+
return source;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface InstalledSurfaces {
|
|
161
|
+
cliVersion: string;
|
|
162
|
+
ompVersion?: string;
|
|
163
|
+
herdrSource?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function inspectSurfaces(deps: UpgradeDeps): Promise<InstalledSurfaces> {
|
|
167
|
+
const session = deps.env["HERDR_SESSION"] || "fleet";
|
|
168
|
+
const [cli, omp, herdr] = await Promise.all([
|
|
169
|
+
mustRun(deps, "omp-conductor", ["--version"]),
|
|
170
|
+
mustRun(deps, "omp", ["plugin", "list", "--json"]),
|
|
171
|
+
mustRun(deps, "herdr", ["--session", session, "plugin", "list"]),
|
|
172
|
+
]);
|
|
173
|
+
const cliVersion = cli.stdout.trim();
|
|
174
|
+
if (cliVersion.length === 0) throw new Error("installed omp-conductor CLI has no version");
|
|
175
|
+
return {
|
|
176
|
+
cliVersion,
|
|
177
|
+
ompVersion: ompPluginVersion(omp.stdout),
|
|
178
|
+
herdrSource: herdrPluginSource(herdr.stdout),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function expectedHerdrSource(gitHead: string): string {
|
|
183
|
+
return `github:${HERDR_SOURCE}@${gitHead}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function surfacesCurrent(
|
|
187
|
+
surfaces: Awaited<ReturnType<typeof inspectSurfaces>>,
|
|
188
|
+
version: string,
|
|
189
|
+
gitHead: string,
|
|
190
|
+
): boolean {
|
|
191
|
+
return (
|
|
192
|
+
surfaces.cliVersion === version &&
|
|
193
|
+
surfaces.ompVersion === version &&
|
|
194
|
+
surfaces.herdrSource === expectedHerdrSource(gitHead)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function previousHerdrInstall(source: string): readonly [string, readonly string[]] | undefined {
|
|
199
|
+
if (source.startsWith("local:")) {
|
|
200
|
+
return ["herdr", ["plugin", "link", source.slice("local:".length), "--enabled"]];
|
|
201
|
+
}
|
|
202
|
+
if (!source.startsWith("github:")) return undefined;
|
|
203
|
+
const at = source.lastIndexOf("@");
|
|
204
|
+
if (at < "github:".length) return undefined;
|
|
205
|
+
return [
|
|
206
|
+
"herdr",
|
|
207
|
+
["plugin", "install", source.slice("github:".length, at), "--ref", source.slice(at + 1), "--yes"],
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function waitForDrain(deps: UpgradeDeps, project?: string): Promise<void> {
|
|
212
|
+
let last = -1;
|
|
213
|
+
while (true) {
|
|
214
|
+
const workers = deps.snapshot(project).liveWorkers;
|
|
215
|
+
if (workers === 0) return;
|
|
216
|
+
if (workers !== last) deps.log(`waiting for ${workers} live worker(s) to finish`);
|
|
217
|
+
last = workers;
|
|
218
|
+
await deps.sleep(DRAIN_POLL_MS);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function recoveryProblem(
|
|
223
|
+
layers: FleetLayers,
|
|
224
|
+
initial: FleetLayers,
|
|
225
|
+
liveWorkers: number,
|
|
226
|
+
): string | undefined {
|
|
227
|
+
if (layers.ticks !== initial.ticks) return `ticks changed from ${initial.ticks} to ${layers.ticks}`;
|
|
228
|
+
if (layers.recovery !== initial.recovery) {
|
|
229
|
+
return `recovery changed from ${initial.recovery} to ${layers.recovery}`;
|
|
230
|
+
}
|
|
231
|
+
if (initial.herdr === "active" && layers.herdr !== "active") return `Herdr is ${layers.herdr}`;
|
|
232
|
+
if (initial.herdr === "active" && initial.recovery === "clear" && layers.pane !== "live") {
|
|
233
|
+
return `orchestrator pane is ${layers.pane}`;
|
|
234
|
+
}
|
|
235
|
+
if (initial.dispatch !== "stopped" && !layers.daemon.running) return "dispatch daemon is not running";
|
|
236
|
+
if (initial.dispatch === "stopped" && layers.daemon.running) return "stopped dispatch daemon was started";
|
|
237
|
+
if (initial.dispatch !== "stopped" && !layers.paused) return "dispatch resumed before verification";
|
|
238
|
+
if (liveWorkers !== 0) return `${liveWorkers} worker(s) appeared during verification`;
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function waitForRecovery(
|
|
243
|
+
deps: UpgradeDeps,
|
|
244
|
+
initial: FleetLayers,
|
|
245
|
+
project?: string,
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
let problem = "recovery did not settle";
|
|
248
|
+
for (let attempt = 0; attempt < RECOVERY_ATTEMPTS; attempt += 1) {
|
|
249
|
+
problem = recoveryProblem(deps.layers(project), initial, deps.snapshot(project).liveWorkers) ?? "";
|
|
250
|
+
if (problem === "") return;
|
|
251
|
+
await deps.sleep(RECOVERY_POLL_MS);
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`upgrade verification failed: ${problem}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function upgradeBrief(
|
|
257
|
+
deps: UpgradeDeps,
|
|
258
|
+
kind: BriefLayout["kind"],
|
|
259
|
+
project?: string,
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
const selected = project === undefined ? [] : ["--project", project];
|
|
262
|
+
if (kind === "legacy-handwritten") {
|
|
263
|
+
await mustRun(deps, "omp-conductor", ["brief-upgrade", "--retrofit", "--apply", ...selected]);
|
|
264
|
+
}
|
|
265
|
+
await mustRun(deps, "omp-conductor", ["brief-upgrade", "--migrate", "--apply", ...selected]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function rollbackUpgrade(
|
|
269
|
+
deps: UpgradeDeps,
|
|
270
|
+
previous: InstalledSurfaces,
|
|
271
|
+
project: string | undefined,
|
|
272
|
+
installTouched: boolean,
|
|
273
|
+
briefChanged: boolean,
|
|
274
|
+
herdrReloadStarted: boolean,
|
|
275
|
+
daemonReloadStarted: boolean,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
const failures: string[] = [];
|
|
278
|
+
const restore = async (label: string, command: string, args: readonly string[]): Promise<void> => {
|
|
279
|
+
deps.log(`rollback: ${label}`);
|
|
280
|
+
try {
|
|
281
|
+
await mustRun(deps, command, args);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
failures.push(err instanceof Error ? err.message : String(err));
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const removeIfPresent = async (
|
|
287
|
+
label: string,
|
|
288
|
+
command: string,
|
|
289
|
+
args: readonly string[],
|
|
290
|
+
): Promise<void> => {
|
|
291
|
+
deps.log(`rollback: ${label}`);
|
|
292
|
+
try {
|
|
293
|
+
await deps.run(command, args);
|
|
294
|
+
} catch {
|
|
295
|
+
// Exact post-rollback identity verification decides whether absence is safe.
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (installTouched) {
|
|
300
|
+
await restore("Bun-global omp-conductor CLI", "bun", [
|
|
301
|
+
"add",
|
|
302
|
+
"-g",
|
|
303
|
+
`${PACKAGE}@${previous.cliVersion}`,
|
|
304
|
+
]);
|
|
305
|
+
if (previous.ompVersion === undefined) {
|
|
306
|
+
await removeIfPresent("remove newly installed omp plugin", "omp", [
|
|
307
|
+
"plugin",
|
|
308
|
+
"uninstall",
|
|
309
|
+
PACKAGE,
|
|
310
|
+
]);
|
|
311
|
+
} else {
|
|
312
|
+
await restore("omp plugin omp-conductor", "omp", [
|
|
313
|
+
"plugin",
|
|
314
|
+
"install",
|
|
315
|
+
`${PACKAGE}@${previous.ompVersion}`,
|
|
316
|
+
]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (previous.herdrSource === undefined) {
|
|
320
|
+
await removeIfPresent("remove newly installed Herdr plugin", "herdr", [
|
|
321
|
+
"plugin",
|
|
322
|
+
"uninstall",
|
|
323
|
+
HERDR_PLUGIN,
|
|
324
|
+
]);
|
|
325
|
+
} else {
|
|
326
|
+
const install = previousHerdrInstall(previous.herdrSource);
|
|
327
|
+
if (install === undefined) {
|
|
328
|
+
failures.push(`cannot restore unknown Herdr source ${previous.herdrSource}`);
|
|
329
|
+
} else {
|
|
330
|
+
if (previous.herdrSource.startsWith("local:")) {
|
|
331
|
+
await removeIfPresent("remove managed Herdr plugin", "herdr", [
|
|
332
|
+
"plugin",
|
|
333
|
+
"uninstall",
|
|
334
|
+
HERDR_PLUGIN,
|
|
335
|
+
]);
|
|
336
|
+
}
|
|
337
|
+
await restore("Herdr plugin herdr-conductor", install[0], install[1]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
}
|
|
342
|
+
if (briefChanged) {
|
|
343
|
+
try {
|
|
344
|
+
deps.log("rollback: ORCHESTRATOR.md package floor");
|
|
345
|
+
await upgradeBrief(deps, "overlay", project);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
failures.push(err instanceof Error ? err.message : String(err));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (herdrReloadStarted) {
|
|
352
|
+
await restore("restart herdr-fleet.service", "systemctl", ["restart", HERDR_UNIT]);
|
|
353
|
+
}
|
|
354
|
+
if (daemonReloadStarted) {
|
|
355
|
+
try {
|
|
356
|
+
deps.log("rollback: restart omp-conductor dispatch daemon");
|
|
357
|
+
await deps.restartDaemon();
|
|
358
|
+
} catch (err) {
|
|
359
|
+
failures.push(err instanceof Error ? err.message : String(err));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (installTouched) {
|
|
364
|
+
try {
|
|
365
|
+
const restored = await inspectSurfaces(deps);
|
|
366
|
+
if (
|
|
367
|
+
restored.cliVersion !== previous.cliVersion ||
|
|
368
|
+
restored.ompVersion !== previous.ompVersion ||
|
|
369
|
+
restored.herdrSource !== previous.herdrSource
|
|
370
|
+
) {
|
|
371
|
+
failures.push(
|
|
372
|
+
`restored identities differ: cli=${restored.cliVersion}, omp=${restored.ompVersion ?? "missing"}, ` +
|
|
373
|
+
`herdr=${restored.herdrSource ?? "missing"}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
} catch (err) {
|
|
377
|
+
failures.push(err instanceof Error ? err.message : String(err));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (failures.length > 0) throw new Error(failures.join("; "));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function upgradeConductor(
|
|
385
|
+
options: UpgradeOptions = {},
|
|
386
|
+
overrides: Partial<UpgradeDeps> = {},
|
|
387
|
+
): Promise<UpgradeResult> {
|
|
388
|
+
const deps: UpgradeDeps = { ...DEFAULT_DEPS, ...overrides };
|
|
389
|
+
if (deps.env["HERDR_ENV"] !== undefined) {
|
|
390
|
+
throw new Error("run omp-conductor upgrade from a shell outside the target Herdr session");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const requested = options.version === undefined ? `${PACKAGE}@latest` : `${PACKAGE}@${options.version}`;
|
|
394
|
+
const daemon = deps.daemonIdentity();
|
|
395
|
+
if (daemon.running && options.project !== undefined && daemon.project !== options.project) {
|
|
396
|
+
const active = daemon.project === undefined ? "an unrecorded project" : daemon.project;
|
|
397
|
+
throw new Error(
|
|
398
|
+
`active daemon serves ${active}; refusing to upgrade --project ${options.project} while it is running`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const project = options.project ?? daemon.project;
|
|
402
|
+
|
|
403
|
+
const release = parseRegistry(
|
|
404
|
+
(await mustRun(deps, "npm", ["view", requested, "version", "gitHead", "--json"])).stdout,
|
|
405
|
+
);
|
|
406
|
+
const initial = deps.layers(project);
|
|
407
|
+
const surfaces = await inspectSurfaces(deps);
|
|
408
|
+
const brief = deps.brief(project);
|
|
409
|
+
const installNeeded = !surfacesCurrent(surfaces, release.version, release.gitHead);
|
|
410
|
+
if (!installNeeded && brief.current) {
|
|
411
|
+
return {
|
|
412
|
+
previousVersion: surfaces.cliVersion,
|
|
413
|
+
...release,
|
|
414
|
+
alreadyCurrent: true,
|
|
415
|
+
dispatch: initial.dispatch,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (brief.kind === "missing") throw new Error("no ORCHESTRATOR.md exists for the configured project");
|
|
420
|
+
if (initial.herdr === "unknown") throw new Error("cannot determine whether herdr-fleet.service is active");
|
|
421
|
+
|
|
422
|
+
deps.log(`target release: omp-conductor@${release.version} (${release.gitHead})`);
|
|
423
|
+
if (!initial.paused) {
|
|
424
|
+
deps.log("safety: pausing new issue claims");
|
|
425
|
+
deps.setPaused(true);
|
|
426
|
+
}
|
|
427
|
+
deps.log("drain: waiting for live omp worker sessions");
|
|
428
|
+
try {
|
|
429
|
+
await waitForDrain(deps, project);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
const failure = err instanceof Error ? err.message : String(err);
|
|
432
|
+
throw new Error(
|
|
433
|
+
`upgrade failed while draining workers: ${failure}; no installation started; dispatch remains paused`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
deps.log("drain: complete");
|
|
437
|
+
|
|
438
|
+
let installTouched = false;
|
|
439
|
+
let briefChanged = false;
|
|
440
|
+
let herdrReloadStarted = false;
|
|
441
|
+
let daemonReloadStarted = false;
|
|
442
|
+
try {
|
|
443
|
+
if (installNeeded) {
|
|
444
|
+
installTouched = true;
|
|
445
|
+
deps.log(`install 1/3: Bun-global omp-conductor CLI → ${release.version}`);
|
|
446
|
+
await mustRun(deps, "bun", ["add", "-g", `${PACKAGE}@${release.version}`]);
|
|
447
|
+
deps.log(`install 2/3: omp plugin omp-conductor → ${release.version}`);
|
|
448
|
+
await mustRun(deps, "omp", ["plugin", "install", `${PACKAGE}@${release.version}`]);
|
|
449
|
+
if (surfaces.herdrSource?.startsWith("local:")) {
|
|
450
|
+
await mustRun(deps, "herdr", ["plugin", "unlink", HERDR_PLUGIN]);
|
|
451
|
+
}
|
|
452
|
+
deps.log(`install 3/3: Herdr plugin herdr-conductor → ${release.gitHead}`);
|
|
453
|
+
await mustRun(deps, "herdr", [
|
|
454
|
+
"plugin",
|
|
455
|
+
"install",
|
|
456
|
+
HERDR_SOURCE,
|
|
457
|
+
"--ref",
|
|
458
|
+
release.gitHead,
|
|
459
|
+
"--yes",
|
|
460
|
+
]);
|
|
461
|
+
} else {
|
|
462
|
+
deps.log("install: CLI, omp plugin, and Herdr plugin already pinned to the target release");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
deps.log("brief: refreshing managed ORCHESTRATOR.md floor while preserving POLICY.md");
|
|
466
|
+
briefChanged = true;
|
|
467
|
+
await upgradeBrief(deps, brief.kind, project);
|
|
468
|
+
|
|
469
|
+
if (initial.herdr === "active") {
|
|
470
|
+
herdrReloadStarted = true;
|
|
471
|
+
deps.log("reload: restarting herdr-fleet.service and recovering the orchestrator pane");
|
|
472
|
+
await mustRun(deps, "systemctl", ["restart", HERDR_UNIT]);
|
|
473
|
+
}
|
|
474
|
+
if (initial.dispatch !== "stopped") {
|
|
475
|
+
daemonReloadStarted = true;
|
|
476
|
+
deps.log("reload: restarting the omp-conductor dispatch daemon");
|
|
477
|
+
await deps.restartDaemon();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
deps.log("verify 1/2: package identities, services, pane, ticks, and paused dispatch");
|
|
481
|
+
const installed = await inspectSurfaces(deps);
|
|
482
|
+
if (!surfacesCurrent(installed, release.version, release.gitHead)) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`installed identities differ: cli=${installed.cliVersion}, omp=${installed.ompVersion ?? "missing"}, ` +
|
|
485
|
+
`herdr=${installed.herdrSource ?? "missing"}`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (initial.herdr === "active") {
|
|
489
|
+
const active = await mustRun(deps, "systemctl", ["is-active", HERDR_UNIT]);
|
|
490
|
+
if (active.stdout.trim() !== "active") throw new Error(`${HERDR_UNIT} is not active after restart`);
|
|
491
|
+
}
|
|
492
|
+
await waitForRecovery(deps, initial, project);
|
|
493
|
+
deps.log("verify 2/2: recovered fleet remains stable");
|
|
494
|
+
await deps.sleep(1_000);
|
|
495
|
+
await waitForRecovery(deps, initial, project);
|
|
496
|
+
|
|
497
|
+
if (!initial.paused) deps.setPaused(false);
|
|
498
|
+
const restoredDispatch = deps.layers(project).dispatch;
|
|
499
|
+
if (restoredDispatch !== initial.dispatch) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`dispatch state is ${restoredDispatch}; expected to restore ${initial.dispatch}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
try {
|
|
506
|
+
if (!deps.layers(project).paused) deps.setPaused(true);
|
|
507
|
+
} catch {
|
|
508
|
+
deps.setPaused(true);
|
|
509
|
+
}
|
|
510
|
+
const failure = err instanceof Error ? err.message : String(err);
|
|
511
|
+
deps.log(`upgrade failed: ${failure}`);
|
|
512
|
+
try {
|
|
513
|
+
await rollbackUpgrade(
|
|
514
|
+
deps,
|
|
515
|
+
surfaces,
|
|
516
|
+
project,
|
|
517
|
+
installTouched,
|
|
518
|
+
briefChanged,
|
|
519
|
+
herdrReloadStarted,
|
|
520
|
+
daemonReloadStarted,
|
|
521
|
+
);
|
|
522
|
+
} catch (rollbackErr) {
|
|
523
|
+
const rollback = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
524
|
+
throw new Error(
|
|
525
|
+
`upgrade failed: ${failure}; rollback incomplete: ${rollback}; dispatch remains paused`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`upgrade failed: ${failure}; previous installation restored; dispatch remains paused`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
previousVersion: surfaces.cliVersion,
|
|
533
|
+
...release,
|
|
534
|
+
alreadyCurrent: false,
|
|
535
|
+
dispatch: initial.dispatch,
|
|
536
|
+
};
|
|
537
|
+
}
|