@hybridlabor-api/aos 4.15.0 → 4.17.0

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.
Files changed (104) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude/hooks/env-file-protection.mjs +19 -1
  3. package/.claude/hooks/go-gate.mjs +787 -68
  4. package/.claude/hooks/go-grant.mjs +88 -0
  5. package/.claude/settings.json +8 -0
  6. package/.codex-plugin/plugin.json +36 -6
  7. package/.opencode/commands/bdb-aos-brainstorm.md +5 -0
  8. package/.opencode/commands/bdb-aos-doctor.md +5 -0
  9. package/.opencode/commands/bdb-aos-graph.md +5 -0
  10. package/.opencode/commands/bdb-aos-init.md +5 -0
  11. package/.opencode/commands/bdb-aos-loop.md +5 -0
  12. package/.opencode/commands/bdb-aos-mastersession.md +5 -0
  13. package/.opencode/commands/bdb-aos-memb.md +5 -0
  14. package/.opencode/commands/bdb-aos-orchestrator.md +5 -0
  15. package/.opencode/commands/bdb-aos-plan.md +5 -0
  16. package/.opencode/commands/bdb-aos-playbooks.md +5 -0
  17. package/.opencode/commands/bdb-aos-setup.md +5 -0
  18. package/.opencode/commands/bdb-aos-shipping.md +5 -0
  19. package/.opencode/commands/bdb-aos-startproject.md +5 -0
  20. package/.opencode/commands/bdb-aos-store.md +5 -0
  21. package/.opencode/plugins/bdb-aos.js +54 -10
  22. package/README.md +4 -4
  23. package/agy-commands/loop.md +5 -0
  24. package/bin/aos-acp.mjs +7 -3
  25. package/bin/aos-doctor.mjs +26 -9
  26. package/bin/aos-uninstall.mjs +25 -1
  27. package/commands/brainstorm.md +5 -0
  28. package/commands/doctor.md +5 -0
  29. package/commands/graph.md +5 -0
  30. package/commands/init.md +5 -0
  31. package/commands/loop.md +5 -0
  32. package/commands/mastersession.md +5 -0
  33. package/commands/memb.md +5 -0
  34. package/commands/orchestrator.md +5 -0
  35. package/commands/plan.md +5 -0
  36. package/commands/playbooks.md +5 -0
  37. package/commands/setup.md +5 -0
  38. package/commands/shipping.md +5 -0
  39. package/commands/startproject.md +5 -0
  40. package/commands/store.md +5 -0
  41. package/docs/codex-agy-setup.md +16 -0
  42. package/docs/opencode-setup.md +36 -0
  43. package/docs/plugin-migration.md +61 -0
  44. package/installer.js +564 -215
  45. package/lib/plugin-migration.js +462 -0
  46. package/lib/store-ui/index.html +9 -1
  47. package/lib/store-ui/server.mjs +2 -0
  48. package/package.json +7 -2
  49. package/plugin-commands.json +144 -0
  50. package/plugin.json +302 -0
  51. package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +38 -0
  52. package/plugins/bdb-aos-codex/skills/brainstorm/SKILL.md +6 -0
  53. package/plugins/bdb-aos-codex/skills/doctor/SKILL.md +6 -0
  54. package/plugins/bdb-aos-codex/skills/graph/SKILL.md +6 -0
  55. package/plugins/bdb-aos-codex/skills/init/SKILL.md +6 -0
  56. package/plugins/bdb-aos-codex/skills/loop/SKILL.md +6 -0
  57. package/plugins/bdb-aos-codex/skills/mastersession/SKILL.md +6 -0
  58. package/plugins/bdb-aos-codex/skills/memb/SKILL.md +6 -0
  59. package/plugins/bdb-aos-codex/skills/orchestrator/SKILL.md +6 -0
  60. package/plugins/bdb-aos-codex/skills/plan/SKILL.md +6 -0
  61. package/plugins/bdb-aos-codex/skills/playbooks/SKILL.md +6 -0
  62. package/plugins/bdb-aos-codex/skills/setup/SKILL.md +6 -0
  63. package/plugins/bdb-aos-codex/skills/shipping/SKILL.md +6 -0
  64. package/plugins/bdb-aos-codex/skills/startproject/SKILL.md +6 -0
  65. package/plugins/bdb-aos-codex/skills/store/SKILL.md +6 -0
  66. package/scripts/build-plugin-manifest.mjs +202 -3
  67. package/skills/basic/master-session/SKILL.md +1 -1
  68. package/skills/bdb-aos/scripts/list-playbooks.mjs +72 -0
  69. package/skills/global_config/gogate/SKILL.md +123 -0
  70. package/skills/global_config/loop-templates/SKILL.md +27 -0
  71. package/skills/global_config/loop-templates/references/ci-until-green.md +27 -0
  72. package/skills/global_config/loop-templates/references/daily-summary.md +27 -0
  73. package/skills/global_config/loop-templates/references/pr-to-merge.md +27 -0
  74. package/skills/global_config/loop-templates/references/review-rounds.md +27 -0
  75. package/skills/playbooks/pb-bug-fix/SKILL.md +48 -0
  76. package/skills/playbooks/pb-clip-from-moodboard/SKILL.md +55 -0
  77. package/skills/playbooks/pb-crew-call-sheet/SKILL.md +40 -0
  78. package/skills/playbooks/pb-deploy-saas/SKILL.md +44 -0
  79. package/skills/playbooks/pb-docs-site/SKILL.md +44 -0
  80. package/skills/playbooks/pb-focus-chunks/SKILL.md +40 -0
  81. package/skills/playbooks/pb-handover/SKILL.md +41 -0
  82. package/skills/playbooks/pb-harness-work/SKILL.md +64 -0
  83. package/skills/playbooks/pb-health-weekly/SKILL.md +43 -0
  84. package/skills/playbooks/pb-idea-to-launch/SKILL.md +51 -0
  85. package/skills/playbooks/pb-image-to-3d/SKILL.md +46 -0
  86. package/skills/playbooks/pb-inbox-zero/SKILL.md +39 -0
  87. package/skills/playbooks/pb-invoice-check/SKILL.md +41 -0
  88. package/skills/playbooks/pb-landing-page/SKILL.md +46 -0
  89. package/skills/playbooks/pb-launch-video/SKILL.md +49 -0
  90. package/skills/playbooks/pb-machine-setup/SKILL.md +44 -0
  91. package/skills/playbooks/pb-master/SKILL.md +53 -0
  92. package/skills/playbooks/pb-newsletter/SKILL.md +43 -0
  93. package/skills/playbooks/pb-offer/SKILL.md +39 -0
  94. package/skills/playbooks/pb-open-source/SKILL.md +50 -0
  95. package/skills/playbooks/pb-pcb-to-case/SKILL.md +46 -0
  96. package/skills/playbooks/pb-redesign-app/SKILL.md +47 -0
  97. package/skills/playbooks/pb-release-aos/SKILL.md +50 -0
  98. package/skills/playbooks/pb-security-sweep/SKILL.md +48 -0
  99. package/skills/playbooks/pb-ship/SKILL.md +49 -0
  100. package/skills/playbooks/pb-show-build/SKILL.md +48 -0
  101. package/skills/playbooks/pb-social-pack/SKILL.md +47 -0
  102. package/skills/playbooks/pb-todo/SKILL.md +41 -0
  103. package/skills/playbooks/pb-worktrees-land/SKILL.md +52 -0
  104. package/.codex-plugin/marketplace.json +0 -11
package/installer.js CHANGED
@@ -23,6 +23,7 @@ const net = require('net');
23
23
  const readline = require('readline');
24
24
  const util = require('util');
25
25
  const crypto = require('crypto');
26
+ const pluginMigration = require('./lib/plugin-migration');
26
27
 
