@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.
- package/.claude/hooks/aos-bus.mjs +146 -0
- package/.claude/hooks/go-gate.mjs +2 -1
- package/.opencode/plugins/bdb-aos.js +111 -5
- package/bin/aos-acp.mjs +7 -3
- package/bin/aos-doctor.mjs +21 -6
- package/docs/master-session-acp.md +15 -0
- package/docs/sessions/audit-agents.md +2 -0
- package/installer.js +341 -163
- package/package.json +3 -2
- package/scripts/validate-skills.mjs +29 -6
- package/skills/basic/godmode-shipping/SKILL.md +0 -1
- package/skills/basic/master-session/SKILL.md +1 -1
- package/skills/basic/startcycle/SKILL.md +0 -1
- package/skills/basic/startcycle-graph/SKILL.md +0 -1
- package/skills/global_config/ask-tim/SKILL.md +0 -1
- package/skills/global_config/grill-me/SKILL.md +0 -1
- package/skills/global_config/grill-with-docs/SKILL.md +0 -1
- package/skills/playbooks/pb-bug-fix/SKILL.md +48 -0
- package/skills/playbooks/pb-ci-fix/SKILL.md +0 -1
- package/skills/playbooks/pb-clip-from-moodboard/SKILL.md +55 -0
- package/skills/playbooks/pb-crew-call-sheet/SKILL.md +40 -0
- package/skills/playbooks/pb-deploy-saas/SKILL.md +44 -0
- package/skills/playbooks/pb-docs-site/SKILL.md +44 -0
- package/skills/playbooks/pb-event-tracker/SKILL.md +0 -1
- package/skills/playbooks/pb-focus-chunks/SKILL.md +40 -0
- package/skills/playbooks/pb-handover/SKILL.md +41 -0
- package/skills/playbooks/pb-harness-work/SKILL.md +64 -0
- package/skills/playbooks/pb-health-weekly/SKILL.md +43 -0
- package/skills/playbooks/pb-idea-to-launch/SKILL.md +51 -0
- package/skills/playbooks/pb-image-to-3d/SKILL.md +46 -0
- package/skills/playbooks/pb-inbox-zero/SKILL.md +39 -0
- package/skills/playbooks/pb-invoice-check/SKILL.md +41 -0
- package/skills/playbooks/pb-landing-page/SKILL.md +46 -0
- package/skills/playbooks/pb-launch-video/SKILL.md +49 -0
- package/skills/playbooks/pb-machine-setup/SKILL.md +44 -0
- package/skills/playbooks/pb-master/SKILL.md +53 -0
- package/skills/playbooks/pb-meeting-actions/SKILL.md +0 -1
- package/skills/playbooks/pb-newsletter/SKILL.md +43 -0
- package/skills/playbooks/pb-offer/SKILL.md +39 -0
- package/skills/playbooks/pb-open-source/SKILL.md +50 -0
- package/skills/playbooks/pb-pcb-to-case/SKILL.md +46 -0
- package/skills/playbooks/pb-project-new/SKILL.md +0 -1
- package/skills/playbooks/pb-redesign-app/SKILL.md +47 -0
- package/skills/playbooks/pb-release-aos/SKILL.md +50 -0
- package/skills/playbooks/pb-security-sweep/SKILL.md +48 -0
- package/skills/playbooks/pb-ship/SKILL.md +49 -0
- package/skills/playbooks/pb-show-build/SKILL.md +48 -0
- package/skills/playbooks/pb-social-pack/SKILL.md +47 -0
- package/skills/playbooks/pb-todo/SKILL.md +41 -0
- package/skills/playbooks/pb-week-plan/SKILL.md +0 -1
- 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
|
|
501
|
-
//
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
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
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
-
|
|
2527
|
-
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
-
|
|
2545
|
-
|
|
2546
|
-
if (
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
-
|
|
2634
|
+
}
|
|
2576
2635
|
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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
|
-
|
|
2605
|
-
|
|
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
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
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 (
|
|
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
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
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
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
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:
|
|
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
|
-
|
|
5425
|
-
'
|
|
5426
|
-
'
|
|
5427
|
-
'
|
|
5428
|
-
'
|
|
5429
|
-
'
|
|
5430
|
-
'
|
|
5431
|
-
'
|
|
5432
|
-
'
|
|
5433
|
-
'
|
|
5434
|
-
'
|
|
5435
|
-
'
|
|
5436
|
-
|
|
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,
|