@hybridlabor-api/aos 4.16.0 → 4.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude/hooks/env-file-protection.mjs +19 -1
  3. package/.claude/hooks/go-gate.mjs +787 -68
  4. package/.claude/hooks/go-grant.mjs +88 -0
  5. package/.claude/settings.json +8 -0
  6. package/.codex-plugin/plugin.json +36 -6
  7. package/.opencode/commands/bdb-aos-brainstorm.md +5 -0
  8. package/.opencode/commands/bdb-aos-doctor.md +5 -0
  9. package/.opencode/commands/bdb-aos-graph.md +5 -0
  10. package/.opencode/commands/bdb-aos-init.md +5 -0
  11. package/.opencode/commands/bdb-aos-loop.md +5 -0
  12. package/.opencode/commands/bdb-aos-mastersession.md +5 -0
  13. package/.opencode/commands/bdb-aos-memb.md +5 -0
  14. package/.opencode/commands/bdb-aos-orchestrator.md +5 -0
  15. package/.opencode/commands/bdb-aos-plan.md +5 -0
  16. package/.opencode/commands/bdb-aos-playbooks.md +5 -0
  17. package/.opencode/commands/bdb-aos-setup.md +5 -0
  18. package/.opencode/commands/bdb-aos-shipping.md +5 -0
  19. package/.opencode/commands/bdb-aos-startproject.md +5 -0
  20. package/.opencode/commands/bdb-aos-store.md +5 -0
  21. package/.opencode/plugins/bdb-aos.js +54 -10
  22. package/README.md +4 -4
  23. package/agy-commands/loop.md +5 -0
  24. package/bin/aos-doctor.mjs +5 -3
  25. package/bin/aos-uninstall.mjs +25 -1
  26. package/commands/brainstorm.md +5 -0
  27. package/commands/doctor.md +5 -0
  28. package/commands/graph.md +5 -0
  29. package/commands/init.md +5 -0
  30. package/commands/loop.md +5 -0
  31. package/commands/mastersession.md +5 -0
  32. package/commands/memb.md +5 -0
  33. package/commands/orchestrator.md +5 -0
  34. package/commands/plan.md +5 -0
  35. package/commands/playbooks.md +5 -0
  36. package/commands/setup.md +5 -0
  37. package/commands/shipping.md +5 -0
  38. package/commands/startproject.md +5 -0
  39. package/commands/store.md +5 -0
  40. package/docs/codex-agy-setup.md +16 -0
  41. package/docs/opencode-setup.md +36 -0
  42. package/docs/plugin-migration.md +61 -0
  43. package/installer.js +223 -52
  44. package/lib/plugin-migration.js +462 -0
  45. package/lib/store-ui/index.html +9 -1
  46. package/lib/store-ui/server.mjs +2 -0
  47. package/package.json +7 -2
  48. package/plugin-commands.json +144 -0
  49. package/plugin.json +302 -0
  50. package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +38 -0
  51. package/plugins/bdb-aos-codex/skills/brainstorm/SKILL.md +6 -0
  52. package/plugins/bdb-aos-codex/skills/doctor/SKILL.md +6 -0
  53. package/plugins/bdb-aos-codex/skills/graph/SKILL.md +6 -0
  54. package/plugins/bdb-aos-codex/skills/init/SKILL.md +6 -0
  55. package/plugins/bdb-aos-codex/skills/loop/SKILL.md +6 -0
  56. package/plugins/bdb-aos-codex/skills/mastersession/SKILL.md +6 -0
  57. package/plugins/bdb-aos-codex/skills/memb/SKILL.md +6 -0
  58. package/plugins/bdb-aos-codex/skills/orchestrator/SKILL.md +6 -0
  59. package/plugins/bdb-aos-codex/skills/plan/SKILL.md +6 -0
  60. package/plugins/bdb-aos-codex/skills/playbooks/SKILL.md +6 -0
  61. package/plugins/bdb-aos-codex/skills/setup/SKILL.md +6 -0
  62. package/plugins/bdb-aos-codex/skills/shipping/SKILL.md +6 -0
  63. package/plugins/bdb-aos-codex/skills/startproject/SKILL.md +6 -0
  64. package/plugins/bdb-aos-codex/skills/store/SKILL.md +6 -0
  65. package/scripts/build-plugin-manifest.mjs +202 -3
  66. package/skills/bdb-aos/scripts/list-playbooks.mjs +72 -0
  67. package/skills/global_config/gogate/SKILL.md +123 -0
  68. package/skills/global_config/loop-templates/SKILL.md +27 -0
  69. package/skills/global_config/loop-templates/references/ci-until-green.md +27 -0
  70. package/skills/global_config/loop-templates/references/daily-summary.md +27 -0
  71. package/skills/global_config/loop-templates/references/pr-to-merge.md +27 -0
  72. package/skills/global_config/loop-templates/references/review-rounds.md +27 -0
  73. package/.codex-plugin/marketplace.json +0 -11
