@hybridlabor-api/aos 4.14.1 → 4.16.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 (51) hide show
  1. package/.claude/hooks/aos-bus.mjs +146 -0
  2. package/.claude/hooks/go-gate.mjs +2 -1
  3. package/.opencode/plugins/bdb-aos.js +111 -5
  4. package/bin/aos-acp.mjs +7 -3
  5. package/bin/aos-doctor.mjs +21 -6
  6. package/docs/master-session-acp.md +15 -0
  7. package/docs/sessions/audit-agents.md +2 -0
  8. package/installer.js +341 -163
  9. package/package.json +3 -2
  10. package/scripts/validate-skills.mjs +29 -6
  11. package/skills/basic/godmode-shipping/SKILL.md +0 -1
  12. package/skills/basic/master-session/SKILL.md +1 -1
  13. package/skills/basic/startcycle/SKILL.md +0 -1
  14. package/skills/basic/startcycle-graph/SKILL.md +0 -1
  15. package/skills/global_config/ask-tim/SKILL.md +0 -1
  16. package/skills/global_config/grill-me/SKILL.md +0 -1
  17. package/skills/global_config/grill-with-docs/SKILL.md +0 -1
  18. package/skills/playbooks/pb-bug-fix/SKILL.md +48 -0
  19. package/skills/playbooks/pb-ci-fix/SKILL.md +0 -1
  20. package/skills/playbooks/pb-clip-from-moodboard/SKILL.md +55 -0
  21. package/skills/playbooks/pb-crew-call-sheet/SKILL.md +40 -0
  22. package/skills/playbooks/pb-deploy-saas/SKILL.md +44 -0
  23. package/skills/playbooks/pb-docs-site/SKILL.md +44 -0
  24. package/skills/playbooks/pb-event-tracker/SKILL.md +0 -1
  25. package/skills/playbooks/pb-focus-chunks/SKILL.md +40 -0
  26. package/skills/playbooks/pb-handover/SKILL.md +41 -0
  27. package/skills/playbooks/pb-harness-work/SKILL.md +64 -0
  28. package/skills/playbooks/pb-health-weekly/SKILL.md +43 -0
  29. package/skills/playbooks/pb-idea-to-launch/SKILL.md +51 -0
  30. package/skills/playbooks/pb-image-to-3d/SKILL.md +46 -0
  31. package/skills/playbooks/pb-inbox-zero/SKILL.md +39 -0
  32. package/skills/playbooks/pb-invoice-check/SKILL.md +41 -0
  33. package/skills/playbooks/pb-landing-page/SKILL.md +46 -0
  34. package/skills/playbooks/pb-launch-video/SKILL.md +49 -0
  35. package/skills/playbooks/pb-machine-setup/SKILL.md +44 -0
  36. package/skills/playbooks/pb-master/SKILL.md +53 -0
  37. package/skills/playbooks/pb-meeting-actions/SKILL.md +0 -1
  38. package/skills/playbooks/pb-newsletter/SKILL.md +43 -0
  39. package/skills/playbooks/pb-offer/SKILL.md +39 -0
  40. package/skills/playbooks/pb-open-source/SKILL.md +50 -0
  41. package/skills/playbooks/pb-pcb-to-case/SKILL.md +46 -0
  42. package/skills/playbooks/pb-project-new/SKILL.md +0 -1
  43. package/skills/playbooks/pb-redesign-app/SKILL.md +47 -0
  44. package/skills/playbooks/pb-release-aos/SKILL.md +50 -0
  45. package/skills/playbooks/pb-security-sweep/SKILL.md +48 -0
  46. package/skills/playbooks/pb-ship/SKILL.md +49 -0
  47. package/skills/playbooks/pb-show-build/SKILL.md +48 -0
  48. package/skills/playbooks/pb-social-pack/SKILL.md +47 -0
  49. package/skills/playbooks/pb-todo/SKILL.md +41 -0
  50. package/skills/playbooks/pb-week-plan/SKILL.md +0 -1
  51. package/skills/playbooks/pb-worktrees-land/SKILL.md +52 -0
package/installer.js CHANGED
@@ -497,8 +497,8 @@ const CORE_MCP = 'memb-mcp';
497
497
  // compared against: (a) the SOURCE hash (unmodified) and (b) the MANIFEST
498
498
  // hash (last-installed value). This gives us three cases:
499
499
  // 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
500
+ // ours + user-edited → disk_hash != manifest_hash → keep user file,
501
+ // write <file>.new
502
502
  // foreign → no manifest record AND disk_hash
