omp-conductor 0.3.19 → 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 CHANGED
@@ -194,12 +194,17 @@ or installation, so an operator cannot drain one queue and kill another queue's
194
194
  workers.
195
195
 
196
196
  Ticks remain in their existing armed or disarmed state, so an ordinary update
197
- does not halt the exact pane or require another Telegram arm challenge. Any
198
- installation, reload, or verification failure leaves dispatch paused instead of
199
- bringing up a mixed fleet. The command never publishes npm, edits an install
200
- root, or delegates lifecycle steps to an AI session. It refuses to run inside a
201
- Herdr-managed session because an updater that restarts itself cannot verify the
202
- result.
197
+ does not halt the exact pane or require another Telegram arm challenge. Progress
198
+ names the Bun-global CLI, omp plugin, Herdr plugin, brief, reloads, and both
199
+ verification passes separately.
200
+
201
+ An installation, brief, reload, or verification failure pauses dispatch and
202
+ attempts to restore the exact CLI/plugin identities that were present before the
203
+ command. If rollback also fails, the error names every failed restoration and
204
+ keeps dispatch paused; it never brings a known mixed fleet back into service.
205
+ The command never publishes npm, edits an install root, or delegates lifecycle
206
+ steps to an AI session. It refuses to run inside a Herdr-managed session because
207
+ an updater that restarts itself cannot verify the result.
203
208
 
204
209
  The bundled `skill://conductor-update` remains available as a natural-language
205
210
  entry point, but it only runs this command; the lifecycle is implemented and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.3.19",
3
+ "version": "0.3.20",
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/cli.ts CHANGED
@@ -394,10 +394,12 @@ try {
394
394
  project: flag(argv, "project"),
395
395
  });
396
396
  process.stdout.write(
397
- result.alreadyCurrent
398
- ? `conductor ${result.version} already current on all three surfaces\n`
399
- : `upgraded conductor ${result.previousVersion} → ${result.version} (${result.gitHead})\n` +
400
- `dispatch ${result.dispatch} restored\n`,
397
+ `${result.alreadyCurrent ? "already current" : "upgrade complete"}:\n` +
398
+ ` Bun-global CLI omp-conductor@${result.version}\n` +
399
+ ` omp plugin omp-conductor@${result.version}\n` +
400
+ ` Herdr plugin herdr-conductor@${result.gitHead}\n` +
401
+ ` orchestrator brief managed ORCHESTRATOR.md floor current; POLICY.md preserved\n` +
402
+ ` dispatch ${result.dispatch}${result.alreadyCurrent ? "" : " restored"}\n`,
401
403
  );
402
404
  break;
403
405
  }
package/src/upgrade.ts CHANGED
@@ -125,33 +125,55 @@ function parseRegistry(raw: string): { version: string; gitHead: string } {
125
125
  }
126
126
 
127
127
  function ompPluginVersion(raw: string): string | undefined {
128
+ let parsed: unknown;
128
129
  try {
129
- const parsed = JSON.parse(raw) as { npm?: Array<{ name?: unknown; version?: unknown }> };
130
- const plugin = parsed.npm?.find((entry) => entry.name === PACKAGE);
131
- return typeof plugin?.version === "string" ? plugin.version : undefined;
130
+ parsed = JSON.parse(raw);
132
131
  } catch {
133
- return undefined;
132
+ throw new Error("omp plugin list returned invalid JSON");
134
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;
135
148
  }
136
149
 
137
150
  function herdrPluginSource(raw: string): string | undefined {
138
151
  const line = raw.split("\n").find((candidate) => /^- herdr-conductor\b/.test(candidate.trim()));
139
- return line?.match(/\[([^\]]+)\]\s*$/)?.[1];
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;
140
158
  }
141
159
 