@@ -0,0 +1,36 @@
1
+ # OpenCode setup
2
+
3
+ The AOS installer copies these into `~/.config/opencode` (`%APPDATA%\opencode` on Windows):
4
+
5
+ - `plugins/bdb-aos.js` with `plugins/aos-hooks/` and `plugins/lib/` (go-gate, loop keeper, bus)
6
+ - `commands/startcycle-graph.md` and one generated `commands/bdb-aos-<cmd>.md` per AOS command, called as `/bdb-aos-<cmd>` (flat hyphen names; a colon is not valid in Windows file names)
7
+
8
+ The command files are generated from `plugin-commands.json` by `node scripts/build-plugin-manifest.mjs` and checked by `--check`. A command may carry `bodies.opencode`; without it the Claude body is used with `/bdb-aos:<cmd>` rewritten to `/bdb-aos-<cmd>`.
9
+
10
+ ## Updating and your edits
11
+
12
+ - A command file you edited is kept; the shipped version lands next to it as `<file>.new`.
13
+ - A file that already existed under a shipped name but was never installed by AOS is kept the same way.
14
+ - The installer never deletes files it did not create.
15
+
16
+ ## Lean MCP set
17
+
18
+ OpenCode deliberately runs a small MCP set (`memb_mcp`, `deja`, `zavora_computer_use`). AOS never copies other harnesses' MCP lists into OpenCode and never adds ComfyUI or show-control MCPs there. Media playbooks stop with "Missing MCP" on OpenCode by design.
19
+
20
+ ## Optional components (off by default)
21
+
22
+ ```bash
23
+ AOS_OPENCODE_OPTIONAL=ponytail,loop,rtk aos # or: aos --opencode-optional=ponytail,loop,rtk
24
+ ```
25
+
26
+ - `ponytail`: appends `@dietrichgebert/ponytail@4.10.0` to `plugin[]`
27
+ - `loop`: appends `@bybrawe/opencode-loop@0.6.2` to `plugin[]`
28
+ - `rtk`: prints `brew install rtk && rtk init -g --opencode`; AOS installs nothing
29
+
30
+ Entries are appended only when no entry for that package exists, after a timestamped `opencode.jsonc.<ts>.bak` backup. AOS never runs foreign installers (the `opencode-loop` npx installer rewrites the config) and skips `orca-opencode-status.js` (the Orca app maintains it) and `dag.jsonc` / GraphAgent (AGPL engine, different program).
31
+
32
+ ## Known limits
33
+
34
+ - **`/loop-shell` and the go-gate (unverified).** `opencode-loop` can run shell commands as child processes. `tool.execute.before`, where the AOS go-gate sits, may never see them. Do not schedule `git push`, publish or other gated commands through them. The installer prints this warning whenever `opencode-loop` is in `plugin[]`.
35
+ - **Machine prompts are never human.** Every prompt the plugin sends itself (loop nudge, bus wake) is `synthetic` and carries `aos_loop` or `aos_bus` metadata, so it cannot count as a GO. A test scans the plugin source for this.
36
+ - **Double loading of `bdb-aos.js`.** OpenCode auto-loads `plugins/*.{ts,js}` and also loads paths in `plugin[]`. From the 1.18.30 binary strings: both lists are merged through one de-duplication keyed on the `file://` URL, so the same absolute path loads once (read from the binary, not observed by running OpenCode). A differently spelled second path (checkout path, symlinked config dir) would load twice, giving two gates and two loop keepers. The installer registers exactly one path, `<config>/plugins/bdb-aos.js`, and warns when `plugin[]` holds another spelling.
@@ -0,0 +1,61 @@
1
+ # Plugin registration and loose-copy migration
2
+
3
+ The installer registers the `bdb-aos` plugin for Claude Code, then removes only its own loose skill copies there. Hooks, MCP servers, `trail-autostart` and the bus stay in the installer, so disabling the plugin never disables the go-gate.
4
+
5
+ | Harness | Loose copies | Why |
6
+ |---|---|---|
7
+ | Claude Code | **removed** from `~/.claude/skills` only after registration **and** evidence that Claude Code installed the plugin | the plugin bundles the skills |
8
+ | Codex | stay (`~/.codex/skills`) | the Codex plugin bundles only the command wrapper skills; nested skill scanning is unverified |
9
+ | agy | stay (`~/.gemini/config/skills`) | skill bundling is unverified (`agy plugin validate` processed 9 skills) |
10
+ | OpenCode | stay | plugins cannot bundle skills |
11
+ | all | `~/.agents/skills` always stays as the shared store | |
12
+
13
+ ## Rules
14
+
15
+ - **Registration first, evidence second.** Registration means `settings.json` holds the marketplace and `enabledPlugins` keys; that alone proves nothing about an installed plugin. Copies are removed only when Claude's plugin state shows the install: an entry for `bdb-aos@bdb-marketplace` in `~/.claude/plugins/installed_plugins.json`, or a cache directory `~/.claude/plugins/cache/bdb-marketplace/bdb-aos/<version>`. Either way the directory must resolve (realpath) under `~/.claude/plugins`, must not be cached from another (older external) marketplace, and must contain `skills/<name>/SKILL.md` for a skill the install manifest lists for `~/.claude/skills`. `installPath: "/"`, an empty `skills` dir or a path elsewhere do not count. The `installed_plugins.json` shape (`{version, plugins: {"<name>@<marketplace>": [{installPath, ...}]}}`) is verified against other plugins on a local machine; that `bdb-aos` writes the same shape and the cache layout fallback are **inferred**, not observed.
16
+ - **Not verifiable yet.** The installer registers the plugin, keeps every copy and prints "restart Claude Code, then run the installer again to retire the loose copies". The retirement happens on a later run once the evidence exists. Until then the installer keeps writing `~/.claude/skills` as before.
17
+ - **Ordering.** The migration runs before the install target loop. Once Claude is covered, `~/.claude/skills` is not written again, so a rerun changes nothing and creates no backup.
18
+ - **Merge, never overwrite.** Other keys survive. `settings.json` is backed up (`settings.json.<ts>.bak`) before a change; an unchanged file is not rewritten. A symlinked `settings.json` is written through to its real file and stays a link.
19
+ - **Opt-out.** `"bdb-aos@bdb-marketplace": false` in `enabledPlugins` (or `bdb-aos@<old external key>: false`): nothing is registered, nothing is removed.
20
+ - **Own copies only, safely resolved.** A file is removed when the install manifest lists it and its bytes still match the recorded hash. Keys with `..` or relative paths are rejected; each file and the root are resolved with `path.resolve` and `fs.realpathSync`, the real file must be a regular file under the real root. If `~/.claude/skills` itself is a symlink, nothing is removed. Edited files and files not in the manifest stay.
21
+ - **All or nothing.** If any listed copy is edited or unsafe, nothing is removed, Claude is not treated as covered, the installer names the skills involved and keeps updating the copies it owns. Move your edits out and run the installer again to retire them.
22
+ - **OpenWiki daemon.** `openwiki-skill/scripts/` is never retired, and the daemon is installed and run from the installer-owned `~/.agents/skills/openwiki-skill/scripts` (copied there when missing), so scheduled jobs never point into `~/.claude/skills`.
23
+ - **Malformed settings.** If `enabledPlugins` or `extraKnownMarketplaces` exists but is not a plain object, registration is refused and nothing is written.
24
+ - **One at a time.** A lock file (`~/.agents/.bdb-plugin-migration.lock`, stale after ten minutes or a dead pid) keeps two migrations apart; a file that vanishes mid-run is skipped.
25
+ - **Backup before removal.** Removed files are copied to `~/.agents/backups/plugin-migration-<timestamp>/` keeping their path relative to `$HOME`.
26
+ - **External marketplace.** An entry pointing at `hybridlabor-api/bdb-marketplace` is replaced by this repo's marketplace (`hybridlabor-api/aos`) only when no other plugin id uses it. Only `bdb-aos@<oldkey>` is renamed to `bdb-aos@bdb-marketplace`; other plugins' ids are never renamed. A foreign marketplace that also lists other plugins stays in place. If the name `bdb-marketplace` itself is that external marketplace with other plugins, or points at another source, registration is refused with a message and nothing changes. Nothing on GitHub is changed.
27
+ - **Old command files.** Command files from earlier versions that are no longer in the generated table are not touched by the migration; they are neither removed nor backed up. Only the generated `commands/` set is kept in sync by `scripts/build-plugin-manifest.mjs`.
28
+ - **State.** `~/.agents/.bdb-plugin-migration.json` records each change: `addedMarketplace`, `addedEnabled`, `replaced`, `renames`, `keptForeign`. Deregistering reverses exactly those.
29
+
30
+ ## Packaging
31
+
32
+ The npm package ships `plugins/bdb-aos-codex/` (a real directory) and the root manifests (`plugin.json`, `.codex-plugin/`, `.agents/plugins/marketplace.json`, `commands/`, `agy-commands/`). `plugins/bdb-aos/` is built from symlinks and is **not** in the npm package. The Claude Code plugin directory (`.claude-plugin/` and `plugins/bdb-aos/`) is git-only: Claude installs it from the GitHub marketplace `hybridlabor-api/aos`, not from npm. `.agents/plugins` (the Codex marketplace file) is excluded from the `.agents` copies into `~/.agents` and project directories.
33
+
34
+ The root `.codex-plugin/.mcp.json` is no longer referenced: the previous manifest pointed at it through an `mcp_config` key, which real Codex plugins do not use (they use `mcpServers`). It stays unreferenced on purpose, because the installer already registers the MCP servers in Codex's config and the plugin would register them twice. The plugin therefore delivers no MCP servers.
35
+
36
+ ## Opt out and preview
37
+
38
+ ```bash
39
+ AOS_PLUGIN_MIGRATION=off npx -y @hybridlabor-api/aos@latest # skip entirely
40
+ AOS_PLUGIN_MIGRATION=check npx -y @hybridlabor-api/aos@latest # report only
41
+ npx -y @hybridlabor-api/aos@latest --plugin-migration=off|check
42
+ ```
43
+
44
+ `--dry-run` implies `check`. Default is on.
45
+
46
+ ## Undo
47
+
48
+ ```bash
49
+ aos-uninstall --restore-plugin-backup # puts removed files back, never overwrites an existing file
50
+ aos-uninstall # also removes the plugin registration it added and restores a replaced external marketplace
51
+ ```
52
+
53
+ ## Known limits
54
+
55
+ - The lock guards migrations only. An installer copy running at the same moment in another process is not serialized; a file removed under it is skipped, and the next run reconciles.
56
+ - Until Claude Code shows the plugin as installed, both copies exist; skills may then appear twice after the plugin loads and before the next installer run retires the copies.
57
+ - `aos-doctor` accepts the plugin evidence as Claude having the skills.
58
+ - A deregister that reverses a marketplace rename keeps the content of `enabledPlugins` but may reorder its keys (cosmetic).
59
+ - Restoring a backup after an uninstall creates a new install manifest and puts the legacy `~/.claude/skills/startcycle` marker back, so AOS is detected as installed again.
60
+
61
+ Backups survive uninstall (the state keeps its backup index and `~/.agents/backups/plugin-migration-*` is scanned), so `--restore-plugin-backup` also works afterwards. Restored files are recorded in the install manifest, and the restore deregisters the plugin so skills are not loaded twice; the restore also sets `bdb-aos@bdb-marketplace` to `false` in `settings.json` (the opt-out), so the next installer run does not register the plugin or retire the restored files. Set it back to `true` to migrate again.
package/installer.js CHANGED
@@ -23,6 +23,7 @@ const net = require('net');
23
23
  const readline = require('readline');