503
503
  // not in any known source hash → skip + warn
504
504
 
@@ -560,8 +560,11 @@ function adoptFileIntoManifest(manifest, targetPath, sourceHash) {
560
560
  };
561
561
  }
562
562
 
563
+ // User-edited files this run left untouched (shipped copy written as <file>.new).
564
+ const keptUserEdits = [];
565
+
563
566
  // Manifest-aware file writer implementing the three-way conflict policy.
564
- // Returns 'wrote' | 'bak' | 'skipped'.
567
+ // Returns 'wrote' | 'bak' | 'kept' | 'skipped'.
565
568
  // knownSourceHashes: Set of sha256 hashes of all source files in the current
566
569
  // payload (used to identify files we recognise even before manifest entry).
567
570
  function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes) {
@@ -627,15 +630,13 @@ function resolveFileConflict(sourcePath, targetPath, manifest, knownSourceHashes
627
630
  }
628
631
 
629
632
  // Case: ours + user-edited (manifest recorded our hash, but disk now differs).
633
+ // The user's file wins: the shipped version lands next to it as .new, and
634
+ // the manifest keeps the old hash so the next run still sees the edit.
630
635
  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';
636
+ const newPath = `${targetPath}.new`;
637
+ fs.copyFileSync(sourcePath, newPath);
638
+ keptUserEdits.push(targetPath);
639
+ return 'kept';
639
640
  }
640
641
 
641
642
  // Default: manifest entry present, disk matches manifest (no change needed) – overwrite.