27
28
  function verifyDaemonListening(port, name, timeoutMs = 4000) {
28
29
  return new Promise((resolve) => {
@@ -497,8 +498,8 @@ const CORE_MCP = 'memb-mcp';
497
498
  // compared against: (a) the SOURCE hash (unmodified) and (b) the MANIFEST
498
499
  // hash (last-installed value). This gives us three cases:
499
500
  // ours + unmodified → source_hash == disk_hash → overwrite + update manifest
500
- // ours + user-edited → manifest_hash == source_hash,
501
- // but disk_hash != source_hash → .bak + warn + write
501
+ // ours + user-edited → disk_hash != manifest_hash → keep user file,
502
+ // write <file>.new
502
503
  // foreign → no manifest record AND disk_hash
503
504
  // not in any known source hash → skip + warn
504
505
 
@@ -560,8 +561,11 @@ function adoptFileIntoManifest(manifest, targetPath, sourceHash) {
560
561
  };
561
562
  }
562
563
 
564
+ // User-edited files this run left untouched (shipped copy written as <file>.new).
565
+ const keptUserEdits = [];
566
+
563
567
  // Manifest-aware file writer implementing the three-way conflict policy.
564
- // Returns 'wrote' | 'bak' | 'skipped'.
568
+ // Returns 'wrote' | 'bak' | 'kept' | 'skipped'.
565
569
  // knownSourceHashes: Set of sha256 hashes of all source files in the current
566
570
  // payload (used to identify files we recognise even before manifest entry).
567
571
  function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes) {
@@ -627,15 +631,13 @@ function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes
627
631
  }
628
632
 
629
633
  // Case: ours + user-edited (manifest recorded our hash, but disk now differs).
634
+ // The user's file wins: the shipped version lands next to it as .new, and
635
+ // the manifest keeps the old hash so the next run still sees the edit.
630
636
  if (manifestEntry && diskHash !== manifestEntry.sha256) {
631
- const bakPath = `${targetPath}.${timestamp}.bak`;
632
- try { fs.copyFileSync(targetPath, bakPath); } catch (e) {
633
- log.warn(`[manifest] Could not create backup ${bakPath}: ${e.message}`);
634
- }
635
- fs.copyFileSync(sourcePath, targetPath);
636
- manifest[targetPath] = { path: targetPath, sha256: sourceHash, version: pkg.version, installedAt: new Date().toISOString() };
637
- log.warn(`[manifest] User-edited file backed up to ${bakPath}, new version written.`);
638
- return 'bak';
637
+ const newPath = `${targetPath}.new`;
638
+ fs.copyFileSync(sourcePath, newPath);
639
+ keptUserEdits.push(targetPath);
640
+ return 'kept';
639
641
  }
640
642
 
641
643
  // Default: manifest entry present, disk matches manifest (no change needed) – overwrite.
@@ -765,6 +767,17 @@ function pruneRemovedSkills(manifest) {
765
767
 
766
768
  function flushSessionManifest() {
767
769
  if (_sessionManifest) saveInstallManifest(_sessionManifest);
770
+ reportKeptUserEdits();
771
+ }
772
+
773
+ function reportKeptUserEdits() {
774
+ if (keptUserEdits.length === 0) return;
775
+ log.warn([
776
+ `${keptUserEdits.length} file(s) you edited were KEPT; the shipped version is next to each as <file>.new:`,
777
+ ...keptUserEdits.map((p) => ` ${p}`),
778
+ 'Merge or delete the .new files when convenient.',
779
+ ].join('\n'));
780
+ keptUserEdits.length = 0;
768
781
  }
769
782
 
770
783
  // skills/global_legacy/ has not existed in the shipped payload for a long
@@ -1265,9 +1278,94 @@ function reportFatal(stage, e) {
1265
1278
  process.exitCode = 1;
1266
1279
  }
1267
1280
 
1281
+ // AOS_PLUGIN_MIGRATION=off skips plugin registration and loose-copy removal; =check only reports.
1282
+ function pluginMigrationMode(argv = process.argv, env = process.env) {
1283
+ const flag = argv.find((a) => a.startsWith('--plugin-migration='));
1284
+ const raw = String(flag ? flag.slice('--plugin-migration='.length) : (env.AOS_PLUGIN_MIGRATION || '')).trim().toLowerCase();
1285
+ if (raw === 'off' || raw === '0' || raw === 'false') return 'off';
1286
+ return raw === 'check' || DRY_RUN ? 'check' : 'on';
1287
+ }
1288
+
1289
+ // .agents/plugins holds the Codex marketplace file; it belongs to the repo checkout, not to ~/.agents or a project.
1290
+ const AGENTS_COPY_EXCLUDE = ['plugins'];
1291
+
1292
+ let _pluginMigration = null;
1293
+ function runPluginMigration({ targetHome = homeDir, detected = null, mode = pluginMigrationMode(), registrars } = {}) {
1294
+ if (_pluginMigration && targetHome === homeDir) return _pluginMigration;
1295
+ const keys = detected || detectPlatforms().map((d) => d.key);
1296
+ const ownsManifest = !_sessionManifest;
1297
+ const manifest = _sessionManifest || loadInstallManifest();
1298
+ let result;
1299
+ try {
1300
+ result = pluginMigration.migrate({ home: targetHome, manifest, detected: keys, mode, ...(registrars ? { registrars } : {}) });
1301
+ } catch (e) {
1302
+ log.warn(`Plugin migration skipped: ${e.message}`);
1303
+ result = { covered: new Set(), lines: [] };
1304
+ }
1305
+ for (const line of result.lines) log.step(line);
1306
+ if (ownsManifest && mode === 'on') saveInstallManifest(manifest);
1307
+ if (targetHome === homeDir) _pluginMigration = result;
1308
+ return result;
1309
+ }
1310
+
1311
+ // The plugin migration runs first: when it retires Claude's loose copies, ~/.claude/skills is not
1312
+ // written again, so a rerun neither recreates nor re-backs-up what the plugin now delivers.
1313
+ function installTargetSkills(targets, { mode, backupDir, excludeSkills, skillsBase }) {
1314
+ const detected = detectPlatforms().map((d) => d.key);
1315
+ if (targets.some((t) => t.value === '2') && !detected.includes('claudecode')) detected.push('claudecode');
1316
+ const pluginCovered = runPluginMigration({ detected }).covered;
1317
+
1318
+ for (const t of targets) {
1319
+ const viaPlugin = t.value === '2' && pluginCovered.has('claudecode');
1320
+ if (mode === 'replace') {
1321
+ if (!viaPlugin) {
1322
+ moveIfExists(t.targetSkillDir, path.join(backupDir, `config_skills_backup_${t.value}`), `global config skills (${t.value})`);
1323
+ moveIfExists(t.targetLegacyDir, path.join(backupDir, `legacy_skills_backup_${t.value}`), `legacy skills (${t.value})`);
1324
+ }
1325
+ moveIfExists(t.targetWorkspaceDir, path.join(backupDir, `workspace_skills_backup_${t.value}`), `workspace skills (${t.value})`);
1326
+ }
1327
+
1328
+ installStep(`create the skill target directories (${t.value})`, () => {
1329
+ if (!viaPlugin) {
1330
+ fs.mkdirSync(t.targetSkillDir, { recursive: true });
1331
+ retireObsoleteLegacyDir(t.targetLegacyDir);
1332
+ }
1333
+ fs.mkdirSync(t.targetWorkspaceDir, { recursive: true });
1334
+ }, 'The skill copies below will most likely be skipped as well.');
1335
+
1336
+ if (fs.existsSync(skillsBase)) {
1337
+ installStep(`install the skills (${t.value})`, () => {
1338
+ const rawDirs = fs.readdirSync(skillsBase);
1339
+ const dirs = rawDirs.sort((a, b) => {
1340
+ const aIsLeaf = fs.existsSync(path.join(skillsBase, a, 'SKILL.md'));
1341
+ const bIsLeaf = fs.existsSync(path.join(skillsBase, b, 'SKILL.md'));
1342
+ if (aIsLeaf && !bIsLeaf) return 1;
1343
+ if (!aIsLeaf && bIsLeaf) return -1;
1344
+ return 0;
1345
+ });
1346
+ for (const dir of dirs) {
1347
+ const fullPath = path.join(skillsBase, dir);
1348
+ if (!fs.statSync(fullPath).isDirectory()) continue;
1349
+
1350
+ if (viaPlugin && dir !== 'workspace_agents') continue;
1351
+ if (dir === 'global_legacy') {
1352
+ copyDirRecursiveSync(fullPath, t.targetLegacyDir, excludeSkills);
1353
+ } else if (dir === 'workspace_agents') {
1354
+ copyDirRecursiveSync(fullPath, t.targetWorkspaceDir, excludeSkills);
1355
+ } else {
1356
+ syncSkillEntry(fullPath, dir, t.targetSkillDir, excludeSkills);
1357
+ }
1358
+ }
1359
+ log.step(`Installed all global config & core skills to ${t.targetSkillDir}`);
1360
+ }, 'The skills are missing or incomplete; the rest of the installation continues.');
1361
+ }
1362
+ }
1363
+ }
1364
+
1268
1365
  function syncSkillsToGlobalHarnesses(excludeSkills = []) {
1269
1366
  const skillsBase = path.join(srcDir, 'skills');
1270
1367
  if (!fs.existsSync(skillsBase)) return;
1368
+ const pluginCovered = runPluginMigration().covered;
1271
1369
 
1272
1370
  // Mirror only into harnesses that are actually present. This list used to
1273
1371
  // be unconditional, which both wrote skills nobody would read and planted
@@ -1288,7 +1386,7 @@ function syncSkillsToGlobalHarnesses(excludeSkills = []) {
1288
1386
  { dir: path.join(homeDir, '.cursor', 'skills'), key: 'cursor' },
1289
1387
  { dir: path.join(homeDir, '.roo', 'skills'), key: 'vscode' },
1290
1388
  { dir: process.platform === 'win32' ? path.join(process.env.APPDATA || homeDir, 'opencode', 'skills') : path.join(homeDir, '.config', 'opencode', 'skills'), key: 'opencode' },
1291
- ].filter((d) => d.key === null || detectedKeys.has(d.key));
1389
+ ].filter((d) => d.key === null || (detectedKeys.has(d.key) && !pluginCovered.has(d.key)));
1292
1390
 
1293
1391
  for (const { dir: dest } of extraSkillDestinations) {
1294
1392
  try {
@@ -1537,6 +1635,17 @@ async function promptCredentials(referenceMcpDir) {
1537
1635
 
1538
1636
  const DAEMON_LOGON_FALLBACK_EXIT_CODE = 10;
1539
1637
 
1638
+ // The daemon job points at these scripts for good, so they live in the installer-owned shared store
1639
+ // (~/.agents/skills), which the plugin migration never retires, not in ~/.claude/skills.
1640
+ function stableOpenWikiScripts() {
1641
+ const dest = path.join(homeDir, '.agents', 'skills', 'openwiki-skill');
1642
+ if (!fs.existsSync(path.join(dest, 'scripts', 'install_daemon.sh'))) {
1643
+ const src = path.join(srcDir, 'skills', 'global_config', 'openwiki-skill');
1644
+ if (fs.existsSync(src)) copyDirRecursiveSync(src, dest);
1645
+ }
1646
+ return path.join(dest, 'scripts');
1647
+ }
1648
+
1540
1649
  async function installOpenWikiDaemon(apiKey, targetSkillDir, openwikiEnv = {}) {
1541
1650
  const prov = openwikiEnv.provider || "google";
1542
1651
  if (!apiKey && !["ollama", "lmstudio"].includes(prov)) {
@@ -1550,7 +1659,7 @@ async function installOpenWikiDaemon(apiKey, targetSkillDir, openwikiEnv = {}) {
1550
1659
  const s = spinner();
1551
1660
  s.start('Installing OpenWiki Daemon...');
1552
1661
 
1553
- const scriptBase = path.join(targetSkillDir, 'openwiki-skill', 'scripts');
1662
+ const scriptBase = stableOpenWikiScripts();
1554
1663
 
1555
1664
  const daemonEnv = Object.assign({}, process.env, {
1556
1665
  OPENWIKI_PROVIDER: prov,
@@ -2504,118 +2613,283 @@ function aoSupportedHere() {
2504
2613
  return ['darwin', 'linux', 'win32'].includes(process.platform);
2505
2614
  }
2506
2615
 
2507
- async function installOSAgentWorkspace() {
2508
- if (!aoSupportedHere()) {
2509
- log.warn(`AO is not supported on ${process.platform}/${process.arch}.`);
2510
- return false;
2511
- }
2616
+ const AO_PKG = '@hybridlabor-api/bdb-agent-orchestrator';
2617
+ // `ao service install` restarts the daemon; it can take well over 8s to bind.
2618
+ const AO_DAEMON_WAIT_MS = 30000;
2512
2619
 
2513
- const isWin = process.platform === 'win32';
2514
- const isMac = process.platform === 'darwin';
2515
- const isLinux = process.platform === 'linux';
2620
+ function aoBinTarget() {
2621
+ return process.platform === 'win32'
2622
+ ? path.join(process.env.LOCALAPPDATA || homeDir, 'Programs', 'ao', 'ao.exe')
2623
+ : path.join(homeDir, '.local', 'bin', 'ao');
2624
+ }
2516
2625
 
2517
- // Discover AO directory: check local development checkouts first, then moduleBasePath
2518
- const candidateDirs = [
2519
- path.join(homeDir, 'dev', 'agents', 'bdb-agent-orchestrator'),
2520
- path.join(homeDir, 'dev', 'bdb-dev', 'bdb-agent-orchestrator'),
2521
- path.join(moduleBasePath(), 'bdb-agent-orchestrator')
2522
- ];
2523
- const osAgentDir = candidateDirs.find(d => fs.existsSync(d)) || candidateDirs[candidateDirs.length - 1];
2524
- const isLocalGitRepo = fs.existsSync(path.join(osAgentDir, '.git'));
2626
+ // Go links the `go version -m` text between these two 16-byte markers, so the
2627
+ // revision of a binary can be read from its bytes: no Go toolchain needed, and
2628
+ // the binary (possibly the live daemon) is never executed.
2629
+ const GO_BUILDINFO_START = Buffer.from('3077af0c9274080241e1c107e6d618e6', 'hex');
2630
+ const GO_BUILDINFO_END = Buffer.from('f932433186182072008242104116d8f2', 'hex');
2631
+
2632
+ function readGoBuildInfo(binPath) {
2633
+ let buf;
2634
+ try { buf = fs.readFileSync(binPath); } catch { return null; }
2635
+ const start = buf.indexOf(GO_BUILDINFO_START);
2636
+ const end = start < 0 ? -1 : buf.indexOf(GO_BUILDINFO_END, start);
2637
+ return end < 0 ? null : parseGoBuildInfo(buf.subarray(start + GO_BUILDINFO_START.length, end).toString('utf8'));
2638
+ }
2525
2639
 
2526
- if (isLocalGitRepo) {
2527
- log.step(`Using local development workspace for AO: ${osAgentDir}`);
2528
- } else {
2529
- if (!downloadOrUpdateModule('@hybridlabor-api/bdb-agent-orchestrator', osAgentDir, 'BDB Agent Orchestrator')) {
2530
- log.warn('Skipping AO setup: the module could not be downloaded.');
2531
- return false;
2532
- }
2640
+ // Accepts the embedded text or `go version -m` output (same lines, tab-indented).
2641
+ function parseGoBuildInfo(text) {
2642
+ const get = (key) => (String(text).match(new RegExp(`^\\s*build\\s+${key}=(\\S*)`, 'm')) || [])[1];
2643
+ const revision = get('vcs\\.revision');
2644
+ if (!revision) return null;
2645
+ return { revision, time: get('vcs\\.time') || null, modified: get('vcs\\.modified') === 'true' };
2646
+ }
2647
+
2648
+ // Never trade the installed ao for a build that is not provably newer and clean.
2649
+ // allowDirty: the AOS_AO_DEV_BUILD opt-in. daemonRunning/candidateIsRelease
2650
+ // only matter when the installed binary's revision is unreadable: a clean
2651
+ // published release may then replace it, but not while some daemon of unknown
2652
+ // build is running.
2653
+ function decideAoInstall({ installedExists, installed, candidate, allowDirty = false, candidateIsRelease = false, daemonRunning = true }) {
2654
+ if (!installedExists) return { install: true, reason: 'no ao installed yet' };
2655
+ if (!candidate) return { install: false, reason: 'the candidate build has no readable revision' };
2656
+ if (candidate.modified && !allowDirty) return { install: false, reason: `the candidate ${candidate.revision.slice(0, 9)} is a dirty build (vcs.modified=true)` };
2657
+ if (!installed) {
2658
+ if (candidateIsRelease && !candidate.modified && daemonRunning === false) {
2659
+ return { install: true, reason: `the installed binary's revision is unreadable and no daemon is running; installing the clean release ${candidate.revision.slice(0, 9)}` };
2660
+ }
2661
+ return { install: false, reason: 'the installed binary\'s revision is unreadable, so a downgrade cannot be ruled out' };
2662
+ }
2663
+ if (candidate.revision === installed.revision) return { install: false, reason: `already at ${installed.revision.slice(0, 9)}` };
2664
+ const c = Date.parse(candidate.time || '');
2665
+ const i = Date.parse(installed.time || '');
2666
+ if (!(c > i)) return { install: false, reason: `the candidate ${candidate.revision.slice(0, 9)} (${candidate.time || 'no date'}) is not newer than the installed ${installed.revision.slice(0, 9)} (${installed.time || 'no date'})` };
2667
+ return { install: true, reason: `${installed.revision.slice(0, 9)} -> ${candidate.revision.slice(0, 9)}` };
2668
+ }
2669
+
2670
+ // A dev checkout is a release source only when clean and HEAD sits on a tag.
2671
+ function checkAoWorkspace(dir, git = (args) => execFileSync('git', ['--no-optional-locks', '-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()) {
2672
+ try {
2673
+ if (git(['status', '--porcelain'])) return { ok: false, reason: 'it has uncommitted or untracked changes' };
2674
+ let tag;
2675
+ try { tag = git(['describe', '--tags', '--exact-match', 'HEAD']); } catch { return { ok: false, reason: 'HEAD is not on a release tag' }; }
2676
+ return { ok: true, tag, head: git(['rev-parse', 'HEAD']) };
2677
+ } catch (e) {
2678
+ return { ok: false, reason: `git could not read it (${String(e.message).split('\n')[0]})` };
2533
2679
  }
2534
- if (DRY_RUN) {
2535
- log.step('[dry-run] would link ao binary and run `ao service install`');
2536
- return;
2680
+ }
2681
+
2682
+ // Builds into a temp dir: a build left in the checkout's gitignored backend/bin
2683
+ // outlives every later pull and gets picked up as if it were current.
2684
+ function buildAoFromSource(dir) {
2685
+ if (!hasExecutable('go') || !fs.existsSync(path.join(dir, 'backend', 'cmd', 'ao'))) return null;
2686
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aos-ao-build-')), process.platform === 'win32' ? 'ao.exe' : 'ao');
2687
+ const s = spinner();
2688
+ s.start('Compiling AO daemon binary from Go source...');
2689
+ try {
2690
+ execFileSync('go', ['build', '-ldflags=-s -w', '-o', out, './cmd/ao'], { cwd: path.join(dir, 'backend'), stdio: 'ignore' });
2691
+ s.stop('Compiled AO daemon binary successfully.');
2692
+ return out;
2693
+ } catch (e) {
2694
+ s.stop(`Failed to compile AO from source: ${e.message}`);
2695
+ return null;
2537
2696
  }
2697
+ }
2538
2698
 
2539
- const localBinDir = isWin
2540
- ? path.join(process.env.LOCALAPPDATA || homeDir, 'Programs', 'ao')
2541
- : path.join(homeDir, '.local', 'bin');
2542
- const binTarget = path.join(localBinDir, isWin ? 'ao.exe' : 'ao');
2699
+ // Installed version = the release version the installer recorded for exactly
2700
+ // the revision on disk. Never the dev workspace's package.json.
2701
+ function trustedAoVersion(build, record) {
2702
+ return build && !build.modified && record && record.version && record.revision === build.revision ? record.version : null;
2703
+ }
2543
2704
 
2544
- // Platform-specific binary lookup
2545
- const candidateBinaries = [];
2546
- if (isWin) {
2547
- candidateBinaries.push(
2548
- path.join(osAgentDir, 'backend', 'bin', 'ao-windows-amd64.exe'),
2549
- path.join(osAgentDir, 'backend', 'bin', 'ao.exe'),
2550
- path.join(osAgentDir, 'backend', 'ao.exe')
2551
- );
2552
- } else if (isMac) {
2553
- if (process.arch === 'arm64') {
2554
- candidateBinaries.push(path.join(osAgentDir, 'backend', 'bin', 'ao-darwin-arm64'));
2555
- } else {
2556
- candidateBinaries.push(path.join(osAgentDir, 'backend', 'bin', 'ao-darwin-amd64'));
2557
- }
2558
- candidateBinaries.push(
2559
- path.join(osAgentDir, 'backend', 'ao-daemon'),
2560
- path.join(osAgentDir, 'backend', 'bin', 'ao'),
2561
- path.join(osAgentDir, 'backend', 'ao')
2562
- );
2563
- } else if (isLinux) {
2564
- if (process.arch === 'arm64') {
2565
- candidateBinaries.push(path.join(osAgentDir, 'backend', 'bin', 'ao-linux-arm64'));
2566
- } else {
2567
- candidateBinaries.push(path.join(osAgentDir, 'backend', 'bin', 'ao-linux-amd64'));
2568
- }
2569
- candidateBinaries.push(
2570
- path.join(osAgentDir, 'backend', 'ao-daemon'),
2571
- path.join(osAgentDir, 'backend', 'bin', 'ao'),
2572
- path.join(osAgentDir, 'backend', 'ao')
2573
- );
2705
+ function describeAoVersion({ exists, build, record, latest }) {
2706
+ if (!exists) return { text: 'Not installed', updateAvailable: false };
2707
+ if (!build) return { text: 'Installed (build revision unreadable, version unknown)', updateAvailable: false };
2708
+ const rev = build.revision.slice(0, 9);
2709
+ if (build.modified) return { text: `Dev build ${rev} with uncommitted changes (not a release, version not comparable)`, updateAvailable: false };
2710
+ const version = trustedAoVersion(build, record);
2711
+ if (!version) return { text: `Build ${rev} (release version unknown)`, updateAvailable: false };
2712
+ if (latest && isNewerVersion(version, latest)) return { text: `Update available (v${version} ➔ v${latest})`, updateAvailable: true };
2713
+ return { text: `v${version} (Up to date)`, updateAvailable: false };
2714
+ }
2715
+
2716
+ // The daemon's running.json (AO backend/internal/runfile: pid, port, startedAt).
2717
+ // pid+startedAt tells a restarted daemon from the old one still on :3101.
2718
+ function readAoDaemonIdentity(file = process.env.AO_RUN_FILE || path.join(homeDir, '.ao', 'running.json')) {
2719
+ const data = readJsonFile(file);
2720
+ if (!data || data.pid == null) return null;
2721
+ return data.startedAt ? `${data.pid}@${data.startedAt}` : String(data.pid);
2722
+ }
2723
+
2724
+ async function waitUntil(check, timeoutMs, pollMs = 300) {
2725
+ const deadline = Date.now() + timeoutMs;
2726
+ for (;;) {
2727
+ if (check()) return true;
2728
+ if (Date.now() >= deadline) return false;
2729
+ await new Promise((r) => setTimeout(r, pollMs));
2574
2730
  }
2575
- let daemonBin = candidateBinaries.find(p => fs.existsSync(p));
2731
+ }
2576
2732
 
2577
- // Fallback: compile from Go source if Go is installed and binary is not pre-built
2578
- if (!daemonBin && hasExecutable('go') && fs.existsSync(path.join(osAgentDir, 'backend', 'cmd', 'ao'))) {
2579
- const s = spinner();
2580
- s.start('Compiling AO daemon binary from Go source...');
2733
+ // Back up, swap atomically, register the service, wait for a NEW daemon on
2734
+ // :3101. A failed swap or service install puts the previous binary back.
2735
+ async function installAoBinary({ src, dest, stamp = timestamp, serviceInstall, waitForDaemon, readIdentity = readAoDaemonIdentity, restartWaitMs = AO_DAEMON_WAIT_MS }) {
2736
+ let backup = null;
2737
+ if (src) {
2738
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
2739
+ if (fs.existsSync(dest)) {
2740
+ backup = `${dest}.bak-${stamp}`;
2741
+ fs.copyFileSync(dest, backup);
2742
+ }
2581
2743
  try {
2582
- const buildTarget = path.join(osAgentDir, 'backend', 'bin', isWin ? 'ao.exe' : 'ao');
2583
- fs.mkdirSync(path.dirname(buildTarget), { recursive: true });
2584
- execFileSync('go', ['build', '-ldflags=-s -w', '-o', buildTarget, './cmd/ao'], {
2585
- cwd: path.join(osAgentDir, 'backend'),
2586
- stdio: 'ignore'
2587
- });
2588
- daemonBin = buildTarget;
2589
- s.stop('Compiled AO daemon binary successfully.');
2744
+ installBinaryAtomically(src, dest);
2590
2745
  } catch (e) {
2591
- s.stop(`Failed to compile AO from source: ${e.message}`);
2746
+ if (backup) installBinaryAtomically(backup, dest);
2747
+ log.warn(`Could not place the ao binary (${e.message})${backup ? '; the previous binary is back in place' : ''}.`);
2748
+ return { ok: false, backup, restored: !!backup };
2592
2749
  }
2750
+ log.step(`Installed ao to ${dest}${backup ? ` (previous binary kept at ${backup})` : ''}`);
2751
+ }
2752
+ const before = readIdentity();
2753
+ try {
2754
+ await serviceInstall(dest);
2755
+ } catch (e) {
2756
+ if (backup) {
2757
+ installBinaryAtomically(backup, dest);
2758
+ try { await serviceInstall(dest); } catch (e2) { logDebug(e2, 'ao service install after restore'); }
2759
+ }
2760
+ log.warn(`AO service install failed: ${e.message}${backup ? `. Restored the previous ao from ${backup}` : ''}. Start it by hand with: ao service install`);
2761
+ return { ok: false, backup, restored: !!backup };
2762
+ }
2763
+ // No identity before (file missing/unreadable): the port check is all we have.
2764
+ const restarted = before ? await waitUntil(() => { const now = readIdentity(); return !!now && now !== before; }, restartWaitMs) : true;
2765
+ const listening = await waitForDaemon();
2766
+ const backupNote = backup ? ` The previous binary is at ${backup} (restore: mv "${backup}" "${dest}" && ao service install).` : '';
2767
+ if (listening && restarted) {
2768
+ log.success('AO Orchestrator running on http://localhost:3101');
2769
+ } else if (listening) {
2770
+ log.warn(`AO on :3101 is still the old process (${before}); it was not restarted, probably because it does not run under the AO service. Restart it: ao service install, then check ao service status (or stop it and start it the way it was started).${backupNote}`);
2771
+ } else {
2772
+ const hint = process.platform === 'win32'
2773
+ ? 'Run `ao service install` again or log out and back in, then check `ao service status`.'
2774
+ : 'Check `ao service status` and `ao service logs`.';
2775
+ log.warn(`AO did not answer on :3101 within ${Math.round(AO_DAEMON_WAIT_MS / 1000)}s. ${hint}${backupNote}`);
2593
2776
  }
2777
+ return { ok: true, backup, listening, restarted };
2778
+ }
2594
2779
 
2595
- // Fallback: if already installed and functional, keep it
2596
- if (!daemonBin && fs.existsSync(binTarget)) {
2597
- try {
2598
- execFileSync(binTarget, ['--version'], { stdio: 'ignore' });
2599
- log.step(`Keeping existing working AO binary at ${binTarget}`);
2600
- daemonBin = binTarget;
2601
- } catch (_) {}
2780
+ function aoPlatformBinaries(dir) {
2781
+ const b = (...p) => path.join(dir, 'backend', ...p);
2782
+ if (process.platform === 'win32') return [b('bin', 'ao-windows-amd64.exe'), b('bin', 'ao.exe'), b('ao.exe')];
2783
+ const plat = process.platform === 'darwin' ? 'darwin' : 'linux';
2784
+ const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
2785
+ return [b('bin', `ao-${plat}-${arch}`), b('ao-daemon'), b('bin', 'ao'), b('ao')];
2786
+ }
2787
+
2788
+ // Picks the binary this run may install. A local checkout counts only when it
2789
+ // is a release source (clean, HEAD on a tag) or AOS_AO_DEV_BUILD=1 opts in;
2790
+ // otherwise the published release package is used, never the dev checkout.
2791
+ function selectAoCandidate({
2792
+ checkoutDir, isLocalGitRepo, releaseDir,
2793
+ devOptIn = process.env.AOS_AO_DEV_BUILD === '1',
2794
+ dryRun = DRY_RUN,
2795
+ checkWorkspace = checkAoWorkspace,
2796
+ download = (dir) => downloadOrUpdateModule(AO_PKG, dir, 'BDB Agent Orchestrator'),
2797
+ build = buildAoFromSource,
2798
+ candidatesIn = aoPlatformBinaries,
2799
+ }) {
2800
+ if (isLocalGitRepo) {
2801
+ const ws = checkWorkspace(checkoutDir);
2802
+ if (ws.ok) {
2803
+ if (dryRun) return { bin: null, source: 'dry-run' };
2804
+ // A prebuilt binary counts only when it was built clean from this exact HEAD.
2805
+ const prebuilt = candidatesIn(checkoutDir).find((p) => {
2806
+ const info = readGoBuildInfo(p);
2807
+ return info && !info.modified && info.revision === ws.head;
2808
+ });
2809
+ return { bin: prebuilt || build(checkoutDir), source: 'checkout' };
2810
+ }
2811
+ if (devOptIn) {
2812
+ log.warn(`AOS_AO_DEV_BUILD=1: building ao from ${checkoutDir} HEAD although ${ws.reason}; a downgrade is still refused.`);
2813
+ if (dryRun) return { bin: null, source: 'dry-run' };
2814
+ return { bin: build(checkoutDir), source: 'dev', allowDirty: true };
2815
+ }
2816
+ log.warn(`The AO checkout ${checkoutDir} is not a release source (${ws.reason}; Go also counts untracked files as uncommitted), so the published release is used. To build from the checkout instead, rerun with AOS_AO_DEV_BUILD=1.`);
2817
+ }
2818
+ if (fs.existsSync(path.join(releaseDir, '.git'))) {
2819
+ log.warn(`${releaseDir} is a git checkout; not downloading the release over it. Keeping the installed ao.`);
2820
+ return { bin: null, source: 'release-dir-is-checkout' };
2821
+ }
2822
+ if (dryRun) {
2823
+ log.step(`[dry-run] would download ${AO_PKG} to ${releaseDir}`);
2824
+ return { bin: null, source: 'dry-run' };
2825
+ }
2826
+ if (!download(releaseDir)) {
2827
+ log.warn(`Could not download ${AO_PKG}; keeping the installed ao.`);
2828
+ return { bin: null, source: 'download-failed' };
2602
2829
  }
2830
+ return { bin: candidatesIn(releaseDir).find((p) => fs.existsSync(p)) || null, source: 'release' };
2831
+ }
2603
2832
 
2604
- if (!daemonBin) {
2605
- log.warn(`AO binary missing in ${osAgentDir} — package layout changed or Go build needed.`);
2833
+ async function installOSAgentWorkspace() {
2834
+ if (!aoSupportedHere()) {
2835
+ log.warn(`AO is not supported on ${process.platform}/${process.arch}.`);
2606
2836
  return false;
2607
2837
  }
2608
2838
 
2609
- if (daemonBin !== binTarget) {
2610
- installStep('place the ao binary', () => {
2611
- fs.mkdirSync(localBinDir, { recursive: true });
2612
- installBinaryAtomically(daemonBin, binTarget);
2613
- log.step(`Installed ao to ${binTarget}`);
2614
- }, 'AO cannot be started without its binary.');
2839
+ const releaseDir = path.join(moduleBasePath(), 'bdb-agent-orchestrator');
2840
+ const checkoutDir = [
2841
+ path.join(homeDir, 'dev', 'agents', 'bdb-agent-orchestrator'),
2842
+ path.join(homeDir, 'dev', 'bdb-dev', 'bdb-agent-orchestrator'),
2843
+ ].find((d) => fs.existsSync(path.join(d, '.git')));
2844
+ if (checkoutDir) log.step(`Found a local AO checkout: ${checkoutDir}`);
2845
+
2846
+ const pick = selectAoCandidate({ checkoutDir, isLocalGitRepo: !!checkoutDir, releaseDir });
2847
+ if (DRY_RUN) {
2848
+ log.step('[dry-run] would install the ao binary and run `ao service install`');
2849
+ return;
2850
+ }
2851
+
2852
+ const binTarget = aoBinTarget();
2853
+ const installedExists = fs.existsSync(binTarget);
2854
+ let src = null;
2855
+ let candidate = null;
2856
+ if (pick.bin) {
2857
+ candidate = readGoBuildInfo(pick.bin);
2858
+ const installed = installedExists ? readGoBuildInfo(binTarget) : null;
2859
+ const daemonRunning = installedExists && !installed
2860
+ ? (readAoDaemonIdentity() !== null || await verifyDaemonListening(3101, 'AO Orchestrator', 1500))
2861
+ : true;
2862
+ const verdict = decideAoInstall({ installedExists, installed, candidate, allowDirty: !!pick.allowDirty, candidateIsRelease: pick.source === 'release', daemonRunning });
2863
+ if (verdict.install) src = pick.bin;
2864
+ else log.step(`Keeping ${binTarget}: ${verdict.reason}.`);
2865
+ }
2866
+
2867
+ if (!src && !installedExists) {
2868
+ log.warn(`No ao binary could be installed (${pick.source}).`);
2869
+ return false;
2615
2870
  }
2871
+ // Unchanged binary and a live daemon: nothing to restart.
2872
+ if (!src && await verifyDaemonListening(3101, 'AO Orchestrator', 1500)) {
2873
+ log.success('AO Orchestrator running on http://localhost:3101');
2874
+ return;
2875
+ }
2876
+
2877
+ const result = await installAoBinary({
2878
+ src,
2879
+ dest: binTarget,
2880
+ serviceInstall: (bin) => aoServiceInstall(bin),
2881
+ waitForDaemon: () => verifyDaemonListening(3101, 'AO Orchestrator', AO_DAEMON_WAIT_MS),
2882
+ });
2883
+ if (result.ok && src) {
2884
+ const pkgJson = readJsonFile(path.join(pick.source === 'release' ? releaseDir : checkoutDir, 'package.json')) || {};
2885
+ saveManifest({ ao: { version: pick.source === 'dev' ? null : (pkgJson.version || null), revision: candidate ? candidate.revision : null, modified: !!(candidate && candidate.modified), installedAt: new Date().toISOString() } });
2886
+ }
2887
+ return result.ok ? undefined : false;
2888
+ }
2616
2889
 
2890
+ function aoServiceInstall(binTarget) {
2617
2891
  // macOS only: code-sign ad-hoc to prevent AMFI SIGKILL
2618
- if (isMac) {
2892
+ if (process.platform === 'darwin') {
2619
2893
  try {
2620
2894
  execFileSync('codesign', ['-v', binTarget], { stdio: 'ignore' });
2621
2895
  } catch {
@@ -2625,35 +2899,17 @@ async function installOSAgentWorkspace() {
2625
2899
  log.step('Signed it ad-hoc.');
2626
2900
  } catch (e) { log.warn(`Could not sign it: ${e.message}`); }
2627
2901
  }
2628
- }
2629
-
2630
- // AO installs its own service, and does it for macOS, Windows and Linux.
2631
- if (isMac) {
2632
2902
  const legacyPlist = path.join(homeDir, 'Library', 'LaunchAgents', 'com.bdb.agent-workspace.plist');
2633
2903
  if (fs.existsSync(legacyPlist)) {
2634
2904
  try { execFileSync('launchctl', ['unload', legacyPlist], { stdio: 'ignore' }); } catch (e) { logDebug(e, 'unload legacy ao agent'); }
2635
2905
  try { fs.unlinkSync(legacyPlist); log.step('Removed the old com.bdb.agent-workspace service.'); } catch (e) { logDebug(e, 'remove legacy plist'); }
2636
2906
  }
2637
2907
  }
2638
-
2639
- const serviceStep = installStep('register the AO service', () => {
2640
- try {
2641
- execFileSync(binTarget, ['service', 'install'], { stdio: ['ignore', 'pipe', 'pipe'] });
2642
- } catch (e) {
2643
- throw new Error(describeExecError(e));
2644
- }
2645
- }, 'Start it by hand with: ao service install');
2646
- if (!serviceStep.ok) return false;
2647
-
2648
- // Windows startup entries fire asynchronously — give the service more time
2649
- const aoTimeout = process.platform === 'win32' ? 20000 : 8000;
2650
- if (await verifyDaemonListening(3101, 'AO Orchestrator', aoTimeout)) {
2651
- log.success('AO Orchestrator running on http://localhost:3101');
2652
- } else {
2653
- const hint = process.platform === 'win32'
2654
- ? 'Run `ao service install` again or log out and back in, then check `ao service status`.'
2655
- : 'Check `ao service status` and `ao service logs`.';
2656
- log.warn(`AO did not answer on :3101 — ${hint}`);
2908
+ // AO installs its own service, and does it for macOS, Windows and Linux.
2909
+ try {
2910
+ execFileSync(binTarget, ['service', 'install'], { stdio: ['ignore', 'pipe', 'pipe'] });
2911
+ } catch (e) {
2912
+ throw new Error(describeExecError(e));
2657
2913
  }
2658
2914
  }
2659
2915
  async function promptMemBIngestion(mcpCodeTarget) {
@@ -2715,6 +2971,18 @@ function verifyEcosystemInstallation() {
2715
2971
  ];
2716
2972
 
2717
2973
  for (const mod of modules) {
2974
+ if (mod.pkg === AO_PKG) {
2975
+ const bin = aoBinTarget();
2976
+ const exists = fs.existsSync(bin);
2977
+ const build = exists ? readGoBuildInfo(bin) : null;
2978
+ const record = (readJsonFile(path.join(homeDir, '.agents', '.bdb-manifest.json')) || {}).ao || null;
2979
+ const version = trustedAoVersion(build, record);
2980
+ const latest = version ? (newestDistTag(AO_PKG, version) || {}).version : null;
2981
+ const s = describeAoVersion({ exists, build, record, latest });
2982
+ const mark = !exists ? `${colors.dim}⚪` : s.updateAvailable ? `${colors.yellow}⚠️ ` : build && !build.modified ? `${colors.green}✅` : `${colors.yellow}⚠️ `;
2983
+ console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${mark} ${s.text}${colors.reset}`);
2984
+ continue;
2985
+ }
2718
2986
  let modulePkgPath = null;
2719
2987
  for (const p of mod.paths) {
2720
2988
  const candidate = path.join(p, 'package.json');
@@ -2727,29 +2995,9 @@ function verifyEcosystemInstallation() {
2727
2995
  if (modulePkgPath) {
2728
2996
  try {
2729
2997
  const localVer = JSON.parse(fs.readFileSync(modulePkgPath, 'utf8')).version || '1.0.0';
2730
- let newerVersion = null;
2731
- let newerTag = null;
2732
- try {
2733
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
2734
- const distTagsJson = process.platform === 'win32'
2735
- ? execSync(`"${npmCmd}" view ${mod.pkg} dist-tags --json`, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', timeout: 4000 }).trim()
2736
- : execFileSync(npmCmd, ['view', mod.pkg, 'dist-tags', '--json'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', timeout: 4000 }).trim();
2737
- const distTags = JSON.parse(distTagsJson);
2738
- for (const [tag, ver] of Object.entries(distTags)) {
2739
- if (isNewerVersion(localVer, ver)) {
2740
- if (!newerVersion || isNewerVersion(newerVersion, ver)) {
2741
- newerVersion = ver;
2742
- newerTag = tag;
2743
- }
2744
- }
2745
- }
2746
- } catch (e) { logDebug(e, 'operation'); }
2747
-
2748
- // `npm view <pkg> dist-tags --json` fetches all tags.
2749
- // We compare every tag's version against localVer with the fixed
2750
- // isNewerVersion(), and report an update if ANY tag is ahead.
2751
- if (newerVersion) {
2752
- console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.yellow}⚠️ Update available (v${localVer} ➔ v${newerVersion} (${newerTag}))${colors.reset}`);
2998
+ const newer = newestDistTag(mod.pkg, localVer);
2999
+ if (newer) {
3000
+ console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.yellow}⚠️ Update available (v${localVer} ➔ v${newer.version} (${newer.tag}))${colors.reset}`);
2753
3001
  } else {
2754
3002
  console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.green}✅ v${localVer} (Up to date)${colors.reset}`);
2755
3003
  }
@@ -3473,6 +3721,14 @@ function compileClaudeAgents(agents, targetDir, pipelineConfig = null) {
3473
3721
  }
3474
3722
  }
3475
3723
 
3724
+ // .claude/agents/<slug>.md is shipped and copied to ~/.claude/agents by the
3725
+ // manifest-tracked "copy harness directories" step. Compiling the same slug
3726
+ // there first made the two writers fight: the compiled bytes never matched the
3727
+ // manifest, so every update flagged all 13 personas as user-edited.
3728
+ function agentsNotShippedAsFiles(agents, shippedDir = path.join(srcDir, '.claude', 'agents')) {
3729
+ return agents.filter((a) => !fs.existsSync(path.join(shippedDir, `${a.name.toLowerCase().replace(/_/g, '-')}.md`)));
3730
+ }
3731
+
3476
3732
  // Generates .opencode/agents/<name>.md. Emits description, mode: subagent, and model.
3477
3733
  function compileOpenCodeAgents(agents, targetDir, pipelineConfig = null) {
3478
3734
  fs.mkdirSync(targetDir, { recursive: true });
@@ -3575,8 +3831,12 @@ function injectHarnessRules() {
3575
3831
  if (sources.ok) {
3576
3832
  const { globalRules, startcycleContent, agentsMdContent } = sources.value;
3577
3833
 
3834
+ // Run from $HOME, these project files ARE the global files that the
3835
+ // manifest-tracked harness copy below writes; a second writer would
3836
+ // make every update see them as user-edited.
3837
+ const cwdIsHome = path.resolve(currentDir) === path.resolve(homeDir);
3578
3838
  const cursorRulesDir = path.join(currentDir, '.cursor', 'rules');
3579
- installStep(`write the Cursor rules to ${cursorRulesDir}`, () => {
3839
+ if (!cwdIsHome) installStep(`write the Cursor rules to ${cursorRulesDir}`, () => {
3580
3840
  fs.mkdirSync(cursorRulesDir, { recursive: true });
3581
3841
  fs.writeFileSync(path.join(cursorRulesDir, '000_global_rules.mdc'), `---\nname: global-rules\ndescription: Global BDB Agent Rules\n---\n\n${globalRules}`);
3582
3842
  if (startcycleContent) {
@@ -3618,7 +3878,7 @@ function injectHarnessRules() {
3618
3878
  installStep('compile Claude Code subagents', () => {
3619
3879
  if (agentsMdContent) {
3620
3880
  const pipelineConfig = loadPipelineConfig();
3621
- const agents = parseAgentsMd(agentsMdContent);
3881
+ const agents = agentsNotShippedAsFiles(parseAgentsMd(agentsMdContent));
3622
3882
  const claudeAgentsDir = path.join(homeDir, '.claude', 'agents');
3623
3883
  compileClaudeAgents(agents, claudeAgentsDir, pipelineConfig);
3624
3884
  log.step(`Compiled AGENTS.md to Claude Code subagents in ${claudeAgentsDir}`);
@@ -3723,7 +3983,7 @@ function injectHarnessRules() {
3723
3983
  const targetPath = path.join(homeDir, dir);
3724
3984
  // settings.json is merged separately below: a wholesale
3725
3985
  // copy would clobber user-owned keys like enabledPlugins.
3726
- const exclude = dir === '.claude' ? ['settings.json'] : [];
3986
+ const exclude = dir === '.claude' ? ['settings.json'] : dir === '.agents' ? AGENTS_COPY_EXCLUDE : [];
3727
3987
  copyDirRecursiveSync(sourcePath, targetPath, exclude);
3728
3988
  log.step(`Copied ${dir} to ${targetPath}`);
3729
3989
  }
@@ -3735,13 +3995,109 @@ function injectHarnessRules() {
3735
3995
  const globalAgentsDir = path.join(os.homedir(), '.agents');
3736
3996
  const agentsDirSrc = path.join(srcDir, '.agents');
3737
3997
  if (fs.existsSync(agentsDirSrc)) {
3738
- copyDirRecursiveSync(agentsDirSrc, globalAgentsDir);
3998
+ copyDirRecursiveSync(agentsDirSrc, globalAgentsDir, AGENTS_COPY_EXCLUDE);
3739
3999
  log.step(`Synced global .agents/ to ${globalAgentsDir}`);
3740
4000
  }
3741
4001
  }, 'agents.md and workflows/startcycle.md may be missing globally.');
3742
4002
  }
3743
4003
  }
3744
4004
 
4005
+ // Optional third-party OpenCode components. Off unless named in
4006
+ // AOS_OPENCODE_OPTIONAL=ponytail,loop,rtk or --opencode-optional=ponytail,loop,rtk.
4007
+ // Pinned entries only; the foreign installers (opencode-loop's npx installer, rtk init)
4008
+ // are never run because they rewrite the user's opencode config.
4009
+ const OPENCODE_OPTIONAL_PINS = {
4010
+ ponytail: '@dietrichgebert/ponytail@4.10.0',
4011
+ loop: '@bybrawe/opencode-loop@0.6.2',
4012
+ };
4013
+ const OPENCODE_RTK_HINT = 'rtk is not installed by AOS. Run yourself: brew install rtk && rtk init -g --opencode';
4014
+ const OPENCODE_LOOP_SHELL_WARNING = 'opencode-loop: /loop-shell style commands run shell commands as child processes. ' +
4015
+ 'tool.execute.before, and so the AOS go-gate, may never see them (unverified). ' +
4016
+ 'Do not schedule git push, publish or other gated commands through them.';
4017
+
4018
+ function parseOpencodeOptional(argv = process.argv, env = process.env) {
4019
+ const flag = argv.find((a) => a.startsWith('--opencode-optional='));
4020
+ const raw = [env.AOS_OPENCODE_OPTIONAL, flag && flag.slice('--opencode-optional='.length)].filter(Boolean).join(',');
4021
+ return new Set(raw.split(',').map((x) => x.trim().toLowerCase()).filter(Boolean));
4022
+ }
4023
+
4024
+ const pluginPackageName = (entry) => {
4025
+ const str = typeof entry === 'string' ? entry : (Array.isArray(entry) ? String(entry[0]) : '');
4026
+ return str.replace(/(?<=.)@[^@/]*$/, '');
4027
+ };
4028
+
4029
+ // OpenCode auto-loads plugins/*.js and also loads plugin[] paths, then dedupes by file
4030
+ // URL (verified in the 1.18.30 binary strings, not by running it). Same path = one load;
4031
+ // a second differing spelling would load the gate and loop keeper twice.
4032
+ function warnOnDuplicateAosPlugin(plugins, canonical) {
4033
+ const norm = (v) => String(Array.isArray(v) ? v[0] : v).replace(/\\/g, '/');
4034
+ const others = plugins.filter((p) => /bdb-aos\.js$/.test(norm(p)) && norm(p) !== canonical);
4035
+ if (others.length > 0) {
4036
+ log.warn(`opencode.jsonc lists bdb-aos.js under another path (${others.map(norm).join(', ')}). ` +
4037
+ `plugins/bdb-aos.js is auto-loaded too; remove the extra entry to avoid a double gate.`);
4038
+ }
4039
+ }
4040
+
4041
+ function appendOptionalOpencodePlugins(data, configPath, optional) {
4042
+ const wanted = optional instanceof Set ? optional : new Set(optional || []);
4043
+ for (const name of wanted) {
4044
+ if (!(name in OPENCODE_OPTIONAL_PINS) && name !== 'rtk') log.warn(`Unknown OpenCode optional component "${name}" ignored.`);
4045
+ }
4046
+ if (wanted.has('rtk')) log.message(OPENCODE_RTK_HINT);
4047
+ let backedUp = false;
4048
+ for (const name of Object.keys(OPENCODE_OPTIONAL_PINS)) {
4049
+ if (!wanted.has(name) || DRY_RUN) continue;
4050
+ const pin = OPENCODE_OPTIONAL_PINS[name];
4051
+ if (data.plugin.some((p) => pluginPackageName(p) === pluginPackageName(pin))) continue;
4052
+ if (!backedUp && configPath && fs.existsSync(configPath)) {
4053
+ const bak = `${configPath}.${timestamp}.bak`;
4054
+ try { fs.copyFileSync(configPath, bak); backedUp = true; } catch (e) {
4055
+ log.warn(`Could not back up ${configPath}, skipping optional ${name}: ${e.message}`);
4056
+ continue;
4057
+ }
4058
+ }
4059
+ data.plugin.push(pin);
4060
+ log.step(`Added opt-in OpenCode plugin ${pin}`);
4061
+ }
4062
+ if (data.plugin.some((p) => pluginPackageName(p) === pluginPackageName(OPENCODE_OPTIONAL_PINS.loop))) {
4063
+ log.warn(OPENCODE_LOOP_SHELL_WARNING);
4064
+ }
4065
+ }
4066
+
4067
+ // Idempotent and edit-safe: a file is overwritten only when it is absent, already
4068
+ // identical, or still byte-equal to what this installer last recorded. Anything else
4069
+ // (user edit, or an untracked file with the same name) is kept and the shipped copy
4070
+ // lands as <file>.new. Files the installer did not create are never removed.
4071
+ function installOpencodeCommands(srcDirPath, destDirPath) {
4072
+ if (DRY_RUN) {
4073
+ log.message(`[dry-run] install OpenCode commands: ${srcDirPath} -> ${destDirPath}`);
4074
+ return;
4075
+ }
4076
+ const ownsManifest = !_sessionManifest;
4077
+ const manifest = _sessionManifest || loadInstallManifest();
4078
+ fs.mkdirSync(destDirPath, { recursive: true });
4079
+ let wrote = 0;
4080
+ for (const file of fs.readdirSync(srcDirPath)) {
4081
+ const src = path.join(srcDirPath, file);
4082
+ if (!fs.statSync(src).isFile()) continue;
4083
+ const dest = path.join(destDirPath, file);
4084
+ const srcHash = computeFileHash(src);
4085
+ const diskHash = fs.existsSync(dest) ? computeFileHash(dest) : null;
4086
+ if (diskHash === null || diskHash === srcHash || diskHash === manifest[dest]?.sha256) {
4087
+ if (diskHash !== srcHash) { fs.copyFileSync(src, dest); wrote++; }
4088
+ manifest[dest] = { path: dest, sha256: srcHash, version: pkg.version, installedAt: new Date().toISOString() };
4089
+ } else {
4090
+ fs.copyFileSync(src, `${dest}.new`);
4091
+ keptUserEdits.push(dest);
4092
+ }
4093
+ }
4094
+ if (ownsManifest) {
4095
+ saveInstallManifest(manifest);
4096
+ reportKeptUserEdits();
4097
+ }
4098
+ log.step(`OpenCode commands in ${destDirPath}: ${wrote} written`);
4099
+ }
4100
+
3745
4101
  // Copy the OpenCode plugin + command payload and register both in
3746
4102
  // opencode.jsonc. This is the single copy site: the Quick Update path
3747
4103
  // (injectHarnessRules -> installGlobalHooks) and the fresh-install path
@@ -3752,7 +4108,7 @@ function injectHarnessRules() {
3752
4108
  // Sync merges MCP servers in the same pass and writes once). When `data` is
3753
4109
  // omitted the function loads and saves the config itself, which is what lets the
3754
4110
  // Quick Update path register the plugin as well as copy it.
3755
- function installOpencodePlugin({ targetHome = homeDir, configPath = null, data = null } = {}) {
4111
+ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data = null, optional = parseOpencodeOptional() } = {}) {
3756
4112
  const opencodeDir = configPath
3757
4113
  ? path.dirname(configPath)
3758
4114
  : (process.platform === 'win32'
@@ -3789,13 +4145,11 @@ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data =
3789
4145
  try { copyDirRecursiveSync(trailLibSrc, path.join(opencodeDir, 'plugins', 'lib')); } catch (e) { log.warn(`Could not install OpenCode plugin lib: ${e.message}`); }
3790
4146
  }
3791
4147
 
3792
- // Slash-command payloads. Without these the /startcycle-graph command has no
3793
- // native resolution and only survives as a raw-text match in the plugin.
4148
+ // Slash-command payloads (/startcycle-graph and the generated /bdb-aos-<cmd> files).
3794
4149
  const commandsSrc = path.join(srcDir, '.opencode', 'commands');
3795
4150
  if (fs.existsSync(commandsSrc)) {
3796
4151
  try {
3797
- copyDirRecursiveSync(commandsSrc, path.join(opencodeDir, 'commands'));
3798
- log.step(`Installed OpenCode commands to ${path.join(opencodeDir, 'commands')}`);
4152
+ installOpencodeCommands(commandsSrc, path.join(opencodeDir, 'commands'));
3799
4153
  } catch (e) {
3800
4154
  log.warn(`Could not install OpenCode commands: ${e.message}`);
3801
4155
  }
@@ -3822,6 +4176,8 @@ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data =
3822
4176
  return str.includes('bdb-aos');
3823
4177
  });
3824
4178
  if (!alreadyRegistered) data.plugin.push(pluginPathNormalized);
4179
+ warnOnDuplicateAosPlugin(data.plugin, pluginPathNormalized);
4180
+ appendOptionalOpencodePlugins(data, configPath, optional);
3825
4181
 
3826
4182
  // Skill paths. `.agents/skills` stays project-relative on purpose: it is the
3827
4183
  // per-project contract, and OpenCode resolves it against each project, so it
@@ -3987,7 +4343,7 @@ function installGlobalBinaries() {
3987
4343
  // merged result goes to a .bdb-new.json sidecar -- the same recovery pattern
3988
4344
  // the MCP config merge in installMcpsForTarget uses.
3989
4345
  function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
3990
- const bdbHookScripts = ['go-gate.mjs', 'go-token.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'trail-autostart.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
4346
+ const bdbHookScripts = ['go-gate.mjs', 'go-token.mjs', 'go-grant.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'trail-autostart.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
3991
4347
  // memb-inject reads the machine-global memB store under $HOME and is
3992
4348
  // installed once per machine, so it stays $HOME-anchored even inside a
3993
4349
  // project harness -- unlike the two gate hooks, which are per-checkout by
@@ -4095,9 +4451,12 @@ function selfCheckAgyHooks(hooksPath, spawn = spawnSync) {
4095
4451
  const failures = [];
4096
4452
  for (const command of [...new Set(commands)]) {
4097
4453
  // AGENTTRAIL_PORT=1: the no-op payload must not show up on a live map.
4098
- const r = spawn(shell, [flag, command], { cwd: path.dirname(hooksPath), input: '{}', encoding: 'utf8', timeout: 5000, windowsHide: true, env: { ...process.env, AGENTTRAIL_PORT: '1' } });
4454
+ const r = spawn(shell, [flag, command], { cwd: path.dirname(hooksPath), input: '{}', encoding: 'utf8', timeout: 20000, windowsHide: true, env: { ...process.env, AGENTTRAIL_PORT: '1' } });
4099
4455
  const stderr = String((r && r.stderr) || '');
4100
4456
  let reason = null;
4457
+ // A slow machine (memb-inject reads the store) can exceed the limit without the hook being broken:
4458
+ // the failures this check exists for (bad path, missing module, non-zero exit) are immediate.
4459
+ if (r && r.error && r.error.code === 'ETIMEDOUT') { log.warn(`agy hook self-check timed out, not treated as a failure: ${command}`); continue; }
4101
4460
  if (!r || r.error) reason = `spawn failed: ${r && r.error ? r.error.message : 'no result'}`;
4102
4461
  else if (r.status !== 0) reason = `exit ${r.status}\n ${stderr.trim().slice(0, 500)}`;
4103
4462
  else if (/MODULE_NOT_FOUND|Cannot find module/.test(stderr)) reason = `module not found\n ${stderr.trim().slice(0, 500)}`;
@@ -4391,7 +4750,7 @@ function installProjectHarness() {
4391
4750
  installStep('copy .agents/ contract into project', () => {
4392
4751
  const agentsSrc = path.join(srcDir, '.agents');
4393
4752
  if (!fs.existsSync(agentsSrc)) throw new Error(`missing payload: ${agentsSrc}`);
4394
- copyDirRecursiveSync(agentsSrc, projectAgentsDir);
4753
+ copyDirRecursiveSync(agentsSrc, projectAgentsDir, AGENTS_COPY_EXCLUDE);
4395
4754
  log.step(`Copied .agents/ contract to ${projectAgentsDir}`);
4396
4755
  }, 'graph.md / state.schema.json may be missing in the project.');
4397
4756
 
@@ -5420,20 +5779,22 @@ async function runQuickUpdate(installState) {
5420
5779
 
5421
5780
 
5422
5781
  function buildAoAnnouncementBanner() {
5782
+ const W = 78;
5783
+ const row = (text, style = '') => `\x1b[36m│\x1b[0m${style}${text.padEnd(W)}\x1b[0m\x1b[36m│\x1b[0m`;
5423
5784
  return [
5424
- '\x1b[36m╭──────────────────────────────────────────────────────────────────────────────╮\x1b[0m',
5425
- '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
5426
- '\x1b[36m│\x1b[0m \x1b[1m\x1b[35m🚀 BDB AGENT ORCHESTRATOR APP — FINALE BETA JETZT VERFÜGBAR!\x1b[0m \x1b[36m│\x1b[0m',
5427
- '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
5428
- '\x1b[36m│\x1b[0m Die nächste Generation der Cross-Harness Multi-Agenten-Orchestrierung \x1b[36m│\x1b[0m',
5429
- '\x1b[36m│\x1b[0m ist jetzt als finale Beta für alle User freigeschaltet. \x1b[36m│\x1b[0m',
5430
- '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
5431
- '\x1b[36m│\x1b[0m • \x1b[1mDashboard & WebUI:\x1b[0m http://localhost:3101 \x1b[36m│\x1b[0m',
5432
- '\x1b[36m│\x1b[0m • \x1b[1mService-Befehl:\x1b[0m ao service install (Hintergrunddienst aktivieren) \x1b[36m│\x1b[0m',
5433
- '\x1b[36m│\x1b[0m • \x1b[1mQuick Launch:\x1b[0m ao open oder ao service status \x1b[36m│\x1b[0m',
5434
- '\x1b[36m│\x1b[0m • \x1b[1mFeatures:\x1b[0m Session-Telemetrie, Live-AgentTrail & Multi-Workspaces\x1b[36m│\x1b[0m',
5435
- '\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m',
5436
- '\x1b[36m╰──────────────────────────────────────────────────────────────────────────────╯\x1b[0m\n'
5785
+ `\x1b[36m╭${'─'.repeat(W)}╮\x1b[0m`,
5786
+ row(''),
5787
+ row(' BDB AGENT ORCHESTRATOR APP — FINAL BETA NOW AVAILABLE', '\x1b[1m\x1b[35m'),
5788
+ row(''),
5789
+ row(' The next generation of cross-harness multi-agent orchestration'),
5790
+ row(' is now open to every user as a final beta.'),
5791
+ row(''),
5792
+ row(' • Dashboard & WebUI: http://localhost:3101'),
5793
+ row(' • Service command: ao service install (enables the background service)'),
5794
+ row(' • Quick launch: ao open or ao service status'),
5795
+ row(' • Features: session telemetry, live AgentTrail, multi-workspaces'),
5796
+ row(''),
5797
+ `\x1b[36m╰${'─'.repeat(W)}╯\x1b[0m\n`
5437
5798
  ].join('\n');
5438
5799
  }
5439
5800
 
@@ -5487,11 +5848,11 @@ function installBinaryAtomically(src, dest) {
5487
5848
 
5488
5849
  async function main() {
5489
5850
  const args = process.argv.slice(2);
5490
- if (args.includes('--version') || args.includes('-V')) {
5851
+ if (args.includes('--version') || args.includes('-V') || args[0] === 'version') {
5491
5852
  console.log(pkg.version);
5492
5853
  return;
5493
5854
  }
5494
- if (args.includes('--help') || args.includes('-h')) {
5855
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
5495
5856
  console.log(`Usage: aos [command] [options]
5496
5857
 
5497
5858
  Commands:
@@ -5501,6 +5862,15 @@ Commands:
5501
5862
  Options:
5502
5863
  -y, --yes Non-interactive install (implied without a TTY)
5503
5864
  --dry-run Show what would change
5865
+ --opencode-optional=LIST
5866
+ Opt in to OpenCode extras (comma list: ponytail, loop, rtk), or set
5867
+ AOS_OPENCODE_OPTIONAL. Pinned plugin[] entries are appended after a
5868
+ config backup; rtk only prints a brew hint. Off by default. AOS never
5869
+ runs foreign installers and never touches OpenCode's mcp set.
5870
+ --plugin-migration=off|check
5871
+ Skip (off) or only report (check) the bdb-aos plugin registration and
5872
+ removal of AOS's own loose skill copies; same as AOS_PLUGIN_MIGRATION.
5873
+ --dry-run implies check. Default: on.
5504
5874
  --verbose, -v Verbose output
5505
5875
  -V, --version Print the version and exit
5506
5876
  -h, --help Print this help and exit`);
@@ -5868,45 +6238,7 @@ Options:
5868
6238
  const s = spinner();
5869
6239
  s.start(`Installing optimized skills${tier === '2' ? ' [Basic Tier]' : ''} to ${targets.length} target(s)...`);
5870
6240
 
5871
- for (const t of targets) {
5872
- if (mode === 'replace') {
5873
- moveIfExists(t.targetSkillDir, path.join(backupDir, `config_skills_backup_${t.value}`), `global config skills (${t.value})`);
5874
- moveIfExists(t.targetLegacyDir, path.join(backupDir, `legacy_skills_backup_${t.value}`), `legacy skills (${t.value})`);
5875
- moveIfExists(t.targetWorkspaceDir, path.join(backupDir, `workspace_skills_backup_${t.value}`), `workspace skills (${t.value})`);
5876
- }
5877
-
5878
- installStep(`create the skill target directories (${t.value})`, () => {
5879
- fs.mkdirSync(t.targetSkillDir, { recursive: true });
5880
- retireObsoleteLegacyDir(t.targetLegacyDir);
5881
- fs.mkdirSync(t.targetWorkspaceDir, { recursive: true });
5882
- }, 'The skill copies below will most likely be skipped as well.');
5883
-
5884
- if (fs.existsSync(skillsBase)) {
5885
- installStep(`install the skills (${t.value})`, () => {
5886
- const rawDirs = fs.readdirSync(skillsBase);
5887
- const dirs = rawDirs.sort((a, b) => {
5888
- const aIsLeaf = fs.existsSync(path.join(skillsBase, a, 'SKILL.md'));
5889
- const bIsLeaf = fs.existsSync(path.join(skillsBase, b, 'SKILL.md'));
5890
- if (aIsLeaf && !bIsLeaf) return 1;
5891
- if (!aIsLeaf && bIsLeaf) return -1;
5892
- return 0;
5893
- });
5894
- for (const dir of dirs) {
5895
- const fullPath = path.join(skillsBase, dir);
5896
- if (!fs.statSync(fullPath).isDirectory()) continue;
5897
-
5898
- if (dir === 'global_legacy') {
5899
- copyDirRecursiveSync(fullPath, t.targetLegacyDir, excludeSkills);
5900
- } else if (dir === 'workspace_agents') {
5901
- copyDirRecursiveSync(fullPath, t.targetWorkspaceDir, excludeSkills);
5902
- } else {
5903
- syncSkillEntry(fullPath, dir, t.targetSkillDir, excludeSkills);
5904
- }
5905
- }
5906
- log.step(`Installed all global config & core skills to ${t.targetSkillDir}`);
5907
- }, 'The skills are missing or incomplete; the rest of the installation continues.');
5908
- }
5909
- }
6241
+ installTargetSkills(targets, { mode, backupDir, excludeSkills, skillsBase });
5910
6242
 
5911
6243
  syncSkillsToGlobalHarnesses(excludeSkills);
5912
6244
  pruneRemovedSkills(_sessionManifest);
@@ -5961,6 +6293,16 @@ if (require.main === module) {
5961
6293
  module.exports = {
5962
6294
  installBinaryAtomically,
5963
6295
  installOSAgentWorkspace,
6296
+ readGoBuildInfo,
6297
+ parseGoBuildInfo,
6298
+ decideAoInstall,
6299
+ checkAoWorkspace,
6300
+ installAoBinary,
6301
+ readAoDaemonIdentity,
6302
+ selectAoCandidate,
6303
+ describeAoVersion,
6304
+ buildAoAnnouncementBanner,
6305
+ agentsNotShippedAsFiles,
5964
6306
  downloadOrUpdateModule,
5965
6307
  detectPlatforms,
5966
6308
  markPlatformsExplicit,
@@ -5976,6 +6318,10 @@ module.exports = {
5976
6318
  mergeCodexTomlMcpServers,
5977
6319
  installGlobalHooks,
5978
6320
  installOpencodePlugin,
6321
+ installOpencodeCommands,
6322
+ parseOpencodeOptional,
6323
+ pluginMigrationMode,
6324
+ runPluginMigration,
5979
6325
  installProjectHarness,
5980
6326
  promptMcpSelection,
5981
6327
  mirrorMcpServersTo,
@@ -5988,6 +6334,9 @@ module.exports = {
5988
6334
  resolveFileConflict,
5989
6335
  buildKnownSourceHashes,
5990
6336
  initSessionManifest,
6337
+ installTargetSkills,
6338
+ stableOpenWikiScripts,
6339
+ AGENTS_COPY_EXCLUDE,
5991
6340
  flushSessionManifest,
5992
6341
  copyDirRecursiveSync,
5993
6342
  INSTALL_MANIFEST_PATH,