@hybridlabor-api/aos 4.13.1 → 4.14.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 (151) hide show
  1. package/.agents/AGENTS.md +8 -0
  2. package/.agents/nodes.json +5 -2
  3. package/.claude/hooks/conventional-commits.mjs +14 -15
  4. package/.claude/hooks/env-file-protection.mjs +14 -15
  5. package/.claude/hooks/go-gate.mjs +157 -13
  6. package/.claude/hooks/go-token.mjs +55 -0
  7. package/.claude/hooks/memb-inject.mjs +75 -62
  8. package/.claude/hooks/trail-autostart.mjs +27 -0
  9. package/.claude/settings.json +18 -0
  10. package/.claude/workflows/startcycle-dispatch.mjs +11 -4
  11. package/.opencode/plugins/bdb-aos.js +98 -121
  12. package/.opencode/plugins/lib/trail-autostart.js +38 -0
  13. package/CLAUDE.md +1 -1
  14. package/README.de.md +6 -10
  15. package/README.md +6 -10
  16. package/README.pt.md +6 -10
  17. package/THIRD_PARTY_NOTICES.md +19 -3
  18. package/assets/header-v5.png +0 -0
  19. package/bin/aos-acp.mjs +211 -0
  20. package/bin/aos-doctor.mjs +1 -1
  21. package/bin/aos-uninstall.mjs +2 -2
  22. package/docs/master-session-acp.md +51 -0
  23. package/installer.js +398 -65
  24. package/mcps/mcsc/packages/mcp/server.js +6 -7
  25. package/package.json +4 -3
  26. package/scripts/validate-skills.mjs +76 -0
  27. package/skills/basic/bdbmediastorm/SKILL.md +1 -1
  28. package/skills/basic/godmode-shipping/SKILL.md +3 -0
  29. package/skills/basic/master-session/SKILL.md +89 -0
  30. package/skills/basic/startcycle/SKILL.md +1 -1
  31. package/skills/basic/startcycle-graph/SKILL.md +2 -2
  32. package/skills/basic/startcycle-graph-user/SKILL.md +1 -1
  33. package/skills/basic/teamwork-preview/SKILL.md +1 -1
  34. package/skills/bdbrainstorm/SKILL.md +7 -1
  35. package/skills/global_config/agentic-harness-patterns/SKILL.md +257 -0
  36. package/skills/global_config/agentic-harness-patterns/metadata.json +10 -0
  37. package/skills/global_config/agentic-harness-patterns/references/agent-orchestration-pattern.md +97 -0
  38. package/skills/global_config/agentic-harness-patterns/references/bootstrap-sequence-pattern.md +106 -0
  39. package/skills/global_config/agentic-harness-patterns/references/context-engineering/compress-pattern.md +78 -0
  40. package/skills/global_config/agentic-harness-patterns/references/context-engineering/isolate-pattern.md +82 -0
  41. package/skills/global_config/agentic-harness-patterns/references/context-engineering/select-pattern.md +86 -0
  42. package/skills/global_config/agentic-harness-patterns/references/context-engineering-pattern.md +29 -0
  43. package/skills/global_config/agentic-harness-patterns/references/hook-lifecycle-pattern.md +111 -0
  44. package/skills/global_config/agentic-harness-patterns/references/memory-persistence-pattern.md +109 -0
  45. package/skills/global_config/agentic-harness-patterns/references/permission-gate-pattern.md +111 -0
  46. package/skills/global_config/agentic-harness-patterns/references/skill-runtime-pattern.md +104 -0
  47. package/skills/global_config/agentic-harness-patterns/references/task-decomposition-pattern.md +92 -0
  48. package/skills/global_config/agentic-harness-patterns/references/tool-registry-pattern.md +101 -0
  49. package/skills/global_config/agenttrail/SKILL.md +8 -0
  50. package/skills/global_config/agenttrail/bin/agenttrail.mjs +14 -0
  51. package/skills/global_config/agenttrail/bin/ensure.mjs +118 -0
  52. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  53. package/skills/global_config/bdb-memb-mcp/SKILL.md +4 -5
  54. package/skills/global_config/bdb-visual-edit/SKILL.md +51 -0
  55. package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +59 -0
  56. package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +27 -0
  57. package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +123 -0
  58. package/skills/global_config/factory-collect/SKILL.md +74 -0
  59. package/skills/global_config/factory-human-digest/SKILL.md +92 -0
  60. package/skills/global_config/factory-lookback/SKILL.md +95 -0
  61. package/skills/global_config/factory-review-prs/SKILL.md +63 -0
  62. package/skills/global_config/git-pr-review/SKILL.md +3 -0
  63. package/skills/global_config/grilling/SKILL.md +2 -0
  64. package/skills/global_config/mcsc/SKILL.md +1 -1
  65. package/skills/global_config/plan-arbiter/SKILL.md +125 -0
  66. package/skills/global_config/plan-canvas/SKILL.md +63 -9
  67. package/skills/global_config/plan-canvas/scripts/lib/loopback-guard.js +19 -3
  68. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +285 -0
  69. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/agent-trail.js +129 -0
  70. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/board-client.js +124 -0
  71. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/canvas.mdx +19 -0
  72. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/plan.mdx +18 -0
  73. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/recap-demo/plan.mdx +72 -0
  74. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/README.md +29 -0
  75. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/architecture.json +30 -0
  76. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/00_architecture.html +14950 -0
  77. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/canvas.mdx +511 -0
  78. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/plan.mdx +208 -0
  79. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/recap/plan.mdx +102 -0
  80. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/standard/plan.md +136 -0
  81. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/canvas.mdx +124 -0
  82. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/plan.mdx +37 -0
  83. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/index.js +188 -0
  84. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/kit.js +123 -0
  85. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/mdx.js +411 -0
  86. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +1291 -0
  87. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/meta.json +1 -0
  88. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/plan.mdx +195 -0
  89. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/standard.md +95 -0
  90. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/meta.json +1 -0
  91. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/plan.mdx +105 -0
  92. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/standard.md +76 -0
  93. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/canvas.mdx +81 -0
  94. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/meta.json +1 -0
  95. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/plan.mdx +145 -0
  96. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/standard.md +76 -0
  97. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/meta.json +1 -0
  98. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/plan.mdx +172 -0
  99. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/standard.md +100 -0
  100. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/meta.json +1 -0
  101. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/plan.mdx +67 -0
  102. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/standard.md +49 -0
  103. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/canvas.mdx +63 -0
  104. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/meta.json +1 -0
  105. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/plan.mdx +49 -0
  106. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/standard.md +39 -0
  107. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/meta.json +1 -0
  108. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/plan.mdx +118 -0
  109. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/standard.md +57 -0
  110. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/meta.json +1 -0
  111. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/plan.mdx +173 -0
  112. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/standard.md +96 -0
  113. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/meta.json +1 -0
  114. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/plan.mdx +91 -0
  115. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/standard.md +54 -0
  116. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/canvas.mdx +53 -0
  117. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/meta.json +1 -0
  118. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/plan.mdx +225 -0
  119. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/standard.md +111 -0
  120. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/theme.css +472 -0
  121. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/trail.js +216 -0
  122. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/markdown.js +1 -1
  123. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +37 -4
  124. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +125 -29
  125. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +196 -8
  126. package/skills/global_config/pr-recap/SKILL.md +47 -0
  127. package/skills/global_config/pr-recap/scripts/pr-recap.mjs +200 -0
  128. package/skills/global_config/quick-recap/SKILL.md +55 -0
  129. package/skills/global_config/stay-within-limits/SKILL.md +85 -0
  130. package/skills/global_config/triage/SKILL.md +3 -0
  131. package/skills/global_config/visual-edit/README.md +96 -0
  132. package/skills/global_config/visual-edit/SKILL.md +615 -0
  133. package/skills/global_config/visual-plan/README.md +93 -0
  134. package/skills/global_config/visual-plan/SKILL.md +544 -0
  135. package/skills/global_config/visual-plan/references/canvas.md +139 -0
  136. package/skills/global_config/visual-plan/references/connection.md +51 -0
  137. package/skills/global_config/visual-plan/references/document-quality.md +186 -0
  138. package/skills/global_config/visual-plan/references/exemplar.md +62 -0
  139. package/skills/global_config/visual-plan/references/local-files.md +99 -0
  140. package/skills/global_config/visual-plan/references/wireframe.md +319 -0
  141. package/skills/global_config/visual-recap/README.md +103 -0
  142. package/skills/global_config/visual-recap/SKILL.md +560 -0
  143. package/skills/global_config/visual-recap/references/connection.md +51 -0
  144. package/skills/global_config/visual-recap/references/local-files.md +99 -0
  145. package/skills/global_config/visual-recap/references/wireframe.md +319 -0
  146. package/skills/playbooks/pb-ci-fix/SKILL.md +49 -0
  147. package/skills/playbooks/pb-event-tracker/SKILL.md +45 -0
  148. package/skills/playbooks/pb-meeting-actions/SKILL.md +42 -0
  149. package/skills/playbooks/pb-project-new/SKILL.md +48 -0
  150. package/skills/playbooks/pb-week-plan/SKILL.md +45 -0
  151. package/assets/header-v4.jpg +0 -0