@@ -765,6 +766,17 @@ function pruneRemovedSkills(manifest) {
765
766
 
766
767
  function flushSessionManifest() {
767
768
  if (_sessionManifest) saveInstallManifest(_sessionManifest);
769
+ reportKeptUserEdits();
770
+ }
771
+
772
+ function reportKeptUserEdits() {
773
+ if (keptUserEdits.length === 0) return;
774
+ log.warn([
775
+ `${keptUserEdits.length} file(s) you edited were KEPT; the shipped version is next to each as <file>.new:`,
776
+ ...keptUserEdits.map((p) => ` ${p}`),
777
+ 'Merge or delete the .new files when convenient.',
778
+ ].join('\n'));
779
+ keptUserEdits.length = 0;
768
780
  }
769
781
 
770
782
  // skills/global_legacy/ has not existed in the shipped payload for a long
@@ -2504,118 +2516,283 @@ function aoSupportedHere() {
2504
2516
  return ['darwin', 'linux', 'win32'].includes(process.platform);
2505
2517
  }
2506
2518
 
2507
- async function installOSAgentWorkspace() {
2508
- if (!aoSupportedHere()) {
2509
- log.warn(`AO is not supported on ${process.platform}/${process.arch}.`);
2510
- return false;
2511
- }
2519
+ const AO_PKG = '@hybridlabor-api/bdb-agent-orchestrator';
2520
+ // `ao service install` restarts the daemon; it can take well over 8s to bind.
2521
+ const AO_DAEMON_WAIT_MS = 30000;
2512
2522
 
2513
- const isWin = process.platform === 'win32';
2514
- const isMac = process.platform === 'darwin';
2515
- const isLinux = process.platform === 'linux';
2523
+ function aoBinTarget() {
2524
+ return process.platform === 'win32'
2525
+ ? path.join(process.env.LOCALAPPDATA || homeDir, 'Programs', 'ao', 'ao.exe')
2526
+ : path.join(homeDir, '.local', 'bin', 'ao');
2527
+ }
2516
2528
 
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'));
2529
+ // Go links the `go version -m` text between these two 16-byte markers, so the
2530
+ // revision of a binary can be read from its bytes: no Go toolchain needed, and
2531
+ // the binary (possibly the live daemon) is never executed.
2532
+ const GO_BUILDINFO_START = Buffer.from('3077af0c9274080241e1c107e6d618e6', 'hex');
2533
+ const GO_BUILDINFO_END = Buffer.from('f932433186182072008242104116d8f2', 'hex');
2534
+
2535
+ function readGoBuildInfo(binPath) {
2536
+ let buf;
2537
+ try { buf = fs.readFileSync(binPath); } catch { return null; }
2538
+ const start = buf.indexOf(GO_BUILDINFO_START);
2539
+ const end = start < 0 ? -1 : buf.indexOf(GO_BUILDINFO_END, start);
2540
+ return end < 0 ? null : parseGoBuildInfo(buf.subarray(start + GO_BUILDINFO_START.length, end).toString('utf8'));
2541
+ }
2525
2542
 
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
- }
2543
+ // Accepts the embedded text or `go version -m` output (same lines, tab-indented).
2544
+ function parseGoBuildInfo(text) {
2545
+ const get = (key) => (String(text).match(new RegExp(`^\\s*build\\s+${key}=(\\S*)`, 'm')) || [])[1];
2546
+ const revision = get('vcs\\.revision');
2547
+ if (!revision) return null;
2548
+ return { revision, time: get('vcs\\.time') || null, modified: get('vcs\\.modified') === 'true' };
2549
+ }
2550
+
2551
+ // Never trade the installed ao for a build that is not provably newer and clean.
2552
+ // allowDirty: the AOS_AO_DEV_BUILD opt-in. daemonRunning/candidateIsRelease
2553
+ // only matter when the installed binary's revision is unreadable: a clean
2554
+ // published release may then replace it, but not while some daemon of unknown
2555
+ // build is running.
2556
+ function decideAoInstall({ installedExists, installed, candidate, allowDirty = false, candidateIsRelease = false, daemonRunning = true }) {
2557
+ if (!installedExists) return { install: true, reason: 'no ao installed yet' };
2558
+ if (!candidate) return { install: false, reason: 'the candidate build has no readable revision' };
2559
+ if (candidate.modified && !allowDirty) return { install: false, reason: `the candidate ${candidate.revision.slice(0, 9)} is a dirty build (vcs.modified=true)` };
2560
+ if (!installed) {
2561
+ if (candidateIsRelease && !candidate.modified && daemonRunning === false) {
2562
+ 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)}` };
2563
+ }
2564
+ return { install: false, reason: 'the installed binary\'s revision is unreadable, so a downgrade cannot be ruled out' };
2565
+ }
2566
+ if (candidate.revision === installed.revision) return { install: false, reason: `already at ${installed.revision.slice(0, 9)}` };
2567
+ const c = Date.parse(candidate.time || '');
2568
+ const i = Date.parse(installed.time || '');
2569
+ 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'})` };
2570
+ return { install: true, reason: `${installed.revision.slice(0, 9)} -> ${candidate.revision.slice(0, 9)}` };
2571
+ }
2572
+
2573
+ // A dev checkout is a release source only when clean and HEAD sits on a tag.
2574
+ function checkAoWorkspace(dir, git = (args) => execFileSync('git', ['--no-optional-locks', '-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()) {
2575
+ try {
2576
+ if (git(['status', '--porcelain'])) return { ok: false, reason: 'it has uncommitted or untracked changes' };
2577
+ let tag;
2578
+ try { tag = git(['describe', '--tags', '--exact-match', 'HEAD']); } catch { return { ok: false, reason: 'HEAD is not on a release tag' }; }
2579
+ return { ok: true, tag, head: git(['rev-parse', 'HEAD']) };
2580
+ } catch (e) {
2581
+ return { ok: false, reason: `git could not read it (${String(e.message).split('\n')[0]})` };
2533
2582
  }
2534
- if (DRY_RUN) {
2535
- log.step('[dry-run] would link ao binary and run `ao service install`');
2536
- return;
2583
+ }
2584
+
2585
+ // Builds into a temp dir: a build left in the checkout's gitignored backend/bin
2586
+ // outlives every later pull and gets picked up as if it were current.
2587
+ function buildAoFromSource(dir) {
2588
+ if (!hasExecutable('go') || !fs.existsSync(path.join(dir, 'backend', 'cmd', 'ao'))) return null;
2589
+ const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aos-ao-build-')), process.platform === 'win32' ? 'ao.exe' : 'ao');
2590
+ const s = spinner();
2591
+ s.start('Compiling AO daemon binary from Go source...');
2592
+ try {
2593
+ execFileSync('go', ['build', '-ldflags=-s -w', '-o', out, './cmd/ao'], { cwd: path.join(dir, 'backend'), stdio: 'ignore' });
2594
+ s.stop('Compiled AO daemon binary successfully.');
2595
+ return out;
2596
+ } catch (e) {
2597
+ s.stop(`Failed to compile AO from source: ${e.message}`);
2598
+ return null;
2537
2599
  }
2600
+ }
2538
2601
 
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');
2602
+ // Installed version = the release version the installer recorded for exactly
2603
+ // the revision on disk. Never the dev workspace's package.json.
2604
+ function trustedAoVersion(build, record) {
2605
+ return build && !build.modified && record && record.version && record.revision === build.revision ? record.version : null;
2606
+ }
2543
2607
 
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
- );
2608
+ function describeAoVersion({ exists, build, record, latest }) {
2609
+ if (!exists) return { text: 'Not installed', updateAvailable: false };
2610
+ if (!build) return { text: 'Installed (build revision unreadable, version unknown)', updateAvailable: false };
2611
+ const rev = build.revision.slice(0, 9);
2612
+ if (build.modified) return { text: `Dev build ${rev} with uncommitted changes (not a release, version not comparable)`, updateAvailable: false };
2613
+ const version = trustedAoVersion(build, record);
2614
+ if (!version) return { text: `Build ${rev} (release version unknown)`, updateAvailable: false };
2615
+ if (latest && isNewerVersion(version, latest)) return { text: `Update available (v${version} ➔ v${latest})`, updateAvailable: true };
2616
+ return { text: `v${version} (Up to date)`, updateAvailable: false };
2617
+ }
2618
+
2619
+ // The daemon's running.json (AO backend/internal/runfile: pid, port, startedAt).
2620
+ // pid+startedAt tells a restarted daemon from the old one still on :3101.
2621
+ function readAoDaemonIdentity(file = process.env.AO_RUN_FILE || path.join(homeDir, '.ao', 'running.json')) {
2622
+ const data = readJsonFile(file);
2623
+ if (!data || data.pid == null) return null;
2624
+ return data.startedAt ? `${data.pid}@${data.startedAt}` : String(data.pid);
2625
+ }
2626
+
2627
+ async function waitUntil(check, timeoutMs, pollMs = 300) {
2628
+ const deadline = Date.now() + timeoutMs;
2629
+ for (;;) {
2630
+ if (check()) return true;
2631
+ if (Date.now() >= deadline) return false;
2632
+ await new Promise((r) => setTimeout(r, pollMs));
2574
2633
  }
2575
- let daemonBin = candidateBinaries.find(p => fs.existsSync(p));
2634
+ }
2576
2635
 
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...');
2636
+ // Back up, swap atomically, register the service, wait for a NEW daemon on
2637
+ // :3101. A failed swap or service install puts the previous binary back.
2638
+ async function installAoBinary({ src, dest, stamp = timestamp, serviceInstall, waitForDaemon, readIdentity = readAoDaemonIdentity, restartWaitMs = AO_DAEMON_WAIT_MS }) {
2639
+ let backup = null;
2640
+ if (src) {
2641
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
2642
+ if (fs.existsSync(dest)) {
2643
+ backup = `${dest}.bak-${stamp}`;
2644
+ fs.copyFileSync(dest, backup);
2645
+ }
2581
2646
  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.');
2647
+ installBinaryAtomically(src, dest);
2590
2648
  } catch (e) {
2591
- s.stop(`Failed to compile AO from source: ${e.message}`);
2649
+ if (backup) installBinaryAtomically(backup, dest);
2650
+ log.warn(`Could not place the ao binary (${e.message})${backup ? '; the previous binary is back in place' : ''}.`);
2651
+ return { ok: false, backup, restored: !!backup };
2592
2652
  }
2653
+ log.step(`Installed ao to ${dest}${backup ? ` (previous binary kept at ${backup})` : ''}`);
2654
+ }
2655
+ const before = readIdentity();
2656
+ try {
2657
+ await serviceInstall(dest);
2658
+ } catch (e) {
2659
+ if (backup) {
2660
+ installBinaryAtomically(backup, dest);
2661
+ try { await serviceInstall(dest); } catch (e2) { logDebug(e2, 'ao service install after restore'); }
2662
+ }
2663
+ log.warn(`AO service install failed: ${e.message}${backup ? `. Restored the previous ao from ${backup}` : ''}. Start it by hand with: ao service install`);
2664
+ return { ok: false, backup, restored: !!backup };
2665
+ }
2666
+ // No identity before (file missing/unreadable): the port check is all we have.
2667
+ const restarted = before ? await waitUntil(() => { const now = readIdentity(); return !!now && now !== before; }, restartWaitMs) : true;
2668
+ const listening = await waitForDaemon();
2669
+ const backupNote = backup ? ` The previous binary is at ${backup} (restore: mv "${backup}" "${dest}" && ao service install).` : '';
2670
+ if (listening && restarted) {
2671
+ log.success('AO Orchestrator running on http://localhost:3101');
2672
+ } else if (listening) {
2673
+ 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}`);
2674
+ } else {
2675
+ const hint = process.platform === 'win32'
2676
+ ? 'Run `ao service install` again or log out and back in, then check `ao service status`.'
2677
+ : 'Check `ao service status` and `ao service logs`.';
2678
+ log.warn(`AO did not answer on :3101 within ${Math.round(AO_DAEMON_WAIT_MS / 1000)}s. ${hint}${backupNote}`);
2593
2679
  }
2680
+ return { ok: true, backup, listening, restarted };
2681
+ }
2594
2682
 
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 (_) {}
2683
+ function aoPlatformBinaries(dir) {
2684
+ const b = (...p) => path.join(dir, 'backend', ...p);
2685
+ if (process.platform === 'win32') return [b('bin', 'ao-windows-amd64.exe'), b('bin', 'ao.exe'), b('ao.exe')];
2686
+ const plat = process.platform === 'darwin' ? 'darwin' : 'linux';
2687
+ const arch = process.arch === 'arm64' ? 'arm64' : 'amd64';
2688
+ return [b('bin', `ao-${plat}-${arch}`), b('ao-daemon'), b('bin', 'ao'), b('ao')];
2689
+ }
2690
+
2691
+ // Picks the binary this run may install. A local checkout counts only when it
2692
+ // is a release source (clean, HEAD on a tag) or AOS_AO_DEV_BUILD=1 opts in;
2693
+ // otherwise the published release package is used, never the dev checkout.
2694
+ function selectAoCandidate({
2695
+ checkoutDir, isLocalGitRepo, releaseDir,
2696
+ devOptIn = process.env.AOS_AO_DEV_BUILD === '1',
2697
+ dryRun = DRY_RUN,
2698
+ checkWorkspace = checkAoWorkspace,
2699
+ download = (dir) => downloadOrUpdateModule(AO_PKG, dir, 'BDB Agent Orchestrator'),
2700
+ build = buildAoFromSource,
2701
+ candidatesIn = aoPlatformBinaries,
2702
+ }) {
2703
+ if (isLocalGitRepo) {
2704
+ const ws = checkWorkspace(checkoutDir);
2705
+ if (ws.ok) {
2706
+ if (dryRun) return { bin: null, source: 'dry-run' };
2707
+ // A prebuilt binary counts only when it was built clean from this exact HEAD.
2708
+ const prebuilt = candidatesIn(checkoutDir).find((p) => {
2709
+ const info = readGoBuildInfo(p);
2710
+ return info && !info.modified && info.revision === ws.head;
2711
+ });
2712
+ return { bin: prebuilt || build(checkoutDir), source: 'checkout' };
2713
+ }
2714
+ if (devOptIn) {
2715
+ log.warn(`AOS_AO_DEV_BUILD=1: building ao from ${checkoutDir} HEAD although ${ws.reason}; a downgrade is still refused.`);
2716
+ if (dryRun) return { bin: null, source: 'dry-run' };
2717
+ return { bin: build(checkoutDir), source: 'dev', allowDirty: true };
2718
+ }
2719
+ 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.`);
2720
+ }
2721
+ if (fs.existsSync(path.join(releaseDir, '.git'))) {
2722
+ log.warn(`${releaseDir} is a git checkout; not downloading the release over it. Keeping the installed ao.`);
2723
+ return { bin: null, source: 'release-dir-is-checkout' };
2724
+ }
2725
+ if (dryRun) {
2726
+ log.step(`[dry-run] would download ${AO_PKG} to ${releaseDir}`);
2727
+ return { bin: null, source: 'dry-run' };
2728
+ }
2729
+ if (!download(releaseDir)) {
2730
+ log.warn(`Could not download ${AO_PKG}; keeping the installed ao.`);
2731
+ return { bin: null, source: 'download-failed' };
2602
2732
  }