24
24
  const util = require('util');
25
25
  const crypto = require('crypto');
26
+ const pluginMigration = require('./lib/plugin-migration');
26
27
 
27
28
  function verifyDaemonListening(port, name, timeoutMs = 4000) {
28
29
  return new Promise((resolve) => {
@@ -1277,9 +1278,94 @@ function reportFatal(stage, e) {
1277
1278
  process.exitCode = 1;
1278
1279
  }
1279
1280
 
1281
+ // AOS_PLUGIN_MIGRATION=off skips plugin registration and loose-copy removal; =check only reports.
1282
+ function pluginMigrationMode(argv = process.argv, env = process.env) {
1283
+ const flag = argv.find((a) => a.startsWith('--plugin-migration='));
1284
+ const raw = String(flag ? flag.slice('--plugin-migration='.length) : (env.AOS_PLUGIN_MIGRATION || '')).trim().toLowerCase();
1285
+ if (raw === 'off' || raw === '0' || raw === 'false') return 'off';
1286
+ return raw === 'check' || DRY_RUN ? 'check' : 'on';
1287
+ }
1288
+
1289
+ // .agents/plugins holds the Codex marketplace file; it belongs to the repo checkout, not to ~/.agents or a project.
1290
+ const AGENTS_COPY_EXCLUDE = ['plugins'];
1291
+
1292
+ let _pluginMigration = null;
1293
+ function runPluginMigration({ targetHome = homeDir, detected = null, mode = pluginMigrationMode(), registrars } = {}) {
1294
+ if (_pluginMigration && targetHome === homeDir) return _pluginMigration;
1295
+ const keys = detected || detectPlatforms().map((d) => d.key);
1296
+ const ownsManifest = !_sessionManifest;
1297
+ const manifest = _sessionManifest || loadInstallManifest();
1298
+ let result;
1299
+ try {
1300
+ result = pluginMigration.migrate({ home: targetHome, manifest, detected: keys, mode, ...(registrars ? { registrars } : {}) });
1301
+ } catch (e) {
1302
+ log.warn(`Plugin migration skipped: ${e.message}`);
1303
+ result = { covered: new Set(), lines: [] };
1304
+ }
1305
+ for (const line of result.lines) log.step(line);
1306
+ if (ownsManifest && mode === 'on') saveInstallManifest(manifest);
1307
+ if (targetHome === homeDir) _pluginMigration = result;
1308
+ return result;
1309
+ }
1310
+
1311
+ // The plugin migration runs first: when it retires Claude's loose copies, ~/.claude/skills is not
1312
+ // written again, so a rerun neither recreates nor re-backs-up what the plugin now delivers.
1313
+ function installTargetSkills(targets, { mode, backupDir, excludeSkills, skillsBase }) {
1314
+ const detected = detectPlatforms().map((d) => d.key);
1315
+ if (targets.some((t) => t.value === '2') && !detected.includes('claudecode')) detected.push('claudecode');
1316
+ const pluginCovered = runPluginMigration({ detected }).covered;
1317
+
1318
+ for (const t of targets) {
1319
+ const viaPlugin = t.value === '2' && pluginCovered.has('claudecode');
1320
+ if (mode === 'replace') {
1321
+ if (!viaPlugin) {
1322
+ moveIfExists(t.targetSkillDir, path.join(backupDir, `config_skills_backup_${t.value}`), `global config skills (${t.value})`);
1323
+ moveIfExists(t.targetLegacyDir, path.join(backupDir, `legacy_skills_backup_${t.value}`), `legacy skills (${t.value})`);
1324
+ }
1325
+ moveIfExists(t.targetWorkspaceDir, path.join(backupDir, `workspace_skills_backup_${t.value}`), `workspace skills (${t.value})`);
1326
+ }
1327
+
1328
+ installStep(`create the skill target directories (${t.value})`, () => {
1329
+ if (!viaPlugin) {
1330
+ fs.mkdirSync(t.targetSkillDir, { recursive: true });
1331
+ retireObsoleteLegacyDir(t.targetLegacyDir);
1332
+ }
1333
+ fs.mkdirSync(t.targetWorkspaceDir, { recursive: true });
1334
+ }, 'The skill copies below will most likely be skipped as well.');
1335
+
1336
+ if (fs.existsSync(skillsBase)) {
1337
+ installStep(`install the skills (${t.value})`, () => {
1338
+ const rawDirs = fs.readdirSync(skillsBase);
1339
+ const dirs = rawDirs.sort((a, b) => {
1340
+ const aIsLeaf = fs.existsSync(path.join(skillsBase, a, 'SKILL.md'));
1341
+ const bIsLeaf = fs.existsSync(path.join(skillsBase, b, 'SKILL.md'));
1342
+ if (aIsLeaf && !bIsLeaf) return 1;
1343
+ if (!aIsLeaf && bIsLeaf) return -1;
1344
+ return 0;
1345
+ });
1346
+ for (const dir of dirs) {
1347
+ const fullPath = path.join(skillsBase, dir);
1348
+ if (!fs.statSync(fullPath).isDirectory()) continue;
1349
+
1350
+ if (viaPlugin && dir !== 'workspace_agents') continue;
1351
+ if (dir === 'global_legacy') {
1352
+ copyDirRecursiveSync(fullPath, t.targetLegacyDir, excludeSkills);
1353
+ } else if (dir === 'workspace_agents') {
1354
+ copyDirRecursiveSync(fullPath, t.targetWorkspaceDir, excludeSkills);
1355
+ } else {
1356
+ syncSkillEntry(fullPath, dir, t.targetSkillDir, excludeSkills);
1357
+ }
1358
+ }
1359
+ log.step(`Installed all global config & core skills to ${t.targetSkillDir}`);
1360
+ }, 'The skills are missing or incomplete; the rest of the installation continues.');
1361
+ }
1362
+ }
1363
+ }
1364
+
1280
1365
  function syncSkillsToGlobalHarnesses(excludeSkills = []) {
1281
1366
  const skillsBase = path.join(srcDir, 'skills');
1282
1367
  if (!fs.existsSync(skillsBase)) return;
1368
+ const pluginCovered = runPluginMigration().covered;
1283
1369
 
1284
1370
  // Mirror only into harnesses that are actually present. This list used to
1285
1371
  // be unconditional, which both wrote skills nobody would read and planted
@@ -1300,7 +1386,7 @@ function syncSkillsToGlobalHarnesses(excludeSkills = []) {
1300
1386
  { dir: path.join(homeDir, '.cursor', 'skills'), key: 'cursor' },
1301
1387
  { dir: path.join(homeDir, '.roo', 'skills'), key: 'vscode' },
1302
1388
  { dir: process.platform === 'win32' ? path.join(process.env.APPDATA || homeDir, 'opencode', 'skills') : path.join(homeDir, '.config', 'opencode', 'skills'), key: 'opencode' },
1303
- ].filter((d) => d.key === null || detectedKeys.has(d.key));
1389
+ ].filter((d) => d.key === null || (detectedKeys.has(d.key) && !pluginCovered.has(d.key)));
1304
1390
 
1305
1391
  for (const { dir: dest } of extraSkillDestinations) {
1306
1392
  try {
@@ -1549,6 +1635,17 @@ async function promptCredentials(referenceMcpDir) {
1549
1635
 
1550
1636
  const DAEMON_LOGON_FALLBACK_EXIT_CODE = 10;
1551
1637
 
1638
+ // The daemon job points at these scripts for good, so they live in the installer-owned shared store
1639
+ // (~/.agents/skills), which the plugin migration never retires, not in ~/.claude/skills.
1640
+ function stableOpenWikiScripts() {
1641
+ const dest = path.join(homeDir, '.agents', 'skills', 'openwiki-skill');
1642
+ if (!fs.existsSync(path.join(dest, 'scripts', 'install_daemon.sh'))) {
1643
+ const src = path.join(srcDir, 'skills', 'global_config', 'openwiki-skill');
1644
+ if (fs.existsSync(src)) copyDirRecursiveSync(src, dest);
1645
+ }
1646
+ return path.join(dest, 'scripts');
1647
+ }
1648
+
1552
1649
  async function installOpenWikiDaemon(apiKey, targetSkillDir, openwikiEnv = {}) {
1553
1650
  const prov = openwikiEnv.provider || "google";
1554
1651
  if (!apiKey && !["ollama", "lmstudio"].includes(prov)) {
@@ -1562,7 +1659,7 @@ async function installOpenWikiDaemon(apiKey, targetSkillDir, openwikiEnv = {}) {
1562
1659
  const s = spinner();
1563
1660
  s.start('Installing OpenWiki Daemon...');
1564
1661
 
1565
- const scriptBase = path.join(targetSkillDir, 'openwiki-skill', 'scripts');
1662
+ const scriptBase = stableOpenWikiScripts();
1566
1663
 
1567
1664
  const daemonEnv = Object.assign({}, process.env, {
1568
1665
  OPENWIKI_PROVIDER: prov,
@@ -3886,7 +3983,7 @@ function injectHarnessRules() {
3886
3983
  const targetPath = path.join(homeDir, dir);
3887
3984
  // settings.json is merged separately below: a wholesale
3888
3985
  // copy would clobber user-owned keys like enabledPlugins.
3889
- const exclude = dir === '.claude' ? ['settings.json'] : [];
3986
+ const exclude = dir === '.claude' ? ['settings.json'] : dir === '.agents' ? AGENTS_COPY_EXCLUDE : [];
3890
3987
  copyDirRecursiveSync(sourcePath, targetPath, exclude);
3891
3988
  log.step(`Copied ${dir} to ${targetPath}`);
3892
3989
  }
@@ -3898,13 +3995,109 @@ function injectHarnessRules() {
3898
3995
  const globalAgentsDir = path.join(os.homedir(), '.agents');
3899
3996
  const agentsDirSrc = path.join(srcDir, '.agents');
3900
3997
  if (fs.existsSync(agentsDirSrc)) {
3901
- copyDirRecursiveSync(agentsDirSrc, globalAgentsDir);
3998
+ copyDirRecursiveSync(agentsDirSrc, globalAgentsDir, AGENTS_COPY_EXCLUDE);
3902
3999
  log.step(`Synced global .agents/ to ${globalAgentsDir}`);
3903
4000
  }
3904
4001
  }, 'agents.md and workflows/startcycle.md may be missing globally.');
3905
4002
  }
3906
4003
  }
3907
4004
 
4005
+ // Optional third-party OpenCode components. Off unless named in
4006
+ // AOS_OPENCODE_OPTIONAL=ponytail,loop,rtk or --opencode-optional=ponytail,loop,rtk.
4007
+ // Pinned entries only; the foreign installers (opencode-loop's npx installer, rtk init)
4008
+ // are never run because they rewrite the user's opencode config.
4009
+ const OPENCODE_OPTIONAL_PINS = {
4010
+ ponytail: '@dietrichgebert/ponytail@4.10.0',
4011
+ loop: '@bybrawe/opencode-loop@0.6.2',
4012
+ };
4013
+ const OPENCODE_RTK_HINT = 'rtk is not installed by AOS. Run yourself: brew install rtk && rtk init -g --opencode';
4014
+ const OPENCODE_LOOP_SHELL_WARNING = 'opencode-loop: /loop-shell style commands run shell commands as child processes. ' +
4015
+ 'tool.execute.before, and so the AOS go-gate, may never see them (unverified). ' +
4016
+ 'Do not schedule git push, publish or other gated commands through them.';
4017
+
4018
+ function parseOpencodeOptional(argv = process.argv, env = process.env) {
4019
+ const flag = argv.find((a) => a.startsWith('--opencode-optional='));
4020
+ const raw = [env.AOS_OPENCODE_OPTIONAL, flag && flag.slice('--opencode-optional='.length)].filter(Boolean).join(',');
4021
+ return new Set(raw.split(',').map((x) => x.trim().toLowerCase()).filter(Boolean));
4022
+ }
4023
+
4024
+ const pluginPackageName = (entry) => {
4025
+ const str = typeof entry === 'string' ? entry : (Array.isArray(entry) ? String(entry[0]) : '');
4026
+ return str.replace(/(?<=.)@[^@/]*$/, '');
4027
+ };
4028
+
4029
+ // OpenCode auto-loads plugins/*.js and also loads plugin[] paths, then dedupes by file
4030
+ // URL (verified in the 1.18.30 binary strings, not by running it). Same path = one load;
4031
+ // a second differing spelling would load the gate and loop keeper twice.
4032
+ function warnOnDuplicateAosPlugin(plugins, canonical) {
4033
+ const norm = (v) => String(Array.isArray(v) ? v[0] : v).replace(/\\/g, '/');
4034
+ const others = plugins.filter((p) => /bdb-aos\.js$/.test(norm(p)) && norm(p) !== canonical);
4035
+ if (others.length > 0) {
4036
+ log.warn(`opencode.jsonc lists bdb-aos.js under another path (${others.map(norm).join(', ')}). ` +
4037
+ `plugins/bdb-aos.js is auto-loaded too; remove the extra entry to avoid a double gate.`);
4038
+ }
4039
+ }
4040
+
4041
+ function appendOptionalOpencodePlugins(data, configPath, optional) {
4042
+ const wanted = optional instanceof Set ? optional : new Set(optional || []);
4043
+ for (const name of wanted) {
4044
+ if (!(name in OPENCODE_OPTIONAL_PINS) && name !== 'rtk') log.warn(`Unknown OpenCode optional component "${name}" ignored.`);
4045
+ }
4046
+ if (wanted.has('rtk')) log.message(OPENCODE_RTK_HINT);
4047
+ let backedUp = false;
4048
+ for (const name of Object.keys(OPENCODE_OPTIONAL_PINS)) {
4049
+ if (!wanted.has(name) || DRY_RUN) continue;
4050
+ const pin = OPENCODE_OPTIONAL_PINS[name];
4051
+ if (data.plugin.some((p) => pluginPackageName(p) === pluginPackageName(pin))) continue;
4052
+ if (!backedUp && configPath && fs.existsSync(configPath)) {
4053
+ const bak = `${configPath}.${timestamp}.bak`;
4054
+ try { fs.copyFileSync(configPath, bak); backedUp = true; } catch (e) {
4055
+ log.warn(`Could not back up ${configPath}, skipping optional ${name}: ${e.message}`);
4056
+ continue;
4057
+ }
4058
+ }
4059
+ data.plugin.push(pin);
4060
+ log.step(`Added opt-in OpenCode plugin ${pin}`);
4061
+ }
4062
+ if (data.plugin.some((p) => pluginPackageName(p) === pluginPackageName(OPENCODE_OPTIONAL_PINS.loop))) {
4063
+ log.warn(OPENCODE_LOOP_SHELL_WARNING);
4064
+ }
4065
+ }
4066
+
4067
+ // Idempotent and edit-safe: a file is overwritten only when it is absent, already
4068
+ // identical, or still byte-equal to what this installer last recorded. Anything else
4069
+ // (user edit, or an untracked file with the same name) is kept and the shipped copy
4070
+ // lands as <file>.new. Files the installer did not create are never removed.
4071
+ function installOpencodeCommands(srcDirPath, destDirPath) {
4072
+ if (DRY_RUN) {
4073
+ log.message(`[dry-run] install OpenCode commands: ${srcDirPath} -> ${destDirPath}`);
4074
+ return;
4075
+ }
4076
+ const ownsManifest = !_sessionManifest;
4077
+ const manifest = _sessionManifest || loadInstallManifest();
4078
+ fs.mkdirSync(destDirPath, { recursive: true });
4079
+ let wrote = 0;
4080
+ for (const file of fs.readdirSync(srcDirPath)) {
4081
+ const src = path.join(srcDirPath, file);
4082
+ if (!fs.statSync(src).isFile()) continue;
4083
+ const dest = path.join(destDirPath, file);
4084
+ const srcHash = computeFileHash(src);
4085
+ const diskHash = fs.existsSync(dest) ? computeFileHash(dest) : null;
4086
+ if (diskHash === null || diskHash === srcHash || diskHash === manifest[dest]?.sha256) {
4087
+ if (diskHash !== srcHash) { fs.copyFileSync(src, dest); wrote++; }
4088
+ manifest[dest] = { path: dest, sha256: srcHash, version: pkg.version, installedAt: new Date().toISOString() };
4089
+ } else {
4090
+ fs.copyFileSync(src, `${dest}.new`);
4091
+ keptUserEdits.push(dest);
4092
+ }
4093
+ }
4094
+ if (ownsManifest) {
4095
+ saveInstallManifest(manifest);
4096
+ reportKeptUserEdits();
4097
+ }
4098
+ log.step(`OpenCode commands in ${destDirPath}: ${wrote} written`);
4099
+ }
4100
+
3908
4101
  // Copy the OpenCode plugin + command payload and register both in
3909
4102
  // opencode.jsonc. This is the single copy site: the Quick Update path
3910
4103
  // (injectHarnessRules -> installGlobalHooks) and the fresh-install path
@@ -3915,7 +4108,7 @@ function injectHarnessRules() {
3915
4108
  // Sync merges MCP servers in the same pass and writes once). When `data` is
3916
4109
  // omitted the function loads and saves the config itself, which is what lets the
3917
4110
  // Quick Update path register the plugin as well as copy it.
3918
- function installOpencodePlugin({ targetHome = homeDir, configPath = null, data = null } = {}) {
4111
+ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data = null, optional = parseOpencodeOptional() } = {}) {
3919
4112
  const opencodeDir = configPath
3920
4113
  ? path.dirname(configPath)
3921
4114
  : (process.platform === 'win32'
@@ -3952,13 +4145,11 @@ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data =
3952
4145
  try { copyDirRecursiveSync(trailLibSrc, path.join(opencodeDir, 'plugins', 'lib')); } catch (e) { log.warn(`Could not install OpenCode plugin lib: ${e.message}`); }
3953
4146
  }
3954
4147
 
3955
- // Slash-command payloads. Without these the /startcycle-graph command has no
3956
- // native resolution and only survives as a raw-text match in the plugin.
4148
+ // Slash-command payloads (/startcycle-graph and the generated /bdb-aos-<cmd> files).
3957
4149
  const commandsSrc = path.join(srcDir, '.opencode', 'commands');
3958
4150
  if (fs.existsSync(commandsSrc)) {
3959
4151
  try {
3960
- copyDirRecursiveSync(commandsSrc, path.join(opencodeDir, 'commands'));
3961
- log.step(`Installed OpenCode commands to ${path.join(opencodeDir, 'commands')}`);
4152
+ installOpencodeCommands(commandsSrc, path.join(opencodeDir, 'commands'));
3962
4153
  } catch (e) {
3963
4154
  log.warn(`Could not install OpenCode commands: ${e.message}`);
3964
4155
  }
@@ -3985,6 +4176,8 @@ function installOpencodePlugin({ targetHome = homeDir, configPath = null, data =
3985
4176
  return str.includes('bdb-aos');
3986
4177
  });
3987
4178
  if (!alreadyRegistered) data.plugin.push(pluginPathNormalized);
4179
+ warnOnDuplicateAosPlugin(data.plugin, pluginPathNormalized);
4180
+ appendOptionalOpencodePlugins(data, configPath, optional);
3988
4181
 
3989
4182
  // Skill paths. `.agents/skills` stays project-relative on purpose: it is the
3990
4183
  // per-project contract, and OpenCode resolves it against each project, so it
@@ -4150,7 +4343,7 @@ function installGlobalBinaries() {
4150
4343
  // merged result goes to a .bdb-new.json sidecar -- the same recovery pattern
4151
4344
  // the MCP config merge in installMcpsForTarget uses.
4152
4345
  function mergeBdbSettingsHooks(settingsPath, { projectLocal = false } = {}) {
4153
- const bdbHookScripts = ['go-gate.mjs', 'go-token.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'trail-autostart.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
4346
+ const bdbHookScripts = ['go-gate.mjs', 'go-token.mjs', 'go-grant.mjs', 'graph-gate.mjs', 'memb-inject.mjs', 'trail-relay.mjs', 'trail-autostart.mjs', 'conventional-commits.mjs', 'env-file-protection.mjs'];
4154
4347
  // memb-inject reads the machine-global memB store under $HOME and is
4155
4348
  // installed once per machine, so it stays $HOME-anchored even inside a
4156
4349
  // project harness -- unlike the two gate hooks, which are per-checkout by
@@ -4557,7 +4750,7 @@ function installProjectHarness() {
4557
4750
  installStep('copy .agents/ contract into project', () => {
4558
4751
  const agentsSrc = path.join(srcDir, '.agents');
4559
4752
  if (!fs.existsSync(agentsSrc)) throw new Error(`missing payload: ${agentsSrc}`);
4560
- copyDirRecursiveSync(agentsSrc, projectAgentsDir);
4753
+ copyDirRecursiveSync(agentsSrc, projectAgentsDir, AGENTS_COPY_EXCLUDE);
4561
4754
  log.step(`Copied .agents/ contract to ${projectAgentsDir}`);
4562
4755
  }, 'graph.md / state.schema.json may be missing in the project.');
4563
4756
 
@@ -5655,11 +5848,11 @@ function installBinaryAtomically(src, dest) {
5655
5848
 
5656
5849
  async function main() {
5657
5850
  const args = process.argv.slice(2);
5658
- if (args.includes('--version') || args.includes('-V')) {
5851
+ if (args.includes('--version') || args.includes('-V') || args[0] === 'version') {
5659
5852
  console.log(pkg.version);
5660
5853
  return;
5661
5854
  }
5662
- if (args.includes('--help') || args.includes('-h')) {
5855
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
5663
5856
  console.log(`Usage: aos [command] [options]
5664
5857
 
5665
5858
  Commands:
@@ -5669,6 +5862,15 @@ Commands:
5669
5862
  Options:
5670
5863
  -y, --yes Non-interactive install (implied without a TTY)
5671
5864
  --dry-run Show what would change
5865
+ --opencode-optional=LIST
5866
+ Opt in to OpenCode extras (comma list: ponytail, loop, rtk), or set
5867
+ AOS_OPENCODE_OPTIONAL. Pinned plugin[] entries are appended after a
5868
+ config backup; rtk only prints a brew hint. Off by default. AOS never
5869
+ runs foreign installers and never touches OpenCode's mcp set.
5870
+ --plugin-migration=off|check
5871
+ Skip (off) or only report (check) the bdb-aos plugin registration and
5872
+ removal of AOS's own loose skill copies; same as AOS_PLUGIN_MIGRATION.
5873
+ --dry-run implies check. Default: on.
5672
5874
  --verbose, -v Verbose output
5673
5875
  -V, --version Print the version and exit
5674
5876
  -h, --help Print this help and exit`);
@@ -6036,45 +6238,7 @@ Options:
6036
6238
  const s = spinner();
6037
6239
  s.start(`Installing optimized skills${tier === '2' ? ' [Basic Tier]' : ''} to ${targets.length} target(s)...`);
6038
6240
 
6039
- for (const t of targets) {
6040
- if (mode === 'replace') {
6041
- moveIfExists(t.targetSkillDir, path.join(backupDir, `config_skills_backup_${t.value}`), `global config skills (${t.value})`);
6042
- moveIfExists(t.targetLegacyDir, path.join(backupDir, `legacy_skills_backup_${t.value}`), `legacy skills (${t.value})`);
6043
- moveIfExists(t.targetWorkspaceDir, path.join(backupDir, `workspace_skills_backup_${t.value}`), `workspace skills (${t.value})`);
6044
- }
6045
-
6046
- installStep(`create the skill target directories (${t.value})`, () => {
6047
- fs.mkdirSync(t.targetSkillDir, { recursive: true });
6048
- retireObsoleteLegacyDir(t.targetLegacyDir);
6049
- fs.mkdirSync(t.targetWorkspaceDir, { recursive: true });
6050
- }, 'The skill copies below will most likely be skipped as well.');
6051
-
6052
- if (fs.existsSync(skillsBase)) {
6053
- installStep(`install the skills (${t.value})`, () => {
6054
- const rawDirs = fs.readdirSync(skillsBase);
6055
- const dirs = rawDirs.sort((a, b) => {
6056
- const aIsLeaf = fs.existsSync(path.join(skillsBase, a, 'SKILL.md'));
6057
- const bIsLeaf = fs.existsSync(path.join(skillsBase, b, 'SKILL.md'));
6058
- if (aIsLeaf && !bIsLeaf) return 1;
6059
- if (!aIsLeaf && bIsLeaf) return -1;
6060
- return 0;
6061
- });
6062
- for (const dir of dirs) {
6063
- const fullPath = path.join(skillsBase, dir);
6064
- if (!fs.statSync(fullPath).isDirectory()) continue;
6065
-
6066
- if (dir === 'global_legacy') {
6067
- copyDirRecursiveSync(fullPath, t.targetLegacyDir, excludeSkills);
6068
- } else if (dir === 'workspace_agents') {
6069
- copyDirRecursiveSync(fullPath, t.targetWorkspaceDir, excludeSkills);
6070
- } else {
6071
- syncSkillEntry(fullPath, dir, t.targetSkillDir, excludeSkills);
6072
- }
6073
- }
6074
- log.step(`Installed all global config & core skills to ${t.targetSkillDir}`);
6075
- }, 'The skills are missing or incomplete; the rest of the installation continues.');
6076
- }
6077
- }
6241
+ installTargetSkills(targets, { mode, backupDir, excludeSkills, skillsBase });
6078
6242
 
6079
6243
  syncSkillsToGlobalHarnesses(excludeSkills);
6080
6244
  pruneRemovedSkills(_sessionManifest);
@@ -6154,6 +6318,10 @@ module.exports = {
6154
6318
  mergeCodexTomlMcpServers,
6155
6319
  installGlobalHooks,
6156
6320
  installOpencodePlugin,
6321
+ installOpencodeCommands,
6322
+ parseOpencodeOptional,
6323
+ pluginMigrationMode,
6324
+ runPluginMigration,
6157
6325
  installProjectHarness,
6158
6326
  promptMcpSelection,
6159
6327
  mirrorMcpServersTo,
@@ -6166,6 +6334,9 @@ module.exports = {
6166
6334
  resolveFileConflict,
6167
6335
  buildKnownSourceHashes,
6168
6336
  initSessionManifest,
6337
+ installTargetSkills,
6338
+ stableOpenWikiScripts,
6339
+ AGENTS_COPY_EXCLUDE,
6169
6340
  flushSessionManifest,
6170
6341
  copyDirRecursiveSync,
6171
6342
  INSTALL_MANIFEST_PATH,