package/installer.js CHANGED
@@ -526,12 +526,24 @@ function loadInstallManifest(customPath = null) {
526
526
  return {};
527
527
  }
528
528
 
529
+ // Temp file in the same directory + rename: a crash mid-write cannot leave a truncated manifest.
530
+ function writeFileAtomic(filePath, data) {
531
+ const tmp = `${filePath}.${process.pid}.tmp`;
532
+ try {
533
+ fs.writeFileSync(tmp, data);
534
+ fs.renameSync(tmp, filePath);
535
+ } catch (e) {
536
+ try { fs.unlinkSync(tmp); } catch (_) {}
537
+ throw e;
538
+ }
539
+ }
540
+
529
541
  function saveInstallManifest(manifest, customPath = null) {
530
542
  if (DRY_RUN) return;
531
543
  const manifestPath = customPath || getInstallManifestPath();
532
544
  try {
533
545
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
534
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
546
+ writeFileAtomic(manifestPath, JSON.stringify(manifest, null, 2));
535
547
  } catch (e) {
536
548
  log.warn(`Could not write install manifest: ${e.message}`);
537
549
  }
@@ -978,7 +990,11 @@ function detectInstallState() {
978
990
  } catch (e) { logDebug(e, 'operation'); }
979
991
  }
980
992
  }
981
- if (!localVersion) localVersion = '3.8.0';
993
+ if (!localVersion) {
994
+ // ponytail: placeholder only feeds the version display/compare; pruning is driven by the file manifest, not this value.
995
+ log.step('Previous AOS version is unknown (no manifest); assuming 3.8.0 for the update comparison only.');
996
+ localVersion = '3.8.0';
997
+ }
982
998
  }
983
999
 
984
1000
  const basePath = scriptDir.includes('_npx') ? path.join(homeDir, '.agents') : path.dirname(srcDir);
@@ -1039,7 +1055,7 @@ function saveManifest(data = {}) {
1039
1055
  platform: process.platform,
1040
1056
  ...data
1041
1057
  });
1042
- fs.writeFileSync(manifestPath, JSON.stringify(updated, null, 2));
1058
+ writeFileAtomic(manifestPath, JSON.stringify(updated, null, 2));
1043
1059
  } catch (e) { logDebug(e, 'manifest read/parse'); }
1044
1060
  }
1045
1061
 
@@ -1218,6 +1234,20 @@ function syncSkillEntry(fullPath, dirName, targetSkillDir, excludeSkills) {
1218
1234
  }
1219
1235
  }
1220
1236
 
1237
+ function redactSecrets(text) {
1238
+ return String(text)
1239
+ .replace(/([\w.-]*(?:key|token|secret|passw(?:or)?d|auth|credential)[\w.-]*\s*[=:]\s*)("[^"]*"|'[^']*'|\S+)/gi, '$1[redacted]')
1240
+ .replace(/\bBearer\s+\S+/gi, 'Bearer [redacted]')
1241
+ .replace(/\b(?:sk|ghp|gho|ghs|xox[a-z])[-_][A-Za-z0-9_-]{16,}/g, '[redacted]');
1242
+ }
1243
+
1244
+ // execFileSync error -> one message that keeps the child's own stderr/stdout (last 2 KB, redacted).
1245
+ function describeExecError(e) {
1246
+ const out = [e && e.stderr, e && e.stdout].map((b) => (b ? b.toString() : '').trim()).filter(Boolean).join('\n');
1247
+ const head = redactSecrets(String((e && e.message) || e).split('\n')[0]).slice(0, 300);
1248
+ return out ? `${head}\n${redactSecrets(out.slice(-2048))}` : head;
1249
+ }
1250
+
1221
1251
  function installStep(what, fn, hint) {
1222
1252
  try {
1223
1253
  return { ok: true, value: fn() };
@@ -1937,7 +1967,7 @@ async function installMemB(interactive) {
1937
1967
  // banner. Stop at the module that could not be fetched.
1938
1968
  if (!downloadOrUpdateModule('@hybridlabor-api/memb', membDir, 'memB Vector Engine')) {
1939
1969
  log.warn('Skipping memB setup: the module could not be downloaded.');
1940
- return;
1970
+ return false;
1941
1971
  }
1942
1972
  await installDeja();
1943
1973
  if (DRY_RUN) {
@@ -1945,6 +1975,7 @@ async function installMemB(interactive) {
1945
1975
  return;
1946
1976
  }
1947
1977
 
1978
+ let membOk = true;
1948
1979
  const reqFile = path.join(membDir, 'requirements.txt');
1949
1980
  const serverPy = path.join(membDir, 'src', 'backend', 'server.py');
1950
1981
  if (fs.existsSync(reqFile)) {
@@ -2017,10 +2048,12 @@ async function installMemB(interactive) {
2017
2048
  );
2018
2049
  if (repaired && !verifyPythonImports(venvPython, membDir, importCheck)) {
2019
2050
  log.warn('memB dependency repair did not pass the import check; inspect the venv before starting the daemon.');
2051
+ membOk = false;
2020
2052
  }
2021
2053
  }
2022
2054
  } catch (e) {
2023
2055
  log.warn(`Failed memB standalone venv setup: ${e.message}`);
2056
+ membOk = false;
2024
2057
  }
2025
2058
  }
2026
2059
 
@@ -2082,13 +2115,14 @@ async function installMemB(interactive) {
2082
2115
  } catch (e) { logDebug(e, 'windows memb daemon setup'); }
2083
2116
  }
2084
2117
  }
2118
+ return membOk;
2085
2119
  }