2733
+ return { bin: candidatesIn(releaseDir).find((p) => fs.existsSync(p)) || null, source: 'release' };
2734
+ }
2603
2735
 
2604
- if (!daemonBin) {
2605
- log.warn(`AO binary missing in ${osAgentDir} — package layout changed or Go build needed.`);
2736
+ async function installOSAgentWorkspace() {
2737
+ if (!aoSupportedHere()) {
2738
+ log.warn(`AO is not supported on ${process.platform}/${process.arch}.`);
2606
2739
  return false;
2607
2740
  }
2608
2741
 
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.');
2742
+ const releaseDir = path.join(moduleBasePath(), 'bdb-agent-orchestrator');
2743
+ const checkoutDir = [
2744
+ path.join(homeDir, 'dev', 'agents', 'bdb-agent-orchestrator'),
2745
+ path.join(homeDir, 'dev', 'bdb-dev', 'bdb-agent-orchestrator'),
2746
+ ].find((d) => fs.existsSync(path.join(d, '.git')));
2747
+ if (checkoutDir) log.step(`Found a local AO checkout: ${checkoutDir}`);
2748
+
2749
+ const pick = selectAoCandidate({ checkoutDir, isLocalGitRepo: !!checkoutDir, releaseDir });
2750
+ if (DRY_RUN) {
2751
+ log.step('[dry-run] would install the ao binary and run `ao service install`');
2752
+ return;
2753
+ }
2754
+
2755
+ const binTarget = aoBinTarget();
2756
+ const installedExists = fs.existsSync(binTarget);
2757
+ let src = null;
2758
+ let candidate = null;
2759
+ if (pick.bin) {
2760
+ candidate = readGoBuildInfo(pick.bin);
2761
+ const installed = installedExists ? readGoBuildInfo(binTarget) : null;
2762
+ const daemonRunning = installedExists && !installed
2763
+ ? (readAoDaemonIdentity() !== null || await verifyDaemonListening(3101, 'AO Orchestrator', 1500))
2764
+ : true;
2765
+ const verdict = decideAoInstall({ installedExists, installed, candidate, allowDirty: !!pick.allowDirty, candidateIsRelease: pick.source === 'release', daemonRunning });
2766
+ if (verdict.install) src = pick.bin;
2767
+ else log.step(`Keeping ${binTarget}: ${verdict.reason}.`);
2615
2768
  }
2616
2769
 
2770
+ if (!src && !installedExists) {
2771
+ log.warn(`No ao binary could be installed (${pick.source}).`);
2772
+ return false;
2773
+ }
2774
+ // Unchanged binary and a live daemon: nothing to restart.
2775
+ if (!src && await verifyDaemonListening(3101, 'AO Orchestrator', 1500)) {
2776
+ log.success('AO Orchestrator running on http://localhost:3101');
2777
+ return;
2778
+ }
2779
+
2780
+ const result = await installAoBinary({
2781
+ src,
2782
+ dest: binTarget,
2783
+ serviceInstall: (bin) => aoServiceInstall(bin),
2784
+ waitForDaemon: () => verifyDaemonListening(3101, 'AO Orchestrator', AO_DAEMON_WAIT_MS),
2785
+ });
2786
+ if (result.ok && src) {
2787
+ const pkgJson = readJsonFile(path.join(pick.source === 'release' ? releaseDir : checkoutDir, 'package.json')) || {};
2788
+ saveManifest({ ao: { version: pick.source === 'dev' ? null : (pkgJson.version || null), revision: candidate ? candidate.revision : null, modified: !!(candidate && candidate.modified), installedAt: new Date().toISOString() } });
2789
+ }
2790
+ return result.ok ? undefined : false;
2791
+ }
2792
+
2793
+ function aoServiceInstall(binTarget) {
2617
2794
  // macOS only: code-sign ad-hoc to prevent AMFI SIGKILL
2618
- if (isMac) {
2795
+ if (process.platform === 'darwin') {
2619
2796
  try {
2620
2797
  execFileSync('codesign', ['-v', binTarget], { stdio: 'ignore' });
2621
2798
  } catch {
@@ -2625,35 +2802,17 @@ async function installOSAgentWorkspace() {
2625
2802
  log.step('Signed it ad-hoc.');
2626
2803
  } catch (e) { log.warn(`Could not sign it: ${e.message}`); }
2627
2804
  }
2628
- }
2629
-
2630
- // AO installs its own service, and does it for macOS, Windows and Linux.
2631
- if (isMac) {
2632
2805
  const legacyPlist = path.join(homeDir, 'Library', 'LaunchAgents', 'com.bdb.agent-workspace.plist');
2633
2806
  if (fs.existsSync(legacyPlist)) {
2634
2807
  try { execFileSync('launchctl', ['unload', legacyPlist], { stdio: 'ignore' }); } catch (e) { logDebug(e, 'unload legacy ao agent'); }
2635
2808
  try { fs.unlinkSync(legacyPlist); log.step('Removed the old com.bdb.agent-workspace service.'); } catch (e) { logDebug(e, 'remove legacy plist'); }
2636
2809
  }
2637
2810
  }
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}`);
2811
+ // AO installs its own service, and does it for macOS, Windows and Linux.
2812
+ try {
2813
+ execFileSync(binTarget, ['service', 'install'], { stdio: ['ignore', 'pipe', 'pipe'] });
2814
+ } catch (e) {
2815
+ throw new Error(describeExecError(e));
2657
2816
  }
2658
2817
  }
2659
2818
  async function promptMemBIngestion(mcpCodeTarget) {
@@ -2715,6 +2874,18 @@ function verifyEcosystemInstallation() {
2715
2874
  ];
2716
2875
 
2717
2876
  for (const mod of modules) {
2877
+ if (mod.pkg === AO_PKG) {
2878
+ const bin = aoBinTarget();
2879
+ const exists = fs.existsSync(bin);
2880
+ const build = exists ? readGoBuildInfo(bin) : null;
2881
+ const record = (readJsonFile(path.join(homeDir, '.agents', '.bdb-manifest.json')) || {}).ao || null;
2882
+ const version = trustedAoVersion(build, record);
2883
+ const latest = version ? (newestDistTag(AO_PKG, version) || {}).version : null;
2884
+ const s = describeAoVersion({ exists, build, record, latest });
2885
+ const mark = !exists ? `${colors.dim}⚪` : s.updateAvailable ? `${colors.yellow}⚠️ ` : build && !build.modified ? `${colors.green}✅` : `${colors.yellow}⚠️ `;
2886
+ console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${mark} ${s.text}${colors.reset}`);
2887
+ continue;
2888
+ }
2718
2889
  let modulePkgPath = null;
