omp-conductor 0.19.5 → 0.19.7

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/src/upgrade.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
3
4
  import { backupTimestamp, copyToUniqueBackup } from "./backups.ts";
4
5
  import { inspectBriefLayout, type BriefLayout } from "./brief-upgrade.ts";
5
6
  import { pauseInstance, setPaused, statusSnapshot, type AdmissionAckRecord } from "./daemon.ts";
@@ -24,6 +25,7 @@ import {
24
25
  upgradeJournalPath,
25
26
  type UpgradeCheck,
26
27
  type UpgradeJournalEntry,
28
+ type InstallSnapshotManagement,
27
29
  } from "./upgrade-journal.ts";
28
30
  import {
29
31
  enqueueUpgradeReport,
@@ -279,7 +281,7 @@ function parseRegistry(raw: string): { version: string; gitHead: string } {
279
281
  interface ReleaseIdentityPlan {
280
282
  version: string;
281
283
  gitHead: string;
282
- /** What `bun add -g` / `omp plugin install` are given for this identity. */
284
+ /** What the discovered CLI root / `omp plugin install` are given for this identity. */
283
285
  packageSpec: string;
284
286
  /** Named checks already run to earn this identity, journalled as evidence. */
285
287
  checks: UpgradeCheck[];
@@ -370,7 +372,12 @@ async function resolveIdentity(deps: UpgradeDeps, options: UpgradeOptions): Prom
370
372
  return { version, gitHead: sha, packageSpec: `github:${PACKAGE_SOURCE}#${sha}`, checks, bootstrap: true };
371
373
  }
372
374
 
373
- function ompPluginVersion(raw: string): string | undefined {
375
+ interface OmpPluginEntry {
376
+ version: string;
377
+ path?: string;
378
+ }
379
+
380
+ function ompPluginInventory(raw: string): unknown[] {
374
381
  let parsed: unknown;
375
382
  try {
376
383
  parsed = JSON.parse(raw);
@@ -382,15 +389,51 @@ function ompPluginVersion(raw: string): string | undefined {
382
389
  }
383
390
  const npm = Reflect.get(parsed, "npm");
384
391
  if (!Array.isArray(npm)) throw new Error("omp plugin list returned no npm inventory");
385
- const plugin = npm.find(
392
+ return npm;
393
+ }
394
+
395
+ function ompPluginEntry(raw: string): OmpPluginEntry | undefined {
396
+ const plugin = ompPluginInventory(raw).find(
386
397
  (entry) => entry !== null && typeof entry === "object" && Reflect.get(entry, "name") === PACKAGE,
387
398
  );
388
- if (plugin === undefined) return undefined;
399
+ if (plugin === undefined || plugin === null || typeof plugin !== "object") return undefined;
389
400
  const version = Reflect.get(plugin, "version");
390
401
  if (typeof version !== "string" || version.length === 0) {
391
402
  throw new Error("installed omp-conductor plugin has no version");
392
403
  }
393
- return version;
404
+ const path = Reflect.get(plugin, "path");
405
+ return {
406
+ version,
407
+ ...(typeof path === "string" && path.length > 0 ? { path } : {}),
408
+ };
409
+ }
410
+
411
+ function ompPluginVersion(raw: string): string | undefined {
412
+ return ompPluginEntry(raw)?.version;
413
+ }
414
+
415
+ function nodeModulesRoot(path: string): string | undefined {
416
+ const marker = "/node_modules/";
417
+ const at = path.lastIndexOf(marker);
418
+ return at <= 0 ? undefined : path.slice(0, at);
419
+ }
420
+
421
+ function defaultOmpPluginRoot(env: NodeJS.ProcessEnv): string {
422
+ const configured = env["OMP_CODING_AGENT_DIR"];
423
+ if (configured !== undefined && configured.length > 0) return join(configured, "plugins");
424
+ return join(env["HOME"] ?? homedir(), ".omp", "plugins");
425
+ }
426
+
427
+ function ompPluginInstallRoot(raw: string, env: NodeJS.ProcessEnv): string {
428
+ const inventory = ompPluginInventory(raw);
429
+ const ownPath = ompPluginEntry(raw)?.path;
430
+ const fallbackPath = inventory
431
+ .map((entry) =>
432
+ entry !== null && typeof entry === "object" ? Reflect.get(entry, "path") : undefined,
433
+ )
434
+ .find((path): path is string => typeof path === "string" && path.length > 0);
435
+ const root = nodeModulesRoot(ownPath ?? fallbackPath ?? "");
436
+ return root ?? defaultOmpPluginRoot(env);
394
437
  }
395
438
 
396
439
  function herdrPluginSource(raw: string): string | undefined {
@@ -403,12 +446,143 @@ function herdrPluginSource(raw: string): string | undefined {
403
446
  return source;
404
447
  }
405
448
 
406
- /** The three installed identities one host carries: the Bun-global CLI/daemon
407
- * tree, the omp plugin, and the herdr recovery plugin's pin. */
449
+ function defaultHerdrInstallRoot(env: NodeJS.ProcessEnv): string {
450
+ const config =
451
+ env["HERDR_CONFIG_DIR"] ??
452
+ join(env["XDG_CONFIG_HOME"] ?? join(env["HOME"] ?? homedir(), ".config"), "herdr");
453
+ return join(config, "plugins", "github");
454
+ }
455
+
456
+ function herdrPluginManagement(
457
+ raw: string,
458
+ env: NodeJS.ProcessEnv,
459
+ ): InstallSnapshotManagement["herdr"] {
460
+ let parsed: unknown;
461
+ try {
462
+ parsed = JSON.parse(raw);
463
+ } catch {
464
+ throw new Error("herdr plugin list returned invalid JSON");
465
+ }
466
+ const result =
467
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
468
+ ? Reflect.get(parsed, "result")
469
+ : undefined;
470
+ const plugins =
471
+ result !== null && typeof result === "object" && !Array.isArray(result)
472
+ ? Reflect.get(result, "plugins")
473
+ : undefined;
474
+ if (!Array.isArray(plugins)) throw new Error("herdr plugin list returned no plugin inventory");
475
+ const plugin = plugins.find(
476
+ (entry) => entry !== null && typeof entry === "object" && Reflect.get(entry, "plugin_id") === HERDR_PLUGIN,
477
+ );
478
+ const root =
479
+ plugin !== undefined && typeof plugin === "object"
480
+ ? Reflect.get(plugin, "plugin_root")
481
+ : undefined;
482
+ const source =
483
+ plugin !== undefined && typeof plugin === "object"
484
+ ? Reflect.get(plugin, "source")
485
+ : undefined;
486
+ const managedPath =
487
+ source !== null && typeof source === "object" && !Array.isArray(source)
488
+ ? Reflect.get(source, "managed_path")
489
+ : undefined;
490
+ const marker = "/plugins/github/";
491
+ const markerAt = typeof root === "string" ? root.indexOf(marker) : -1;
492
+ const installRoot =
493
+ typeof managedPath === "string" && managedPath.length > 0
494
+ ? dirname(managedPath)
495
+ : markerAt >= 0 && typeof root === "string"
496
+ ? root.slice(0, markerAt + marker.length - 1)
497
+ : defaultHerdrInstallRoot(env);
498
+ return {
499
+ ...(typeof root === "string" && root.length > 0 ? { root } : {}),
500
+ installRoot,
501
+ };
502
+ }
503
+
504
+ function cliInstallRoot(target: string): string {
505
+ const marker = `/node_modules/${PACKAGE}/`;
506
+ const at = target.lastIndexOf(marker);
507
+ if (at <= 0) {
508
+ throw new Error(
509
+ `installed omp-conductor CLI target ${target} is not inside a discoverable node_modules/${PACKAGE} root`,
510
+ );
511
+ }
512
+ return target.slice(0, at);
513
+ }
514
+
515
+ /** Discover the exact managed roots the transaction will mutate (#1019). */
516
+ export async function discoverInstallManagement(
517
+ deps: SurfaceProbeDeps,
518
+ ): Promise<InstallSnapshotManagement> {
519
+ const session = resolveHerdrSession(deps.env);
520
+ const executable = (await mustRun(deps, "which", ["omp-conductor"])).stdout.trim();
521
+ if (executable.length === 0) throw new Error("which omp-conductor returned no executable");
522
+ const [resolved, omp, herdr] = await Promise.all([
523
+ mustRun(deps, "readlink", ["-f", executable]),
524
+ mustRun(deps, "omp", ["plugin", "list", "--json"]),
525
+ mustRun(deps, "herdr", ["--session", session, "plugin", "list", "--json"]),
526
+ ]);
527
+ const target = resolved.stdout.trim();
528
+ if (target.length === 0) throw new Error(`cannot resolve installed CLI ${executable}`);
529
+ const ompRoot = ompPluginInstallRoot(omp.stdout, deps.env);
530
+ return {
531
+ cli: { executable, target, root: cliInstallRoot(target) },
532
+ omp: { root: ompRoot, lock: join(ompRoot, "omp-plugins.lock.json") },
533
+ herdr: herdrPluginManagement(herdr.stdout, deps.env),
534
+ };
535
+ }
536
+
537
+ async function preflightInstall(
538
+ deps: UpgradeDeps,
539
+ management: InstallSnapshotManagement,
540
+ packageSpec: string,
541
+ ): Promise<void> {
542
+ const writable: { label: string; path: string }[] = [
543
+ { label: "CLI package root", path: management.cli.root },
544
+ { label: "CLI package manifest", path: join(management.cli.root, "package.json") },
545
+ { label: "CLI node_modules", path: join(management.cli.root, "node_modules") },
546
+ { label: "OMP plugin root", path: management.omp.root },
547
+ { label: "Herdr managed-plugin root", path: management.herdr.installRoot },
548
+ ];
549
+ if (existsSync(management.omp.lock)) {
550
+ writable.push({ label: "OMP plugin lock", path: management.omp.lock });
551
+ }
552
+ for (const target of writable) {
553
+ const checked = await deps.run("test", ["-w", target.path]);
554
+ if (checked.code !== 0) {
555
+ const detail = checked.stderr.trim() || checked.stdout.trim() || "missing or not writable";
556
+ throw new Error(
557
+ `upgrade preflight refused: ${target.label} ${target.path} is unusable (${detail}); ` +
558
+ "nothing has been installed or paused",
559
+ );
560
+ }
561
+ }
562
+ try {
563
+ await mustRun(deps, "bun", [
564
+ "add",
565
+ "--cwd",
566
+ management.cli.root,
567
+ "--exact",
568
+ "--dry-run",
569
+ packageSpec,
570
+ ]);
571
+ } catch (err) {
572
+ throw new Error(
573
+ `upgrade preflight refused: the CLI install command is unusable at ${management.cli.root} ` +
574
+ `(${err instanceof Error ? err.message : String(err)}); nothing has been installed or paused`,
575
+ );
576
+ }
577
+ }
578
+
579
+ /** The three installed identities one host carries: the discovered CLI package
580
+ * tree, the omp plugin, and the herdr recovery plugin's pin. */
408
581
  export interface InstalledSurfaces {
409
582
  cliVersion: string;
410
583
  ompVersion?: string;
411
584
  herdrSource?: string;
585
+ management?: InstallSnapshotManagement;
412
586
  }
413
587
 
414
588
  /** What reading the three surfaces needs — deliberately narrower than
@@ -427,6 +601,7 @@ export async function inspectSurfaces(
427
601
  deps: SurfaceProbeDeps,
428
602
  /** `readHerdr: false` skips the herdr session probe and plugin read
429
603
  * entirely, so a host with no herdr on PATH can still be asked what its
604
+
430
605
  * CLI and omp plugin are (#904). The upgrade transaction always reads all
431
606
  * three — a release pins every surface. */
432
607
  opts: { readHerdr?: boolean } = {},
@@ -455,6 +630,56 @@ export async function inspectSurfaces(
455
630
  };
456
631
  }
457
632
 
633
+ function packageVersionAt(deps: UpgradeDeps, manifest: string): string {
634
+ let parsed: unknown;
635
+ try {
636
+ parsed = JSON.parse(deps.readFile(manifest));
637
+ } catch (err) {
638
+ throw new Error(
639
+ `cannot read restored package manifest ${manifest}: ${err instanceof Error ? err.message : String(err)}`,
640
+ );
641
+ }
642
+ const version =
643
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
644
+ ? Reflect.get(parsed, "version")
645
+ : undefined;
646
+ if (typeof version !== "string" || version.length === 0) {
647
+ throw new Error(`restored package manifest ${manifest} has no version`);
648
+ }
649
+ return version;
650
+ }
651
+
652
+ /** Verify the snapshot's recorded destinations directly, before PATH-based probes. */
653
+ function verifyRestoredManagement(deps: UpgradeDeps, previous: InstalledSurfaces): void {
654
+ const management = previous.management;
655
+ if (management === undefined) return;
656
+ const cliManifest = join(management.cli.root, "node_modules", PACKAGE, "package.json");
657
+ const cliVersion = packageVersionAt(deps, cliManifest);
658
+ if (cliVersion !== previous.cliVersion) {
659
+ throw new Error(
660
+ `restored CLI root ${management.cli.root} has ${cliVersion}, expected ${previous.cliVersion}`,
661
+ );
662
+ }
663
+ if (previous.ompVersion !== undefined) {
664
+ const ompManifest = join(management.omp.root, "node_modules", PACKAGE, "package.json");
665
+ const ompVersion = packageVersionAt(deps, ompManifest);
666
+ if (ompVersion !== previous.ompVersion) {
667
+ throw new Error(
668
+ `restored OMP plugin root ${management.omp.root} has ${ompVersion}, expected ${previous.ompVersion}`,
669
+ );
670
+ }
671
+ }
672
+ if (previous.herdrSource !== undefined) {
673
+ if (management.herdr.root === undefined) {
674
+ throw new Error("restored Herdr plugin had no recorded root to verify");
675
+ }
676
+ const manifest = join(management.herdr.root, "herdr-plugin.toml");
677
+ if (deps.readFile(manifest).trim().length === 0) {
678
+ throw new Error(`restored Herdr plugin manifest ${manifest} is empty`);
679
+ }
680
+ }
681
+ }
682
+
458
683
  export function expectedHerdrSource(gitHead: string): string {
459
684
  return `github:${HERDR_SOURCE}@${gitHead}`;
460
685
  }
@@ -502,7 +727,7 @@ function previousHerdrInstall(source: string): readonly [string, readonly string
502
727
  * that represents them.
503
728
  *
504
729
  * Everything an upgrade or a draining restart replaces is host-wide: the
505
- * Bun-global CLI, the omp plugin, the Herdr plugin, and the one dispatch
730
+ * discovered CLI package tree, the omp plugin, the Herdr plugin, and the one
506
731
  * daemon. That daemon serves *every* configured project — the generated unit's
507
732
  * ExecStart carries no `--project` — so its restart lands on all of them at
508
733
  * once. The scope therefore names every project whose workers must drain and
@@ -1102,13 +1327,22 @@ export async function rollbackUpgrade(
1102
1327
  // Exact post-rollback identity verification decides whether absence is safe.
1103
1328
  }
1104
1329
  };
1105
-
1106
1330
  if (installTouched) {
1107
- await restore("Bun-global omp-conductor CLI", "bun", [
1108
- "add",
1109
- "-g",
1110
- `${PACKAGE}@${previous.cliVersion}`,
1111
- ]);
1331
+ if (previous.management === undefined) {
1332
+ await restore("legacy Bun-global omp-conductor CLI", "bun", [
1333
+ "add",
1334
+ "-g",
1335
+ `${PACKAGE}@${previous.cliVersion}`,
1336
+ ]);
1337
+ } else {
1338
+ await restore(`omp-conductor CLI at ${previous.management.cli.root}`, "bun", [
1339
+ "add",
1340
+ "--cwd",
1341
+ previous.management.cli.root,
1342
+ "--exact",
1343
+ `${PACKAGE}@${previous.cliVersion}`,
1344
+ ]);
1345
+ }
1112
1346
  if (previous.ompVersion === undefined) {
1113
1347
  await removeIfPresent("remove newly installed omp plugin", "omp", [
1114
1348
  "plugin",
@@ -1192,6 +1426,7 @@ export async function rollbackUpgrade(
1192
1426
 
1193
1427
  if (installTouched) {
1194
1428
  try {
1429
+ verifyRestoredManagement(deps, previous);
1195
1430
  const restored = await inspectSurfaces(deps);
1196
1431
  if (
1197
1432
  restored.cliVersion !== previous.cliVersion ||
@@ -1272,6 +1507,19 @@ export async function upgradeConductor(
1272
1507
  // already passed (#908). Everything below reads the same pair either way.
1273
1508
  const identity = await resolveIdentity(deps, options);
1274
1509
  const release = { version: identity.version, gitHead: identity.gitHead };
1510
+ const requestedProjects = (deps.env["OMP_CONDUCTOR_INSTALL_PROJECTS"] ?? "")
1511
+ .split(",")
1512
+ .filter((name) => name.length > 0);
1513
+ const requestedHolder = deps.env["OMP_CONDUCTOR_INSTALL_HOLDER"];
1514
+ const installHolder =
1515
+ requestedHolder === "human" || requestedHolder === "orchestrator"
1516
+ ? requestedHolder
1517
+ : undefined;
1518
+ const installProjects: readonly (string | undefined)[] =
1519
+ requestedProjects.length === 0 ? scope.selectors : requestedProjects;
1520
+ const installAttribution =
1521
+ `Host-global install holder: ${installHolder ?? "unknown"}; affected projects: ` +
1522
+ `${installProjects.map((selector) => selector ?? "(default)").join(", ")}.`;
1275
1523
  if (identity.bootstrap) {
1276
1524
  // The bootstrap's evidence lands in the journal before the first surface
1277
1525
  // moves, so an interrupted bootstrap leaves a record of what was verified
@@ -1297,15 +1545,24 @@ export async function upgradeConductor(
1297
1545
  version: release.version,
1298
1546
  gitHead: release.gitHead,
1299
1547
  unit: deps.env["OMP_CONDUCTOR_UNIT"],
1548
+ selectors: scope.selectors.map((selector) => selector ?? null),
1549
+ ...(requestedProjects.length === 0 ? {} : { installProjects: requestedProjects }),
1550
+ ...(installHolder === undefined ? {} : { installHolder }),
1300
1551
  });
1301
1552
  }
1302
1553
  const initial = deps.layers(scope.pauseKey);
1303
- const surfaces = await inspectSurfaces(deps);
1554
+ const observedSurfaces = await inspectSurfaces(deps);
1304
1555
  const briefs: ScopedBrief[] = scope.selectors.map((selector) => ({
1305
1556
  selector,
1306
1557
  ...deps.brief(selector),
1307
1558
  }));
1308
- const installNeeded = !surfacesCurrent(surfaces, release.version, release.gitHead);
1559
+ const installNeeded = !surfacesCurrent(observedSurfaces, release.version, release.gitHead);
1560
+ const management = installNeeded ? await discoverInstallManagement(deps) : undefined;
1561
+ if (management !== undefined) await preflightInstall(deps, management, identity.packageSpec);
1562
+ const surfaces: InstalledSurfaces = {
1563
+ ...observedSurfaces,
1564
+ ...(management === undefined ? {} : { management }),
1565
+ };
1309
1566
  // The host-unit baseline, read while the OLD package is still the one
1310
1567
  // rendering (#905). A destination already differing from the old render was
1311
1568
  // edited outside conductor, so the reconcile after the install must not
@@ -1404,6 +1661,7 @@ export async function upgradeConductor(
1404
1661
  cliVersion: surfaces.cliVersion,
1405
1662
  ompVersion: surfaces.ompVersion,
1406
1663
  herdrSource: surfaces.herdrSource,
1664
+ ...(surfaces.management === undefined ? {} : { management: surfaces.management }),
1407
1665
  },
1408
1666
  ...(preUpgradeBackup === undefined ? {} : { configBackup: preUpgradeBackup }),
1409
1667
  pauseKey: scope.pauseKey,
@@ -1453,9 +1711,18 @@ export async function upgradeConductor(
1453
1711
  const reloadStarted = Date.now();
1454
1712
  try {
1455
1713
  if (installNeeded) {
1714
+ if (management === undefined) {
1715
+ throw new Error("upgrade invariant failed: install needed without discovered surface management");
1716
+ }
1456
1717
  installTouched = true;
1457
- deps.log(`install 1/3: Bun-global omp-conductor CLI → ${release.version}`);
1458
- await mustRun(deps, "bun", ["add", "-g", identity.packageSpec]);
1718
+ deps.log(`install 1/3: omp-conductor CLI at ${management.cli.root} → ${release.version}`);
1719
+ await mustRun(deps, "bun", [
1720
+ "add",
1721
+ "--cwd",
1722
+ management.cli.root,
1723
+ "--exact",
1724
+ identity.packageSpec,
1725
+ ]);
1459
1726
  journal({ kind: "phase", phase: "install", surface: "cli", ok: true, version: release.version, gitHead: release.gitHead });
1460
1727
  deps.log(`install 2/3: omp plugin omp-conductor → ${release.version}`);
1461
1728
  await mustRun(deps, "omp", ["plugin", "install", identity.packageSpec]);
@@ -1641,10 +1908,11 @@ export async function upgradeConductor(
1641
1908
  journal({ kind: "outcome", phase: "rolled-back", ok: false, version: release.version, detail: failure });
1642
1909
  enqueueUpgradeReport(
1643
1910
  "tier2",
1644
- reportProject(scope.selectors),
1911
+ reportProject(installProjects),
1645
1912
  `fleet upgrade to omp-conductor@${release.version} failed and was rolled back`,
1646
1913
  [
1647
1914
  `Reason: ${failure}`,
1915
+ installAttribution,
1648
1916
  `Restored: cli=${surfaces.cliVersion}, omp=${surfaces.ompVersion ?? "missing"}, ` +
1649
1917
  `herdr=${surfaces.herdrSource ?? "missing"}`,
1650
1918
  ...(preUpgradeBackup === undefined ? [] : [`Pre-upgrade config snapshot: ${preUpgradeBackup}`]),
@@ -1657,10 +1925,11 @@ export async function upgradeConductor(
1657
1925
  journal({ kind: "outcome", phase: "rollback-failed", ok: false, version: release.version, detail: rollback });
1658
1926
  enqueueUpgradeReport(
1659
1927
  "tier2",
1660
- reportProject(scope.selectors),
1928
+ reportProject(installProjects),
1661
1929
  `upgrade to omp-conductor@${release.version} failed and ITS ROLLBACK ALSO FAILED`,
1662
1930
  [
1663
1931
  `Failure: ${failure}`,
1932
+ installAttribution,
1664
1933
  `Rollback failure: ${rollback}`,
1665
1934
  "The fleet may be at mixed versions; do not resume dispatch.",
1666
1935
  "Restore manually from the journal at " + upgradeJournalPath(stateDir()) + ".",
@@ -1766,6 +2035,9 @@ export async function rollbackFromJournal(
1766
2035
  cliVersion: snapshot.previous.cliVersion,
1767
2036
  ompVersion: snapshot.previous.ompVersion,
1768
2037
  herdrSource: snapshot.previous.herdrSource,
2038
+ ...(snapshot.previous.management === undefined
2039
+ ? {}
2040
+ : { management: snapshot.previous.management }),
1769
2041
  };
1770
2042
  let configBefore: string | undefined;
1771
2043
  if (request.configBackup !== undefined && existsSync(request.configBackup)) {
@@ -1813,10 +2085,17 @@ export async function rollbackFromJournal(
1813
2085
  });
1814
2086
  enqueueUpgradeReport(
1815
2087
  "tier2",
1816
- reportProject(request.selectors),
2088
+ reportProject(request.installProjects ?? request.selectors),
1817
2089
  `fleet restored to omp-conductor@${restoredVersion} after the failed upgrade to ${request.version}`,
1818
2090
  [
1819
2091
  "The transient rollback unit restored every surface from the durable snapshot;",
2092
+ `Host-global install holder: ${request.installHolder ?? "unknown"}; affected projects: ${
2093
+ (request.installProjects ?? request.selectors).length === 0
2094
+ ? "all configured projects"
2095
+ : (request.installProjects ?? request.selectors)
2096
+ .map((selector) => selector ?? "(default)")
2097
+ .join(", ")
2098
+ }.`,
1820
2099
  "dispatch stays paused until the first tick after these restarts verifies the old version.",
1821
2100
  `Pre-upgrade config snapshot: ${request.configBackup ?? "none recorded"}`,
1822
2101
  ],
@@ -1008,7 +1008,12 @@ export function githubVerbActions(
1008
1008
  return { code: ran.ok ? 0 : 1, stdout: ran.stdout, stderr: ran.stderr };
1009
1009
  },
1010
1010
  env(),
1011
- { kind: "install", version: execution.version },
1011
+ {
1012
+ kind: "install",
1013
+ version: execution.version,
1014
+ holder: execution.holder,
1015
+ projects: execution.projects,
1016
+ },
1012
1017
  );
1013
1018
  if (!launched.ok) return { ok: false, stderr: launched.stderr };
1014
1019
  return { ok: true, detail: launched.unit };