2086
2120
 
2087
2121
  async function installSynapse() {
2088
2122
  const synapseDir = path.join(moduleBasePath(), 'bdb-synapse');
2089
2123
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-synapse', synapseDir, 'BDB Synapse')) {
2090
2124
  log.warn('Skipping Synapse setup: the module could not be downloaded.');
2091
- return;
2125
+ return false;
2092
2126
  }
2093
2127
  if (DRY_RUN) {
2094
2128
  log.step('[dry-run] would link synapse binary into ~/.local/bin/synapse + setup background daemon');
@@ -2217,6 +2251,7 @@ async function installSynapse() {
2217
2251
  }
2218
2252
  } else {
2219
2253
  log.warn(`No pre-built binary for this platform. Compile with: cd "${synapseDir}" && go build -o ${binaryName} ./cmd/synapse/`);
2254
+ return false;
2220
2255
  }
2221
2256
  }
2222
2257
 
@@ -2410,7 +2445,7 @@ async function installCreatorExtension() {
2410
2445
  const creatorDir = path.join(moduleBasePath(), 'bdb-dev-creator-extension');
2411
2446
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-dev-creator-extension', creatorDir, 'BDB Creator Extension')) {
2412
2447
  log.warn('Skipping Creator Extension setup: the module could not be downloaded.');
2413
- return;
2448
+ return false;
2414
2449
  }
2415
2450
  if (DRY_RUN) {
2416
2451
  log.step('[dry-run] would run BDB Creator Extension setup');
@@ -2423,6 +2458,7 @@ async function installCreatorExtension() {
2423
2458
  : spawnSync('node', [installerScript, '--auto'], { stdio: 'inherit', cwd: creatorDir });
2424
2459
  if (setupResult.status !== 0) {
2425
2460
  log.warn(`Creator Extension setup note: exit code ${setupResult.status}`);
2461
+ return false;
2426
2462
  }
2427
2463
  }
2428
2464
  }
@@ -2431,7 +2467,7 @@ async function installHardwarePcb() {
2431
2467
  const hwDir = path.join(moduleBasePath(), 'bdb-hardware-pcb');
2432
2468
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-hardware-pcb', hwDir, 'BDB Hardware & PCB (KiCad + OpenSCAD)')) {
2433
2469
  log.warn('Skipping Hardware & PCB setup: the module could not be downloaded.');
2434
- return;
2470
+ return false;
2435
2471
  }
2436
2472
  if (DRY_RUN) {
2437
2473
  log.step('[dry-run] would run BDB Hardware & PCB setup');
@@ -2442,6 +2478,7 @@ async function installHardwarePcb() {
2442
2478
  const setupResult = spawnSync('node', [installerScript, '--auto'], { stdio: 'inherit', cwd: hwDir });
2443
2479
  if (setupResult.status !== 0) {
2444
2480
  log.warn(`Hardware & PCB setup note: exit code ${setupResult.status}`);
2481
+ return false;
2445
2482
  }
2446
2483
  }
2447
2484
  }
@@ -2450,7 +2487,7 @@ async function installOSRemoteGateway() {
2450
2487
  const remoteDir = path.join(moduleBasePath(), 'bdb-os-remote');
2451
2488
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-os-remote', remoteDir, 'BDB OS Remote Gateway')) {
2452
2489
  log.warn('Skipping OS Remote Gateway setup: the module could not be downloaded.');
2453
- return;
2490
+ return false;
2454
2491
  }
2455
2492
  }
2456
2493
 
@@ -2458,7 +2495,7 @@ async function installDevToolInstaller() {
2458
2495
  const toolInstallerDir = path.join(moduleBasePath(), 'bdb-dev-tool-installer');
2459
2496
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-dev-tool-installer', toolInstallerDir, 'BDB Dev Tool Installer')) {
2460
2497
  log.warn('Skipping Dev Tool Installer setup: the module could not be downloaded.');
2461
- return;
2498
+ return false;
2462
2499
  }
2463
2500
  }
2464
2501
 