2719
2890
  for (const p of mod.paths) {
2720
2891
  const candidate = path.join(p, 'package.json');
@@ -2727,29 +2898,9 @@ function verifyEcosystemInstallation() {
2727
2898
  if (modulePkgPath) {
2728
2899
  try {
2729
2900
  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}`);
2901
+ const newer = newestDistTag(mod.pkg, localVer);
2902
+ if (newer) {
2903
+ console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.yellow}⚠️ Update available (v${localVer} ➔ v${newer.version} (${newer.tag}))${colors.reset}`);
2753
2904
  } else {
2754
2905
  console.log(` • ${colors.bold}${mod.name.padEnd(35)}${colors.reset} ➔ ${colors.green}✅ v${localVer} (Up to date)${colors.reset}`);
2755
2906
  }
@@ -3473,6 +3624,14 @@ function compileClaudeAgents(agents, targetDir, pipelineConfig = null) {
3473
3624
  }
3474
3625
  }
3475
3626
 
3627
+ // .claude/agents/<slug>.md is shipped and copied to ~/.claude/agents by the
3628
+ // manifest-tracked "copy harness directories" step. Compiling the same slug
3629
+ // there first made the two writers fight: the compiled bytes never matched the
3630
+ // manifest, so every update flagged all 13 personas as user-edited.
3631
+ function agentsNotShippedAsFiles(agents, shippedDir = path.join(srcDir, '.claude', 'agents')) {
3632
+ return agents.filter((a) => !fs.existsSync(path.join(shippedDir, `${a.name.toLowerCase().replace(/_/g, '-')}.md`)));
3633
+ }
3634
+
3476
3635
  // Generates .opencode/agents/<name>.md. Emits description, mode: subagent, and model.