142
- async function inspectSurfaces(deps: UpgradeDeps): Promise<{
160
+ interface InstalledSurfaces {
143
161
  cliVersion: string;
144
162
  ompVersion?: string;
145
163
  herdrSource?: string;
146
- }> {
164
+ }
165
+
166
+ async function inspectSurfaces(deps: UpgradeDeps): Promise<InstalledSurfaces> {
147
167
  const session = deps.env["HERDR_SESSION"] || "fleet";
148
168
  const [cli, omp, herdr] = await Promise.all([
149
169
  mustRun(deps, "omp-conductor", ["--version"]),
150
170
  mustRun(deps, "omp", ["plugin", "list", "--json"]),
151
171
  mustRun(deps, "herdr", ["--session", session, "plugin", "list"]),
152
172
  ]);
173
+ const cliVersion = cli.stdout.trim();
174
+ if (cliVersion.length === 0) throw new Error("installed omp-conductor CLI has no version");
153
175
  return {
154
- cliVersion: cli.stdout.trim(),
176
+ cliVersion,
155
177
  ompVersion: ompPluginVersion(omp.stdout),
156
178
  herdrSource: herdrPluginSource(herdr.stdout),
157
179
  };
@@ -173,6 +195,19 @@ function surfacesCurrent(
173
195
  );
174
196
  }
175
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
+
176
211
  async function waitForDrain(deps: UpgradeDeps, project?: string): Promise<void> {
177
212
  let last = -1;
178
213
  while (true) {
@@ -230,6 +265,122 @@ async function upgradeBrief(
230
265
  await mustRun(deps, "omp-conductor", ["brief-upgrade", "--migrate", "--apply", ...selected]);
231
266
  }
232
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
+
233
384
  export async function upgradeConductor(
234
385
  options: UpgradeOptions = {},
235
386
  overrides: Partial<UpgradeDeps> = {},
@@ -268,54 +419,113 @@ export async function upgradeConductor(
268
419
  if (brief.kind === "missing") throw new Error("no ORCHESTRATOR.md exists for the configured project");
269
420
  if (initial.herdr === "unknown") throw new Error("cannot determine whether herdr-fleet.service is active");
270
421
 
271
- if (initial.dispatch === "running") deps.setPaused(true);
272
- await waitForDrain(deps, project);
273
-
274
- deps.log(`upgrading conductor ${surfaces.cliVersion} → ${release.version}`);
275
- if (installNeeded) {
276
- await mustRun(deps, "bun", ["add", "-g", `${PACKAGE}@${release.version}`]);
277
- await mustRun(deps, "omp", ["plugin", "install", `${PACKAGE}@${release.version}`]);
278
- if (surfaces.herdrSource?.startsWith("local:")) {
279
- await mustRun(deps, "herdr", ["plugin", "unlink", HERDR_PLUGIN]);
280
- }
281
- await mustRun(deps, "herdr", [
282
- "plugin",
283
- "install",
284
- HERDR_SOURCE,
285
- "--ref",
286
- release.gitHead,
287
- "--yes",
288
- ]);
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);
289
426
  }
290
- await upgradeBrief(deps, brief.kind, project);
291
-
292
- if (initial.herdr === "active") {
293
- await mustRun(deps, "systemctl", ["restart", HERDR_UNIT]);
294
- }
295
- if (initial.dispatch !== "stopped") {
296
- await deps.restartDaemon();
297
- }
298
-
299
- const installed = await inspectSurfaces(deps);
300
- if (!surfacesCurrent(installed, release.version, release.gitHead)) {
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);
301
432
  throw new Error(
302
- `upgrade verification failed: cli=${installed.cliVersion}, omp=${installed.ompVersion ?? "missing"}, ` +
303
- `herdr=${installed.herdrSource ?? "missing"}`,
433
+ `upgrade failed while draining workers: ${failure}; no installation started; dispatch remains paused`,
304
434
  );
305
435
  }
306
- if (initial.herdr === "active") {
307
- const active = await mustRun(deps, "systemctl", ["is-active", HERDR_UNIT]);
308
- if (active.stdout.trim() !== "active") throw new Error(`${HERDR_UNIT} is not active after restart`);
309
- }
310
- await waitForRecovery(deps, initial, project);
311
- await deps.sleep(1_000);
312
- await waitForRecovery(deps, initial, project);
313
-
314
- if (initial.dispatch === "running") {
315
- deps.setPaused(false);
316
- if (deps.layers(project).dispatch !== "running") {
317
- throw new Error("upgrade installed successfully, but dispatch did not resume");
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
+ );
318
527
  }
528
+ throw new Error(`upgrade failed: ${failure}; previous installation restored; dispatch remains paused`);
319
529
  }
320
530
 
321
531
  return {