@@ -2470,7 +2507,7 @@ function aoSupportedHere() {
2470
2507
  async function installOSAgentWorkspace() {
2471
2508
  if (!aoSupportedHere()) {
2472
2509
  log.warn(`AO is not supported on ${process.platform}/${process.arch}.`);
2473
- return;
2510
+ return false;
2474
2511
  }
2475
2512
 
2476
2513
  const isWin = process.platform === 'win32';
@@ -2491,7 +2528,7 @@ async function installOSAgentWorkspace() {
2491
2528
  } else {
2492
2529
  if (!downloadOrUpdateModule('@hybridlabor-api/bdb-agent-orchestrator', osAgentDir, 'BDB Agent Orchestrator')) {
2493
2530
  log.warn('Skipping AO setup: the module could not be downloaded.');
2494
- return;
2531
+ return false;
2495
2532
  }
2496
2533
  }
2497
2534
  if (DRY_RUN) {
@@ -2566,16 +2603,13 @@ async function installOSAgentWorkspace() {
2566
2603
 
2567
2604
  if (!daemonBin) {
2568
2605
  log.warn(`AO binary missing in ${osAgentDir} — package layout changed or Go build needed.`);
2569
- return;
2606
+ return false;
2570
2607
  }
2571
2608
 
2572
2609
  if (daemonBin !== binTarget) {
2573
2610
  installStep('place the ao binary', () => {
2574
2611
  fs.mkdirSync(localBinDir, { recursive: true });
2575
- fs.copyFileSync(daemonBin, binTarget);
2576
- if (!isWin) {
2577
- fs.chmodSync(binTarget, 0o755);
2578
- }
2612
+ installBinaryAtomically(daemonBin, binTarget);
2579
2613
  log.step(`Installed ao to ${binTarget}`);
2580
2614
  }, 'AO cannot be started without its binary.');
2581
2615
  }
@@ -2602,9 +2636,14 @@ async function installOSAgentWorkspace() {
2602
2636
  }
2603
2637
  }
2604
2638
 
2605
- installStep('register the AO service', () => {
2606
- execFileSync(binTarget, ['service', 'install'], { stdio: 'ignore' });
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
+ }
2607
2645
  }, 'Start it by hand with: ao service install');
2646
+ if (!serviceStep.ok) return false;
2608
2647
 
2609
2648
  // Windows startup entries fire asynchronously — give the service more time
2610
2649
  const aoTimeout = process.platform === 'win32' ? 20000 : 8000;
@@ -2660,6 +2699,9 @@ async function promptMemBIngestion(mcpCodeTarget) {
2660
2699
  }
2661
2700
 
2662
2701
  function verifyEcosystemInstallation() {
2702
+ for (const f of moduleFailures) {
2703
+ console.log(` • ${colors.bold}${f.name}${colors.reset} ➔ ${colors.yellow}❌ NOT installed: ${f.reason.split('\n')[0]}${colors.reset}`);
2704
+ }
2663
2705
  const modules = [
2664
2706
  { name: '1. bdb-synapse', pkg: '@hybridlabor-api/bdb-synapse', paths: [path.join(moduleBasePath(), 'bdb-synapse')] },
2665
2707
  { name: '2. memB', pkg: '@hybridlabor-api/memb', paths: [path.join(moduleBasePath(), 'memB'), path.join(geminiDir, 'config', 'mcps', 'memb-mcp')] },
@@ -2839,6 +2881,18 @@ function getTierExcludeSkills(tier) {
2839
2881
  // primary write above owns its file wholesale, but a secondary store belongs
2840
2882
  // to another product and is very likely to already hold the user's own
2841
2883
  // servers -- so this merges per server key and never replaces the file.
2884
+ // An unattended run has no credentials of its own (empty strings); those must never
2885
+ // replace a value the user already has in a server's env block.
2886
+ function keepExistingEnvValues(incomingServers, previousServers) {
2887
+ for (const [key, incoming] of Object.entries(incomingServers || {})) {
2888
+ const prevEnv = previousServers && previousServers[key] && previousServers[key].env;
2889
+ if (!incoming || typeof incoming !== 'object' || !incoming.env || !prevEnv || typeof prevEnv !== 'object') continue;
2890
+ for (const [k, v] of Object.entries(incoming.env)) {
2891
+ if (v === '' && typeof prevEnv[k] === 'string' && prevEnv[k] !== '') incoming.env[k] = prevEnv[k];
2892
+ }
2893
+ }
2894
+ }
2895
+
2842
2896
  function mirrorMcpServersTo(extraPaths, mcpConfigStr) {
2843
2897
  if (!Array.isArray(extraPaths) || extraPaths.length === 0) return;
2844
2898
 
@@ -2862,7 +2916,9 @@ function mirrorMcpServersTo(extraPaths, mcpConfigStr) {
2862
2916
  }
2863
2917
  const data = existing || {};
2864
2918
  data.mcpServers = data.mcpServers && typeof data.mcpServers === 'object' ? data.mcpServers : {};
2865
- for (const [key, val] of Object.entries(servers)) data.mcpServers[key] = val;
2919
+ const mirrored = JSON.parse(JSON.stringify(servers));
2920
+ keepExistingEnvValues(mirrored, data.mcpServers);
2921
+ for (const [key, val] of Object.entries(mirrored)) data.mcpServers[key] = val;
2866
2922
  fs.mkdirSync(path.dirname(target), { recursive: true });
2867
2923
  // Carries injected API keys, same as the primary config. The `mode`
2868
2924
  // option only applies when writeFileSync CREATES the file -- an
@@ -3182,6 +3238,7 @@ async function installMcpsForTarget(paths, ctx) {
3182
3238
  if (!incoming || typeof incoming !== 'object') return;
3183
3239
  if (!incoming.env && previous.env) incoming.env = previous.env;
3184
3240
  });
3241
+ keepExistingEnvValues(newServers, oldServers);
3185
3242
  oldConfig.mcpServers = Object.assign({}, oldServers, newServers);
3186
3243
  fs.writeFileSync(paths.mcpConfigPath, JSON.stringify(oldConfig, null, 2), { mode: 0o600 });
3187
3244
  try { fs.chmodSync(paths.mcpConfigPath, 0o600); } catch (e) { logDebug(e, 'chmod mcpConfigPath'); }
@@ -3260,7 +3317,16 @@ async function installMcpsForTarget(paths, ctx) {
3260
3317
  // spaces) rather than matching the first alphanumeric run anywhere in the
3261
3318
  // block, so it can't accidentally pick up a word from prose if the heading
3262
3319
  // format changes.
3263
- function parseAgentsMd(content) {
3320
+ function loadCommonSkills(dir = srcDir) {
3321
+ try {
3322
+ const reg = JSON.parse(fs.readFileSync(path.join(dir, '.agents', 'nodes.json'), 'utf8'));
3323
+ return Array.isArray(reg.common) ? reg.common.filter((s) => typeof s === 'string') : [];
3324
+ } catch {
3325
+ return [];
3326
+ }
3327
+ }
3328
+
3329
+ function parseAgentsMd(content, commonSkills = loadCommonSkills()) {
3264
3330
  const blocks = content.split(/\n## /).slice(1);
3265
3331
  const agents = [];
3266
3332
  for (const raw of blocks) {
@@ -3290,7 +3356,7 @@ function parseAgentsMd(content) {
3290
3356
  name,
3291
3357
  role: roleMatch[1].trim(),
3292
3358
  model: modelMatch ? modelMatch[1].trim() : null,
3293
- skills: extractListAfter('Primary Skills'),
3359
+ skills: [...new Set([...extractListAfter('Primary Skills'), ...commonSkills])],
3294
3360
  mcpServers: extractListAfter('MCP Servers'),
3295
3361
  output: outputMatch ? outputMatch[1].trim() : null,
3296
3362
  systemPrompt: raw.trim(),
@@ -3708,6 +3774,21 @@ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data =
3708
3774
  }
3709
3775
  }
3710
3776
 
3777
+ // The plugin imports its decision logic from ./aos-hooks/ next to itself.
3778
+ const hooksSrc = path.join(srcDir, '.claude', 'hooks');
3779
+ if (fs.existsSync(hooksSrc)) {
3780
+ try {
3781
+ copyDirRecursiveSync(hooksSrc, path.join(opencodeDir, 'plugins', 'aos-hooks'));
3782
+ } catch (e) {
3783
+ log.error(`Could not install OpenCode plugin hooks (OpenCode gates inactive): ${e.message}`);
3784
+ }
3785
+ }
3786
+
3787
+ const trailLibSrc = path.join(srcDir, '.opencode', 'plugins', 'lib');
3788
+ if (fs.existsSync(trailLibSrc)) {
3789
+ try { copyDirRecursiveSync(trailLibSrc, path.join(opencodeDir, 'plugins', 'lib')); } catch (e) { log.warn(`Could not install OpenCode plugin lib: ${e.message}`); }
3790
+ }
3791
+
3711
3792
  // Slash-command payloads. Without these the /startcycle-graph command has no
3712
3793
  // native resolution and only survives as a raw-text match in the plugin.
3713
3794
  const commandsSrc = path.join(srcDir, '.opencode', 'commands');
@@ -3906,23 +3987,25 @@ function installGlobalBinaries() {
3906
3987
  // merged result goes to a .bdb-new.json sidecar -- the same recovery pattern
3907
3988
  // the MCP config merge in installMcpsForTarget uses.
3908
3989
  function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
3909
- const bdbHookScripts = ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
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'];
3910
3991
  // memb-inject reads the machine-global memB store under $HOME and is
3911
3992
  // installed once per machine, so it stays $HOME-anchored even inside a
3912
3993
  // project harness -- unlike the two gate hooks, which are per-checkout by
3913
3994
  // design. Pointing it at $CLAUDE_PROJECT_DIR would make it fail on every
3914
3995
  // prompt in any project the harness was never installed into. trail-relay
3915
3996
  // talks to machine-global live-map daemons, so it is anchored the same way.
3916
- const machineGlobalHooks = ['memb-inject.mjs', 'trail-relay.mjs'];
3997
+ const machineGlobalHooks = ['memb-inject.mjs', 'trail-relay.mjs', 'trail-autostart.mjs'];
3917
3998
  const isBdbEntry = (entry) => {
3918
3999
  const cmds = (entry && Array.isArray(entry.hooks) ? entry.hooks : [])
3919
4000
  .map((h) => (h && typeof h.command === 'string' ? h.command : ''))
3920
4001
  .join(' ');
3921
4002
  return bdbHookScripts.some((name) => cmds.includes(name));
3922
4003
  };
3923
- const localize = (cmd) => (projectLocal && !machineGlobalHooks.some((n) => cmd.includes(n))
3924
- ? cmd.split('${HOME}').join('$CLAUDE_PROJECT_DIR')
3925
- : cmd);
4004
+ // Global installs must point at the copies installGlobalHooks put under
4005
+ // $HOME/.claude/hooks; $CLAUDE_PROJECT_DIR would not exist in other projects.
4006
+ const localize = (cmd) => (projectLocal
4007
+ ? (machineGlobalHooks.some((n) => cmd.includes(n)) ? cmd : cmd.split('${HOME}').join('$CLAUDE_PROJECT_DIR'))
4008
+ : cmd.split('$CLAUDE_PROJECT_DIR/.claude/hooks/').join('$HOME/.claude/hooks/'));
3926
4009
  const cloneBdbEntries = (entries) =>
3927
4010
  JSON.parse(JSON.stringify(entries)).map((e) => ({
3928
4011
  ...e,
@@ -3969,7 +4052,16 @@ function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
3969
4052
 
3970
4053
  try {
3971
4054
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
3972
- fs.writeFileSync(settingsPath, JSON.stringify(buildMerged(existing), null, 2) + '\n');
4055
+ const tmpPath = `${settingsPath}.${process.pid}.tmp`;
4056
+ if (existing) {
4057
+ fs.copyFileSync(settingsPath, `${settingsPath}.${process.hrtime.bigint()}.bak`);
4058
+ const dir = path.dirname(settingsPath);
4059
+ const base = path.basename(settingsPath);
4060
+ const baks = fs.readdirSync(dir).filter((f) => f.startsWith(`${base}.`) && /\.\d{13,}\.bak$/.test(f)).sort();
4061
+ for (const old of baks.slice(0, -3)) fs.rmSync(path.join(dir, old), { force: true });
4062
+ }
4063
+ fs.writeFileSync(tmpPath, JSON.stringify(buildMerged(existing), null, 2) + '\n', { mode: existing ? fs.statSync(settingsPath).mode & 0o777 : 0o644 });
4064
+ fs.renameSync(tmpPath, settingsPath);
3973
4065
  } catch (e) {
3974
4066
  log.warn(`Could not write ${settingsPath}: ${e.message}`);
3975
4067
  }
@@ -3979,60 +4071,64 @@ function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
3979
4071
  // ~/.gemini/config/hooks.json), preserving user-defined foreign hooks.
3980
4072
  function mergeAntigravityHooks(hooksPath, { projectLocal = false } = {}) {
3981
4073
  const bdbHookScripts = ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'startcycle-dispatch.mjs', 'trail-relay.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
3982
- const isBdbEntry = (entry) => {
3983
- const cmds = (entry && Array.isArray(entry.hooks) ? entry.hooks : [entry])
3984
- .map((h) => (h && typeof h.command === 'string' ? h.command : (typeof h === 'string' ? h : '')))
3985
- .join(' ');
3986
- return bdbHookScripts.some((name) => cmds.includes(name));
3987
- };
4074
+ const isBdbHandler = (h) => typeof h?.command === 'string' && bdbHookScripts.some((name) => h.command.includes(name));
3988
4075
 
3989
4076
  const baseDir = path.dirname(hooksPath);
3990
4077
  const hooksDir = projectLocal ? path.join(currentDir, '.agents', 'hooks') : path.join(baseDir, 'hooks');
3991
4078
  const workflowsDir = projectLocal ? path.join(currentDir, '.agents', 'workflows') : path.join(baseDir, 'workflows');
3992
4079
  const globalHooksDir = projectLocal ? path.join(homeDir, '.gemini', 'config', 'hooks') : hooksDir;
3993
4080
 
4081
+ const cmd = (dir, script, args = '') => `node "${path.join(dir, script)}"${args}`;
3994
4082
  const bdbHooks = {
3995
4083
  PreToolUse: [
3996
4084
  {
3997
4085
  matcher: "run_command|Bash",
3998
4086
  hooks: [
3999
- { type: "command", command: `node "${path.join(hooksDir, 'go-gate.mjs')}"`, timeout: 10000 },
4000
- { type: "command", command: `node "${path.join(hooksDir, 'conventional-commits.mjs')}"`, timeout: 10000 }
4087
+ { type: "command", command: cmd(hooksDir, 'go-gate.mjs'), timeout: 10 },
4088
+ { type: "command", command: cmd(hooksDir, 'conventional-commits.mjs'), timeout: 10 }
4001
4089
  ]
4002
4090
  },
4003
4091
  {
4004
4092
  matcher: "write_file|edit_file|replace|Write|Edit|MultiEdit",
4005
- hooks: [{ type: "command", command: `node "${path.join(hooksDir, 'env-file-protection.mjs')}"`, timeout: 10000 }]
4093
+ hooks: [{ type: "command", command: cmd(hooksDir, 'env-file-protection.mjs'), timeout: 10 }]
4006
4094
  },
4007
4095
  {
4008
- hooks: [{ type: "command", command: `node "${path.join(globalHooksDir, 'trail-relay.mjs')}" --agent agy --event PreToolUse`, timeout: 2000 }]
4096
+ matcher: "*",
4097
+ hooks: [{ type: "command", command: cmd(globalHooksDir, 'trail-relay.mjs', ' --agent agy --event PreToolUse'), timeout: 2 }]
4009
4098
  }
4010
4099
  ],
4011
4100
  Stop: [
4012
- {
4013
- hooks: [{ type: "command", command: `node "${path.join(hooksDir, 'graph-gate.mjs')}"`, timeout: 10000 }]
4014
- },
4015
- {
4016
- hooks: [{ type: "command", command: `node "${path.join(globalHooksDir, 'trail-relay.mjs')}" --agent agy --event Stop`, timeout: 2000 }]
4017
- }
4101
+ { type: "command", command: cmd(hooksDir, 'graph-gate.mjs'), timeout: 10 },
4102
+ { type: "command", command: cmd(globalHooksDir, 'trail-relay.mjs', ' --agent agy --event Stop'), timeout: 2 }
4018
4103
  ],
4019
4104
  PreInvocation: [
4020
- {
4021
- hooks: [
4022
- { type: "command", command: `node "${path.join(globalHooksDir, 'memb-inject.mjs')}"`, timeout: 8000 },
4023
- { type: "command", command: `node "${path.join(workflowsDir, 'startcycle-dispatch.mjs')}"`, timeout: 30000 }
4024
- ]
4025
- }
4105
+ { type: "command", command: cmd(globalHooksDir, 'memb-inject.mjs'), timeout: 8 },
4106
+ { type: "command", command: cmd(workflowsDir, 'startcycle-dispatch.mjs'), timeout: 30 }
4026
4107
  ]
4027
4108
  };
4028
4109
 
4110
+ // Drops BDB handlers (old wrapped or current shape), keeps foreign ones, and
4111
+ // rewraps flat events so previously written `{ hooks: [...] }` entries migrate.
4112
+ const keepForeign = (event, entries) => {
4113
+ const out = [];
4114
+ for (const e of Array.isArray(entries) ? entries : []) {
4115
+ const nested = e && Array.isArray(e.hooks);
4116
+ if (bdbHooks[event][0].hooks) {
4117
+ if (!nested) { if (!isBdbHandler(e)) out.push(e); continue; }
4118
+ const kept = e.hooks.filter((h) => !isBdbHandler(h));
4119
+ if (kept.length) out.push({ ...e, hooks: kept });
4120
+ } else {
4121
+ out.push(...(nested ? e.hooks : [e]).filter((h) => !isBdbHandler(h)));
4122
+ }
4123
+ }
4124
+ return out;
4125
+ };
4126
+
4029
4127
  const buildMerged = (existing) => {
4030
4128
  const merged = existing && typeof existing === 'object' ? existing : {};
4031
4129
  merged.hooks = merged.hooks && typeof merged.hooks === 'object' ? merged.hooks : {};
4032
4130
  for (const [event, entries] of Object.entries(bdbHooks)) {
4033
- const foreignEntries = (Array.isArray(merged.hooks[event]) ? merged.hooks[event] : [])
4034
- .filter((e) => !isBdbEntry(e));
4035
- merged.hooks[event] = [...foreignEntries, ...entries];
4131
+ merged.hooks[event] = [...keepForeign(event, merged.hooks[event]), ...entries];
4036
4132
  }
4037
4133
  return merged;
4038
4134
  };
@@ -4064,7 +4160,9 @@ function mergeAntigravityHooks(hooksPath, { projectLocal = false } = {}) {
4064
4160
 
4065
4161
  try {
4066
4162
  fs.mkdirSync(path.dirname(hooksPath), { recursive: true });
4067
- fs.writeFileSync(hooksPath, JSON.stringify(buildMerged(existing), null, 2) + '\n');
4163
+ const tmpPath = `${hooksPath}.${process.pid}.tmp`;
4164
+ fs.writeFileSync(tmpPath, JSON.stringify(buildMerged(existing), null, 2) + '\n');
4165
+ fs.renameSync(tmpPath, hooksPath);
4068
4166
  } catch (e) {
4069
4167
  log.warn(`Could not write ${hooksPath}: ${e.message}`);
4070
4168
  }
@@ -4178,13 +4276,35 @@ function mergeCodexTomlHooks(configTomlPath, { projectLocal = false } = {}) {
4178
4276
  }
4179
4277
  }
4180
4278
 
4181
- // Replace existing AOS:HOOKS block or append
4182
- const hookBlockRegex = /# AOS:HOOKS:START[\s\S]*?# AOS:HOOKS:END/;
4183
- if (hookBlockRegex.test(content)) {
4184
- content = content.replace(hookBlockRegex, tomlSnippet);
4185
- } else {
4186
- content = `${content.trimEnd()}\n\n${tomlSnippet}\n`;
4187
- }
4279
+ // Drop every AOS hook entry (marker block or not, duplicates included), then append one fresh block.
4280
+ const bdbCodexScripts = ['go-gate.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'startcycle-dispatch.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
4281
+ const isAosMarker = (l) => /^\s*# AOS:HOOKS:(START|END)\s*$/.test(l);
4282
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
4283
+ const lines = content.split(/\r?\n/).filter((l) => !isAosMarker(l));
4284
+ const sections = [];
4285
+ for (const l of lines) {
4286
+ if (/^\s*\[/.test(l) || !sections.length) sections.push([l]);
4287
+ else sections[sections.length - 1].push(l);
4288
+ }
4289
+ const isHookHeader = (sec) => /^\s*\[\[hooks\.\w+\]\]\s*$/.test(sec[0]);
4290
+ const isHookInner = (sec) => /^\s*\[\[hooks\.\w+\.hooks\]\]\s*$/.test(sec[0]);
4291
+ const kept = [];
4292
+ for (let i = 0; i < sections.length; i++) {
4293
+ if (!isHookHeader(sections[i])) { kept.push(sections[i]); continue; }
4294
+ let j = i + 1;
4295
+ while (j < sections.length && isHookInner(sections[j])) j++;
4296
+ const group = sections.slice(i, j);
4297
+ const ours = group.some((sec) => sec.some((l) => /^\s*command\s*=/.test(l) && bdbCodexScripts.some((n) => l.includes(n))));
4298
+ if (ours) {
4299
+ // keep trailing blanks/comments (they may belong to another tool's marker)
4300
+ const tail = group.flat();
4301
+ let end = tail.length;
4302
+ while (end > 0 && /^\s*(#.*)?$/.test(tail[end - 1])) end--;
4303
+ if (end < tail.length) kept.push(tail.slice(end));
4304
+ } else kept.push(...group);
4305
+ i = j - 1;
4306
+ }
4307
+ content = `${kept.flat().join(eol).trimEnd()}${eol}${eol}${tomlSnippet.split('\n').join(eol)}${eol}`;
4188
4308
 
4189
4309
  try {
4190
4310
  fs.mkdirSync(path.dirname(configTomlPath), { recursive: true });
@@ -4350,13 +4470,33 @@ async function promptOptionalModules(installedModules) {
4350
4470
  required: false
4351
4471
  }));
4352
4472
 
4473
+ return (await runModuleInstalls(allModules, chosen, installedModules)).installed;
4474
+ }
4475
+
4476
+ // Module installers return false (or throw) on failure; undefined/true mean success.
4477
+ // Only successes are recorded, so the manifest never claims a module that is not there.
4478
+ const moduleFailures = [];
4479
+ async function runModuleInstalls(allModules, chosen, installedModules) {
4480
+ const installed = [];
4481
+ const failed = [];
4353
4482
  for (const mod of allModules) {
4354
- if (chosen.includes(mod.id)) {
4355
- await mod.fn();
4356
- installedModules.push(mod.id);
4483
+ if (!chosen.includes(mod.id)) continue;
4484
+ let reason = null;
4485
+ try {
4486
+ if (await mod.fn() === false) reason = 'installation reported a failure (see warnings above)';
4487
+ } catch (e) {
4488
+ reason = e && e.message ? e.message : String(e);
4489
+ }
4490
+ if (reason === null) {
4491
+ installed.push(mod.id);
4492
+ if (!installedModules.includes(mod.id)) installedModules.push(mod.id);
4493
+ } else {
4494
+ failed.push({ id: mod.id, name: mod.name, reason });
4495
+ moduleFailures.push({ id: mod.id, name: mod.name, reason });
4496
+ log.warn(`${mod.name} was not installed: ${reason}`);
4357
4497
  }
4358
4498
  }
4359
- return chosen;
4499
+ return { installed, failed };
4360
4500
  }
4361
4501
 
4362
4502
  // Module ids whose daemons ship a web interface or background service worth
@@ -4397,6 +4537,107 @@ function shouldAutostartLaunchpad(overrides = {}) {
4397
4537
  return isDevEnvironment(overrides);
4398
4538
  }
4399
4539
 
4540
+ // Plan Canvas autostart is strictly opt-in (--autostart-plan-canvas): unlike
4541
+ // the launchpad opener it is NOT implied by a dev checkout. The server never
4542
+ // idle-exits there (AOS_PLAN_CANVAS_IDLE_MS=0 is honoured by server.js).
4543
+ const PLAN_CANVAS_LABEL = 'com.bdb.plan-canvas';
4544
+
4545
+ function shouldAutostartPlanCanvas(overrides = {}) {
4546
+ const argv = overrides.argv || process.argv;
4547
+ return argv.includes('--autostart-plan-canvas');
4548
+ }
4549
+
4550
+ function buildPlanCanvasPlist({ nodeBin, scriptPath, home, logDir }) {
4551
+ return `<?xml version="1.0" encoding="UTF-8"?>
4552
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4553
+ <plist version="1.0">
4554
+ <dict>
4555
+ <key>Label</key>
4556
+ <string>${PLAN_CANVAS_LABEL}</string>
4557
+ <key>ProgramArguments</key>
4558
+ <array>
4559
+ <string>${nodeBin}</string>
4560
+ <string>${scriptPath}</string>
4561
+ <string>server</string>
4562
+ </array>
4563
+ <key>EnvironmentVariables</key>
4564
+ <dict>
4565
+ <key>AOS_PLAN_CANVAS_IDLE_MS</key>
4566
+ <string>0</string>
4567
+ </dict>
4568
+ <key>WorkingDirectory</key>
4569
+ <string>${home}</string>
4570
+ <key>RunAtLoad</key>
4571
+ <true/>
4572
+ <key>KeepAlive</key>
4573
+ <true/>
4574
+ <key>ThrottleInterval</key>
4575
+ <integer>30</integer>
4576
+ <key>StandardOutPath</key>
4577
+ <string>${path.join(logDir, 'plan-canvas.stdout.log')}</string>
4578
+ <key>StandardErrorPath</key>
4579
+ <string>${path.join(logDir, 'plan-canvas.stderr.log')}</string>
4580
+ </dict>
4581
+ </plist>`;
4582
+ }
4583
+
4584
+ // Same bat + hidden-vbs-in-Startup mechanism as the OpenWiki visualizer.
4585
+ function buildPlanCanvasWinFiles({ nodeBin, scriptPath, home, logDir, batPath }) {
4586
+ const out = path.join(logDir, 'plan-canvas.stdout.log');
4587
+ const err = path.join(logDir, 'plan-canvas.stderr.log');
4588
+ return {
4589
+ bat: `@echo off\r\ncd /d "${home}"\r\nset AOS_PLAN_CANVAS_IDLE_MS=0\r\n"${nodeBin}" "${scriptPath}" server >> "${out}" 2>> "${err}"\r\n`,
4590
+ vbs: `Set WshShell = CreateObject("WScript.Shell")\r\nWshShell.CurrentDirectory = "${home}"\r\nWshShell.Run """${batPath}""", 0, False\r\n`,
4591
+ };
4592
+ }
4593
+
4594
+ function installPlanCanvasAutostart(overrides = {}) {
4595
+ if (!shouldAutostartPlanCanvas(overrides)) return false;
4596
+ if ((overrides.dryRun ?? DRY_RUN)) {
4597
+ log.step('[dry-run] would register the Plan Canvas background server (Port 4519, no idle exit)');
4598
+ return false;
4599
+ }
4600
+ const home = overrides.homeDir || homeDir;
4601
+ const platform = overrides.platform || process.platform;
4602
+ const scriptPath = [
4603
+ path.join(home, '.agents', 'skills', 'plan-canvas', 'scripts', 'plan-canvas.js'),
4604
+ path.join(srcDir, 'skills', 'global_config', 'plan-canvas', 'scripts', 'plan-canvas.js'),
4605
+ ].find(p => fs.existsSync(p));
4606
+ if (!scriptPath) {
4607
+ log.warn('Plan Canvas autostart skipped: plan-canvas.js not found. Start it with: aos-plan-canvas server');
4608
+ return false;
4609
+ }
4610
+ const nodeBin = overrides.nodeBin || process.execPath;
4611
+ const logDir = path.join(home, '.agents', 'logs');
4612
+ try {
4613
+ fs.mkdirSync(logDir, { recursive: true });
4614
+ if (platform === 'darwin') {
4615
+ const plistPath = path.join(home, 'Library', 'LaunchAgents', `${PLAN_CANVAS_LABEL}.plist`);
4616
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
4617
+ fs.writeFileSync(plistPath, buildPlanCanvasPlist({ nodeBin, scriptPath, home, logDir }));
4618
+ if (!overrides.skipLaunchctl) {
4619
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: 'ignore' });
4620
+ execSync(`launchctl load -w "${plistPath}" 2>/dev/null || true`, { stdio: 'ignore' });
4621
+ }
4622
+ log.success('Plan Canvas LaunchAgent registered (Port 4519)');
4623
+ } else if (platform === 'win32') {
4624
+ const startupDir = path.join(overrides.appData || process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
4625
+ fs.mkdirSync(startupDir, { recursive: true });
4626
+ const batPath = path.join(home, '.agents', 'run-plan-canvas.bat');
4627
+ const vbsPath = path.join(startupDir, `${PLAN_CANVAS_LABEL}.vbs`);
4628
+ const files = buildPlanCanvasWinFiles({ nodeBin, scriptPath, home, logDir, batPath });
4629
+ fs.writeFileSync(batPath, files.bat, 'utf-8');
4630
+ fs.writeFileSync(vbsPath, files.vbs, 'utf-8');
4631
+ if (!overrides.skipLaunchctl) spawn('wscript.exe', [vbsPath], { detached: true, stdio: 'ignore' }).unref();
4632
+ log.success('Plan Canvas Startup entry registered (Port 4519)');
4633
+ } else {
4634
+ log.step('Plan Canvas autostart is only wired for macOS and Windows. Start it with: aos-plan-canvas server');
4635
+ return false;
4636
+ }
4637
+ return true;
4638
+ } catch (e) { logDebug(e, 'plan canvas autostart'); return false; }
4639
+ }
4640
+
4400
4641
  function generateAndOpenLaunchpad(installedModules = []) {
4401
4642
  if (DRY_RUN) {
4402
4643
  log.step('[dry-run] would generate & open BDB Launchpad HTML');
@@ -4532,6 +4773,16 @@ function generateAndOpenLaunchpad(installedModules = []) {
4532
4773
  .card.offline .card-hint {
4533
4774
  display: block;
4534
4775
  }
4776
+ .card-hint code {
4777
+ font-family: var(--mono);
4778
+ font-style: normal;
4779
+ color: #ffffff;
4780
+ background: #0a0a0a;
4781
+ border: 1px solid var(--accent);
4782
+ border-radius: 4px;
4783
+ padding: 1px 6px;
4784
+ cursor: copy;
4785
+ }
4535
4786
  .footer {
4536
4787
  margin-top: 28px;
4537
4788
  text-align: center;
@@ -4722,6 +4973,25 @@ function generateAndOpenLaunchpad(installedModules = []) {
4722
4973
  <span class="status-dot" id="dot-store" title="Checking..."></span>
4723
4974
  </div>
4724
4975
  </a>
4976
+ <a class="card" href="http://127.0.0.1:4519/" target="_blank">
4977
+ <div class="card-content">
4978
+ <div class="card-icon"><svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
4979
+ <defs><linearGradient id="grad-plancanvas" x1="50%" y1="0%" x2="50%" y2="100%"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#9b30c4"/></linearGradient></defs>
4980
+ <rect x="18" y="22" width="64" height="56" rx="4" fill="url(#grad-plancanvas)" opacity="0.85"/>
4981
+ <rect x="26" y="30" width="22" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/><rect x="52" y="30" width="22" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/>
4982
+ <line x1="26" y1="54" x2="74" y2="54" stroke="#0a0a0a" stroke-width="2.5" opacity="0.5"/><line x1="26" y1="64" x2="56" y2="64" stroke="#0a0a0a" stroke-width="2.5" opacity="0.5"/>
4983
+ </svg></div>
4984
+ <div class="card-info">
4985
+ <h2>Plan Canvas</h2>
4986
+ <p>Review plans in the browser: annotate, chat, approve</p>
4987
+ <div class="card-hint">offline — run: <code data-copy="aos-plan-canvas server" title="Click to copy">aos-plan-canvas server</code></div>
4988
+ </div>
4989
+ </div>
4990
+ <div class="card-meta">
4991
+ <span class="port-pill">:4519</span>
4992
+ <span class="status-dot" id="dot-plancanvas" title="Checking..."></span>
4993
+ </div>
4994
+ </a>
4725
4995
  </div>
4726
4996
  <div class="footer">
4727
4997
  <span>Autostart Daemons • 127.0.0.1</span>
@@ -4749,7 +5019,26 @@ function generateAndOpenLaunchpad(installedModules = []) {
4749
5019
  checkHealth('http://127.0.0.1:9080', 'dot-remote');
4750
5020
  checkHealth('http://127.0.0.1:4321', 'dot-openwiki');
4751
5021
  checkHealth('http://127.0.0.1:4322', 'dot-store');
4752
- }
5022
+ checkHealth('http://127.0.0.1:4519', 'dot-plancanvas');
5023
+ }
5024
+ document.addEventListener('click', (ev) => {
5025
+ const el = ev.target.closest && ev.target.closest('[data-copy]');
5026
+ if (!el) return;
5027
+ ev.preventDefault();
5028
+ ev.stopPropagation();
5029
+ const text = el.getAttribute('data-copy');
5030
+ const fallback = () => {
5031
+ const ta = document.createElement('textarea');
5032
+ ta.value = text;
5033
+ document.body.appendChild(ta);
5034
+ ta.select();
5035
+ try { document.execCommand('copy'); } catch (e) {}
5036
+ ta.remove();
5037
+ };
5038
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).catch(fallback);
5039
+ else fallback();
5040
+ el.title = 'Copied';
5041
+ }, true);
4753
5042
  checkAllHealth();
4754
5043
  setInterval(checkAllHealth, 5000);
4755
5044
  </script>
@@ -4962,6 +5251,7 @@ async function universalHarnessSync(primaryMcpConfigPath, installedModules = [])
4962
5251
  }
4963
5252
  log.success('Universal Sync Complete!');
4964
5253
  generateAndOpenLaunchpad(installedModules);
5254
+ installPlanCanvasAutostart();
4965
5255
  }
4966
5256
 
4967
5257
  async function runQuickUpdate(installState) {
@@ -5128,7 +5418,41 @@ function installAosCli() {
5128
5418
  }
5129
5419
  }
5130
5420
 
5421
+ // A running daemon may execute dest; overwriting its inode keeps stale code-signature
5422
+ // state on macOS and every new exec dies with SIGKILL. Rename gives dest a fresh inode.
5423
+ function installBinaryAtomically(src, dest) {
5424
+ const tmp = path.join(path.dirname(dest), `.${path.basename(dest)}.${process.pid}.${Date.now()}.tmp`);
5425
+ try {
5426
+ fs.copyFileSync(src, tmp);
5427
+ if (process.platform !== 'win32') fs.chmodSync(tmp, 0o755);
5428
+ fs.renameSync(tmp, dest);
5429
+ } catch (e) {
5430
+ try { fs.unlinkSync(tmp); } catch (_) {}
5431
+ throw e;
5432
+ }
5433
+ }
5434
+
5131
5435
  async function main() {
5436
+ const args = process.argv.slice(2);
5437
+ if (args.includes('--version') || args.includes('-V')) {
5438
+ console.log(pkg.version);
5439
+ return;
5440
+ }
5441
+ if (args.includes('--help') || args.includes('-h')) {
5442
+ console.log(`Usage: aos [command] [options]
5443
+
5444
+ Commands:
5445
+ store Browse and install skills
5446
+ doctor|checkup Check the installation
5447
+
5448
+ Options:
5449
+ -y, --yes Non-interactive install (implied without a TTY)
5450
+ --dry-run Show what would change
5451
+ --verbose, -v Verbose output
5452
+ -V, --version Print the version and exit
5453
+ -h, --help Print this help and exit`);
5454
+ return;
5455
+ }
5132
5456
  if (process.argv[2] === 'store') {
5133
5457
  const storeScript = path.join(srcDir, 'bin', 'aos-store.mjs');
5134
5458
  const result = spawnSync(process.execPath, [storeScript, ...process.argv.slice(3)], { stdio: 'inherit' });
@@ -5582,10 +5906,14 @@ if (require.main === module) {
5582
5906
 
5583
5907
  // Exported for tests -- requiring installer.js must not launch the TUI.
5584
5908
  module.exports = {
5909
+ installBinaryAtomically,
5585
5910
  installOSAgentWorkspace,
5586
5911
  downloadOrUpdateModule,
5587
5912
  detectPlatforms,
5588
5913
  markPlatformsExplicit,
5914
+ runModuleInstalls,
5915
+ describeExecError,
5916
+ keepExistingEnvValues,
5589
5917
  mergeBdbSettingsHooks,
5590
5918
  mergeAntigravityHooks,
5591
5919
  mergeCodexTomlHooks,
@@ -5610,6 +5938,7 @@ module.exports = {
5610
5938
  INSTALL_MANIFEST_PATH,
5611
5939
  // Agent compilers & pipeline helpers
5612
5940
  parseAgentsMd,
5941
+ loadCommonSkills,
5613
5942
  compileClaudeAgents,
5614
5943
  compileOpenCodeAgents,
5615
5944
  compileCodexAgents,
@@ -5620,6 +5949,10 @@ module.exports = {
5620
5949
  generateAndOpenLaunchpad,
5621
5950
  shouldOpenLaunchpad,
5622
5951
  shouldAutostartLaunchpad,
5952
+ shouldAutostartPlanCanvas,
5953
+ buildPlanCanvasPlist,
5954
+ buildPlanCanvasWinFiles,
5955
+ installPlanCanvasAutostart,
5623
5956
  isDevEnvironment,
5624
5957
  LAUNCHPAD_WEB_MODULES,
5625
5958
  LAUNCHPAD_OPEN_FLAGS,