3477
3636
  function compileOpenCodeAgents(agents, targetDir, pipelineConfig = null) {
3478
3637
  fs.mkdirSync(targetDir, { recursive: true });
@@ -3575,8 +3734,12 @@ function injectHarnessRules() {
3575
3734
  if (sources.ok) {
3576
3735
  const { globalRules, startcycleContent, agentsMdContent } = sources.value;
3577
3736
 
3737
+ // Run from $HOME, these project files ARE the global files that the
3738
+ // manifest-tracked harness copy below writes; a second writer would
3739
+ // make every update see them as user-edited.
3740
+ const cwdIsHome = path.resolve(currentDir) === path.resolve(homeDir);
3578
3741
  const cursorRulesDir = path.join(currentDir, '.cursor', 'rules');
3579
- installStep(`write the Cursor rules to ${cursorRulesDir}`, () => {
3742
+ if (!cwdIsHome) installStep(`write the Cursor rules to ${cursorRulesDir}`, () => {
3580
3743
  fs.mkdirSync(cursorRulesDir, { recursive: true });
3581
3744
  fs.writeFileSync(path.join(cursorRulesDir, '000_global_rules.mdc'), `---\nname: global-rules\ndescription: Global BDB Agent Rules\n---\n\n${globalRules}`);
3582
3745
  if (startcycleContent) {
@@ -3618,7 +3781,7 @@ function injectHarnessRules() {
3618
3781
  installStep('compile Claude Code subagents', () => {
3619
3782
  if (agentsMdContent) {
3620
3783
  const pipelineConfig = loadPipelineConfig();
3621
- const agents = parseAgentsMd(agentsMdContent);
3784
+ const agents = agentsNotShippedAsFiles(parseAgentsMd(agentsMdContent));
3622
3785
  const claudeAgentsDir = path.join(homeDir, '.claude', 'agents');
3623
3786
  compileClaudeAgents(agents, claudeAgentsDir, pipelineConfig);
3624
3787
  log.step(`Compiled AGENTS.md to Claude Code subagents in ${claudeAgentsDir}`);
@@ -4095,9 +4258,12 @@ function selfCheckAgyHooks(hooksPath, spawn = spawnSync) {
4095
4258
  const failures = [];
4096
4259
  for (const command of [...new Set(commands)]) {
4097
4260
  // 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' } });
4261
+ const r = spawn(shell, [flag, command], { cwd: path.dirname(hooksPath), input: '{}', encoding: 'utf8', timeout: 20000, windowsHide: true, env: { ...process.env, AGENTTRAIL_PORT: '1' } });
4099
4262
  const stderr = String((r && r.stderr) || '');
4100
4263
  let reason = null;
4264
+ // A slow machine (memb-inject reads the store) can exceed the limit without the hook being broken:
4265
+ // the failures this check exists for (bad path, missing module, non-zero exit) are immediate.
4266
+ if (r && r.error && r.error.code === 'ETIMEDOUT') { log.warn(`agy hook self-check timed out, not treated as a failure: ${command}`); continue; }
4101
4267
  if (!r || r.error) reason = `spawn failed: ${r && r.error ? r.error.message : 'no result'}`;
4102
4268
  else if (r.status !== 0) reason = `exit ${r.status}\n ${stderr.trim().slice(0, 500)}`;
4103
4269
  else if (/MODULE_NOT_FOUND|Cannot find module/.test(stderr)) reason = `module not found\n ${stderr.trim().slice(0, 500)}`;
@@ -5420,20 +5586,22 @@ async function runQuickUpdate(installState) {
5420
5586
 
5421
5587
 
5422
5588
  function buildAoAnnouncementBanner() {
5589
+ const W = 78;
5590
+ const row = (text, style = '') => `\x1b[36m│\x1b[0m${style}${text.padEnd(W)}\x1b[0m\x1b[36m│\x1b[0m`;
5423
5591
  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'
5592
+ `\x1b[36m╭${'─'.repeat(W)}╮\x1b[0m`,
5593
+ row(''),
5594
+ row(' BDB AGENT ORCHESTRATOR APP — FINAL BETA NOW AVAILABLE', '\x1b[1m\x1b[35m'),
5595
+ row(''),
5596
+ row(' The next generation of cross-harness multi-agent orchestration'),
5597
+ row(' is now open to every user as a final beta.'),
5598
+ row(''),
5599
+ row(' • Dashboard & WebUI: http://localhost:3101'),
5600
+ row(' • Service command: ao service install (enables the background service)'),
5601
+ row(' • Quick launch: ao open or ao service status'),
5602
+ row(' • Features: session telemetry, live AgentTrail, multi-workspaces'),
5603
+ row(''),
5604
+ `\x1b[36m╰${'─'.repeat(W)}╯\x1b[0m\n`
5437
5605
  ].join('\n');
5438
5606
  }
5439
5607
 
@@ -5961,6 +6129,16 @@ if (require.main === module) {
5961
6129
  module.exports = {
5962
6130
  installBinaryAtomically,
5963
6131
  installOSAgentWorkspace,
6132
+ readGoBuildInfo,
6133
+ parseGoBuildInfo,
6134
+ decideAoInstall,
6135
+ checkAoWorkspace,
6136
+ installAoBinary,
6137
+ readAoDaemonIdentity,
6138
+ selectAoCandidate,
6139
+ describeAoVersion,
6140
+ buildAoAnnouncementBanner,
6141
+ agentsNotShippedAsFiles,
5964
6142
  downloadOrUpdateModule,
5965
6143
  detectPlatforms,
5966
6144
  markPlatformsExplicit,