@hybridlabor-api/aos 4.17.0 → 4.18.1

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 (88) hide show
  1. package/.claude/hooks/aos-bus.mjs +8 -4
  2. package/.claude/hooks/go-gate.mjs +1 -1
  3. package/.claude/hooks/go-token.mjs +17 -2
  4. package/.claude/hooks/memb-inject.mjs +61 -37
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.opencode/commands/bdb-aos-plan.md +1 -1
  7. package/README.md +1 -0
  8. package/THIRD_PARTY_NOTICES.md +2 -2
  9. package/bin/aos-acp.mjs +27 -1
  10. package/bin/aos-doctor.mjs +36 -1
  11. package/bin/aos-uninstall.mjs +16 -2
  12. package/bin/go-check.mjs +79 -0
  13. package/bin/guarded-patterns.json +106 -0
  14. package/commands/plan.md +1 -1
  15. package/docs/codenotch.md +44 -0
  16. package/docs/codex-gate-smoke.md +43 -0
  17. package/docs/delegation-routing.md +32 -0
  18. package/docs/go-check.md +60 -0
  19. package/docs/master-session-acp.md +2 -0
  20. package/docs/opencode-setup.md +18 -0
  21. package/installer.js +155 -70
  22. package/lib/codenotch.js +389 -0
  23. package/lib/retired-skills.js +101 -0
  24. package/mcps/mcsc/README.md +1 -1
  25. package/mcps/mcsc/packages/core/src/adapters/agy.js +3 -1
  26. package/mcps/mcsc/packages/core/src/adapters/codex.js +2 -1
  27. package/mcps/mcsc/packages/core/src/adapters/opencode.js +2 -1
  28. package/mcps/mcsc/packages/core/src/depth.js +16 -0
  29. package/mcps/mcsc/packages/mcp/server.js +15 -2
  30. package/package.json +2 -2
  31. package/plugin-commands.json +1 -2
  32. package/plugin.json +1 -4
  33. package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +1 -1
  34. package/plugins/bdb-aos-codex/skills/plan/SKILL.md +1 -1
  35. package/scripts/codex-gate-smoke.mjs +73 -0
  36. package/skills/basic/master-session/SKILL.md +11 -0
  37. package/skills/global_config/agenttrail/SKILL.md +3 -1
  38. package/skills/global_config/agenttrail/bin/agenttrail.mjs +255 -117
  39. package/skills/global_config/agenttrail/bin/ensure.mjs +60 -40
  40. package/skills/global_config/agenttrail/bin/repoid.mjs +70 -0
  41. package/skills/global_config/agenttrail/public/index.html +9 -1
  42. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  43. package/skills/global_config/bdb-memb-mcp/SKILL.md +5 -4
  44. package/skills/global_config/bdb-visual-edit/SKILL.md +28 -32
  45. package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +2 -2
  46. package/skills/global_config/bdb-visual-edit/scripts/locate-source.mjs +135 -0
  47. package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +30 -2
  48. package/skills/global_config/mcsc/SKILL.md +9 -1
  49. package/skills/global_config/plan-arbiter/SKILL.md +1 -1
  50. package/skills/global_config/plan-canvas/SKILL.md +42 -4
  51. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +1 -1
  52. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +2 -2
  53. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/geometry.js +76 -0
  54. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/index.js +596 -0
  55. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/model.js +192 -0
  56. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/toolbar.js +99 -0
  57. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-server.js +282 -0
  58. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotation-schema.js +210 -0
  59. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/route.js +10 -0
  60. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sdk.js +6 -230
  61. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +45 -4
  62. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sessions.js +60 -21
  63. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/trail-on-approve.js +103 -0
  64. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +19 -7
  65. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +118 -20
  66. package/skills/global_config/subagent-setup/SKILL.md +6 -0
  67. package/skills/global_config/subagent-setup/scripts/setup-subagents.mjs +20 -1
  68. package/skills/playbooks/pb-idea-to-launch/SKILL.md +2 -2
  69. package/skills/playbooks/pb-redesign-app/SKILL.md +3 -3
  70. package/skills/playbooks/pb-release-aos/SKILL.md +2 -2
  71. package/skills/playbooks/pb-ship/SKILL.md +2 -2
  72. package/skills/playbooks/pb-worktrees-land/SKILL.md +2 -2
  73. package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +0 -27
  74. package/skills/global_config/visual-edit/README.md +0 -96
  75. package/skills/global_config/visual-edit/SKILL.md +0 -615
  76. package/skills/global_config/visual-plan/README.md +0 -93
  77. package/skills/global_config/visual-plan/SKILL.md +0 -544
  78. package/skills/global_config/visual-plan/references/canvas.md +0 -139
  79. package/skills/global_config/visual-plan/references/connection.md +0 -51
  80. package/skills/global_config/visual-plan/references/document-quality.md +0 -186
  81. package/skills/global_config/visual-plan/references/exemplar.md +0 -62
  82. package/skills/global_config/visual-plan/references/local-files.md +0 -99
  83. package/skills/global_config/visual-plan/references/wireframe.md +0 -319
  84. package/skills/global_config/visual-recap/README.md +0 -103
  85. package/skills/global_config/visual-recap/SKILL.md +0 -560
  86. package/skills/global_config/visual-recap/references/connection.md +0 -51
  87. package/skills/global_config/visual-recap/references/local-files.md +0 -99
  88. package/skills/global_config/visual-recap/references/wireframe.md +0 -319
@@ -0,0 +1,389 @@
1
+ 'use strict';
2
+ // macOS-only opt-in install of BDB AO Codenotch from the public releases repo.
3
+ // Every side effect (network, hdiutil, xattr, plutil, copy, prompts, roots) is injectable.
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+ const { execFileSync } = require('child_process');
9
+
10
+ // The list endpoint, not /releases/latest: the newest release may lack this platform's assets.
11
+ const RELEASES_URL = 'https://api.github.com/repos/hybridlabor-api/bdb-ao-codenotch/releases?per_page=30';
12
+ const stateFilePath = (home = os.homedir()) => path.join(home, '.agents', '.bdb-codenotch.json');
13
+
14
+ // 'off' wins over 'force'; otherwise macOS installs default on.
15
+ function codenotchMode(argv = process.argv, env = process.env) {
16
+ if (argv.includes('--no-codenotch') || env.AOS_CODENOTCH === '0') return 'off';
17
+ if (argv.includes('--codenotch') || env.AOS_CODENOTCH === '1') return 'force';
18
+ return 'default';
19
+ }
20
+
21
+ function compareVersions(a, b) {
22
+ const parts = (v) => String(v).replace(/^v/i, '').split(/[^0-9]+/).filter(Boolean).map(Number);
23
+ const x = parts(a), y = parts(b);
24
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
25
+ const d = (x[i] || 0) - (y[i] || 0);
26
+ if (d) return d < 0 ? -1 : 1;
27
+ }
28
+ return 0;
29
+ }
30
+
31
+ const run = (cmd) => (args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
32
+ const defaultDeps = () => ({
33
+ fetch: (...a) => fetch(...a),
34
+ hdiutil: run('hdiutil'),
35
+ xattr: run('xattr'),
36
+ plutil: run('plutil'),
37
+ copyApp: (src, dest) => execFileSync('ditto', [src, dest]),
38
+ reg: run('reg'),
39
+ runInstaller: (exe, args, timeoutMs) => runWinProgram(require('child_process').spawnSync, exe, args, timeoutMs),
40
+ });
41
+
42
+ function readPlist(plutil, appPath) {
43
+ const plist = path.join(appPath, 'Contents', 'Info.plist');
44
+ if (!fs.existsSync(plist)) return null;
45
+ const get = (key) => {
46
+ try { return String(plutil(['-extract', key, 'raw', '-o', '-', plist])).trim() || null; } catch { return null; }
47
+ };
48
+ const version = get('CFBundleShortVersionString');
49
+ return version ? { version, bundleId: get('CFBundleIdentifier') } : null;
50
+ }
51
+
52
+ const readState = (file) => {
53
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
54
+ };
55
+
56
+ const isWritableDir = (dir) => {
57
+ try { if (!fs.statSync(dir).isDirectory()) return false; fs.accessSync(dir, fs.constants.W_OK); return true; } catch { return false; }
58
+ };
59
+
60
+ async function fetchLatestRelease(fetchFn, platform) {
61
+ let res;
62
+ try {
63
+ res = await fetchFn(RELEASES_URL, { headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'aos-installer' } });
64
+ } catch (e) {
65
+ return { error: `could not reach GitHub (${e.message}); try again later` };
66
+ }
67
+ if (res.status === 404) return { error: 'no public Codenotch release found yet (hybridlabor-api/bdb-ao-codenotch); try again later' };
68
+ const remaining = res.headers && res.headers.get && res.headers.get('x-ratelimit-remaining');
69
+ if (res.status === 429 || (res.status === 403 && remaining === '0')) return { error: 'GitHub API rate limit reached for unauthenticated requests; try again in about an hour' };
70
+ if (!res.ok) return { error: `GitHub releases API answered HTTP ${res.status}` };
71
+ const data = await res.json();
72
+ const release = selectRelease(Array.isArray(data) ? data : [data], platform);
73
+ return release ? { release } : { error: `no public Codenotch release found yet with a ${platform === 'win32' ? 'Windows Setup .exe' : '.dmg'} and a .sha256 asset; try again later` };
74
+ }
75
+
76
+ function pickMacAssets(assets) {
77
+ const dmg = assets.find((a) => /\.dmg$/i.test(a.name));
78
+ const sha = dmg && (assets.find((a) => a.name === `${dmg.name}.sha256`) || assets.find((a) => /\.sha256$/i.test(a.name)));
79
+ return dmg && sha ? { dmg, sha } : null;
80
+ }
81
+
82
+ // First (newest) non-draft, non-prerelease release that carries this platform's asset and a checksum.
83
+ function selectRelease(releases, platform) {
84
+ return releases.find((r) => {
85
+ if (!r || r.draft || r.prerelease) return false;
86
+ const tag = String(r.tag_name || '').replace(/^v/i, '');
87
+ return tag && !(platform === 'win32' ? pickWinAssets(r.assets || [], tag).error : !pickMacAssets(r.assets || []));
88
+ }) || null;
89
+ }
90
+
91
+ async function download(fetchFn, url) {
92
+ const res = await fetchFn(url, { headers: { 'User-Agent': 'aos-installer' }, redirect: 'follow' });
93
+ if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
94
+ return Buffer.from(await res.arrayBuffer());
95
+ }
96
+
97
+ function findInstalled(state, roots, plutil) {
98
+ const candidates = [state && state.path, ...[roots.applications, roots.userApplications].map((r) => path.join(r, 'Codenotch.app'))].filter(Boolean);
99
+ for (const appPath of candidates) {
100
+ if (!fs.existsSync(appPath)) continue;
101
+ const info = readPlist(plutil, appPath);
102
+ const ours = !!(state && state.path === appPath && info && info.bundleId === state.bundleId && info.version === state.version);
103
+ return { appPath, info, ours };
104
+ }
105
+ return null;
106
+ }
107
+
108
+ const UNINSTALL_ROOT = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall';
109
+ // UNVERIFIED: key name, DisplayVersion and UninstallString come from Tauri's stock NSIS template, not a real
110
+ // build; check on Windows with the first CI-built installer. The DisplayName scan is the fallback.
111
+ const NSIS_KEY = 'Codenotch';
112
+ const INSTALLER_TIMEOUT_MS = 5 * 60 * 1000;
113
+
114
+ function regValues(out) {
115
+ const vals = {};
116
+ for (const line of String(out).split(/\r?\n/)) {
117
+ const m = line.match(/^\s+(\S+)\s+REG_\w+\s+(.*?)\s*$/);
118
+ if (m) vals[m[1]] = m[2];
119
+ }
120
+ return vals;
121
+ }
122
+
123
+ function uninstallExeFrom(vals) {
124
+ const cmd = vals.QuietUninstallString || vals.UninstallString || '';
125
+ const m = cmd.match(/^\s*"([^"]+)"/) || cmd.match(/^\s*(.+?\.exe)/i);
126
+ if (m) return m[1];
127
+ return vals.InstallLocation ? path.win32.join(vals.InstallLocation, 'uninstall.exe') : null;
128
+ }
129
+
130
+ // Returns { version, uninstallPath } or null (not installed, key missing, or reg failing).
131
+ function readWinInstall(reg) {
132
+ const read = (key) => { try { return regValues(reg(['query', key])); } catch { return null; } };
133
+ let vals = read(`${UNINSTALL_ROOT}\\${NSIS_KEY}`);
134
+ if (!vals || !vals.DisplayVersion) {
135
+ vals = null;
136
+ let listing = '';
137
+ try { listing = reg(['query', UNINSTALL_ROOT]); } catch { return null; }
138
+ for (const sub of String(listing).split(/\r?\n/).map((l) => l.trim()).filter((l) => /^HKEY_/i.test(l))) {
139
+ const v = read(sub);
140
+ if (v && /^codenotch$/i.test(v.DisplayName || '') && v.DisplayVersion) { vals = v; break; }
141
+ }
142
+ }
143
+ return vals ? { version: vals.DisplayVersion, uninstallPath: uninstallExeFrom(vals) } : null;
144
+ }
145
+
146
+ function runWinProgram(spawnSyncFn, exe, args, timeoutMs) {
147
+ const r = spawnSyncFn(exe, args, { timeout: timeoutMs, windowsHide: true, stdio: 'ignore' });
148
+ if (r.error) throw new Error(r.error.code === 'ETIMEDOUT' ? `timed out after ${Math.round(timeoutMs / 1000)}s` : r.error.message);
149
+ if (r.status !== 0) throw new Error(`exited with code ${r.status}`);
150
+ }
151
+
152
+ function pickWinAssets(assets, latest) {
153
+ const exact = assets.find((a) => a.name === `Codenotch-Setup-${latest}.exe`);
154
+ const exes = exact ? [exact] : assets.filter((a) => /Setup.*\.exe$/i.test(a.name));
155
+ if (exes.length !== 1) return { error: exes.length ? 'latest release has several Setup .exe assets; cannot pick one' : 'latest release has no Windows Setup .exe asset' };
156
+ const sha = assets.find((a) => a.name === `${exes[0].name}.sha256`);
157
+ return sha ? { exe: exes[0], sha } : { error: 'latest release has no matching .sha256 asset; refusing to run an unverified installer' };
158
+ }
159
+
160
+ async function installCodenotchWin(o, log) {
161
+ const home = o.home || os.homedir();
162
+ const tmp = (o.roots && o.roots.tmp) || os.tmpdir();
163
+ const stateFile = o.stateFile || stateFilePath(home);
164
+ const interactive = !!o.interactive;
165
+ const confirm = o.confirm || (async () => false);
166
+ const fail = (status, message) => { log.warn(`Codenotch: ${message}`); return { status, message }; };
167
+
168
+ const { release, error } = await fetchLatestRelease(o.fetch, 'win32');
169
+ if (error) return fail(/rate limit/.test(error) ? 'rate-limited' : /no public/.test(error) ? 'not-found' : 'error', error);
170
+ const latest = String(release.tag_name || '').replace(/^v/i, '');
171
+ const picked = pickWinAssets(release.assets || [], latest);
172
+ if (!latest || picked.error) return fail('error', picked.error || 'latest release has no tag');
173
+
174
+ const state = readState(stateFile);
175
+ const installed = readWinInstall(o.reg);
176
+ let replacing = false;
177
+ if (installed) {
178
+ if (compareVersions(installed.version, latest) >= 0) {
179
+ const message = `already installed (${installed.version})${installed.uninstallPath ? ` at ${installed.uninstallPath}` : ''}`;
180
+ log.info(`Codenotch: ${message}`);
181
+ return { status: 'up-to-date', message };
182
+ }
183
+ const ours = !!(state && state.platform === 'win32' && state.version === installed.version && state.uninstallPath === installed.uninstallPath);
184
+ if (!ours && !interactive) {
185
+ const message = `an existing Codenotch ${installed.version} was not installed by AOS; keeping it (latest ${latest})`;
186
+ log.info(`Codenotch: ${message}`);
187
+ return { status: 'kept-foreign', message };
188
+ }
189
+ if (interactive && !(await confirm(`Replace Codenotch ${installed.version} with ${latest}?`))) {
190
+ return { status: 'declined', message: 'kept the installed Codenotch' };
191
+ }
192
+ replacing = true;
193
+ }
194
+
195
+ const work = fs.mkdtempSync(path.join(tmp, 'aos-codenotch-'));
196
+ try {
197
+ const buf = await download(o.fetch, picked.exe.browser_download_url);
198
+ const expected = ((await download(o.fetch, picked.sha.browser_download_url)).toString('utf8').match(/[0-9a-f]{64}/i) || [])[0];
199
+ if (!expected || expected.toLowerCase() !== crypto.createHash('sha256').update(buf).digest('hex')) {
200
+ return fail('checksum-mismatch', `sha256 mismatch for ${picked.exe.name}; refusing to run it`);
201
+ }
202
+ const exePath = path.join(work, picked.exe.name);
203
+ fs.writeFileSync(exePath, buf);
204
+
205
+ try { o.runInstaller(exePath, ['/S'], o.timeoutMs || INSTALLER_TIMEOUT_MS); }
206
+ catch (e) { return fail('error', `silent installer failed: ${e.message}`); }
207
+
208
+ const after = readWinInstall(o.reg);
209
+ if (!after || compareVersions(after.version, latest) < 0) return fail('error', `installer finished but the registry shows ${after ? after.version : 'no Codenotch'}; not recorded`);
210
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
211
+ fs.writeFileSync(stateFile, JSON.stringify({ platform: 'win32', uninstallPath: after.uninstallPath, version: after.version, installedAt: new Date().toISOString() }, null, 2));
212
+ const message = `${replacing ? 'updated' : 'installed'} Codenotch ${after.version}${after.uninstallPath ? ` (uninstaller: ${after.uninstallPath})` : ''}`;
213
+ log.success(`Codenotch: ${message}`);
214
+ return { status: replacing ? 'updated' : 'installed', message, path: after.uninstallPath, version: after.version };
215
+ } catch (e) {
216
+ return fail('error', e.message);
217
+ } finally {
218
+ fs.rmSync(work, { recursive: true, force: true });
219
+ }
220
+ }
221
+
222
+ // Returns { status, message }. Never throws, never elevates.
223
+ async function installCodenotch(opts = {}) {
224
+ const o = { ...defaultDeps(), ...opts };
225
+ const platform = o.platform || process.platform;
226
+ const log = { info() {}, warn() {}, success() {}, ...(o.log || {}) };
227
+ if (platform === 'win32') return installCodenotchWin(o, log);
228
+ if (platform !== 'darwin') return { status: 'skipped-platform' };
229
+
230
+ const home = o.home || os.homedir();
231
+ const roots = { applications: '/Applications', userApplications: path.join(home, 'Applications'), tmp: os.tmpdir(), ...(o.roots || {}) };
232
+ const stateFile = o.stateFile || stateFilePath(home);
233
+ const interactive = !!o.interactive;
234
+ const confirm = o.confirm || (async () => false);
235
+ const fail = (status, message) => { log.warn(`Codenotch: ${message}`); return { status, message }; };
236
+
237
+ const { release, error } = await fetchLatestRelease(o.fetch, 'darwin');
238
+ if (error) return fail(/rate limit/.test(error) ? 'rate-limited' : /no public/.test(error) ? 'not-found' : 'error', error);
239
+
240
+ const latest = String(release.tag_name || '').replace(/^v/i, '');
241
+ const { dmg, sha: shaAsset } = pickMacAssets(release.assets || []);
242
+
243
+ const state = readState(stateFile);
244
+ const installed = findInstalled(state, roots, o.plutil);
245
+ let destDir = null, replacing = null;
246
+ if (installed) {
247
+ const cur = installed.info && installed.info.version;
248
+ if (cur && compareVersions(cur, latest) >= 0) {
249
+ const message = `already installed (${cur}) at ${installed.appPath}`;
250
+ log.info(`Codenotch: ${message}`);
251
+ return { status: 'up-to-date', message };
252
+ }
253
+ if (!installed.ours && !interactive) {
254
+ const message = `${installed.appPath} was not installed by AOS; keeping it (installed ${cur || 'unknown version'}, latest ${latest})`;
255
+ log.info(`Codenotch: ${message}`);
256
+ return { status: 'kept-foreign', message };
257
+ }
258
+ if (interactive && !(await confirm(`Replace Codenotch ${cur || '(unknown version)'} at ${installed.appPath} with ${latest}?`))) {
259
+ return { status: 'declined', message: 'kept the installed Codenotch' };
260
+ }
261
+ replacing = installed.appPath;
262
+ destDir = path.dirname(replacing);
263
+ if (!isWritableDir(destDir)) return fail('error', `${destDir} is not writable; update Codenotch manually`);
264
+ }
265
+
266
+ const work = fs.mkdtempSync(path.join(roots.tmp, 'aos-codenotch-'));
267
+ const mountPoint = path.join(work, 'mnt');
268
+ let mounted = false;
269
+ try {
270
+ const dmgBuf = await download(o.fetch, dmg.browser_download_url);
271
+ const expected = ((await download(o.fetch, shaAsset.browser_download_url)).toString('utf8').match(/[0-9a-f]{64}/i) || [])[0];
272
+ const actual = crypto.createHash('sha256').update(dmgBuf).digest('hex');
273
+ if (!expected || expected.toLowerCase() !== actual) return fail('checksum-mismatch', `sha256 mismatch for ${dmg.name}; refusing to mount it`);
274
+ const dmgPath = path.join(work, dmg.name);
275
+ fs.writeFileSync(dmgPath, dmgBuf);
276
+
277
+ fs.mkdirSync(mountPoint);
278
+ o.hdiutil(['attach', '-nobrowse', '-readonly', '-mountpoint', mountPoint, dmgPath]);
279
+ mounted = true;
280
+
281
+ const apps = fs.readdirSync(mountPoint, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.endsWith('.app'));
282
+ if (apps.length !== 1) return fail('error', `expected exactly one .app in the DMG, found ${apps.length}`);
283
+ const appName = apps[0].name;
284
+ const src = path.join(mountPoint, appName);
285
+
286
+ let dest;
287
+ if (replacing) dest = replacing;
288
+ else {
289
+ destDir = isWritableDir(roots.applications) ? roots.applications : roots.userApplications;
290
+ fs.mkdirSync(destDir, { recursive: true });
291
+ dest = path.join(destDir, appName);
292
+ if (fs.existsSync(dest)) {
293
+ const cur = readPlist(o.plutil, dest);
294
+ if (!interactive || !(cur && compareVersions(cur.version, latest) < 0) || !(await confirm(`Replace ${dest} (not installed by AOS) with Codenotch ${latest}?`))) {
295
+ return { status: 'kept-foreign', message: `${dest} exists and was not installed by AOS; keeping it` };
296
+ }
297
+ replacing = dest;
298
+ }
299
+ }
300
+
301
+ const backup = replacing ? `${dest}.aos-old` : null;
302
+ if (backup) fs.renameSync(dest, backup);
303
+ try {
304
+ o.copyApp(src, dest);
305
+ o.xattr(['-dr', 'com.apple.quarantine', dest]);
306
+ } catch (e) {
307
+ fs.rmSync(dest, { recursive: true, force: true });
308
+ if (backup) fs.renameSync(backup, dest);
309
+ throw e;
310
+ }
311
+ if (backup) fs.rmSync(backup, { recursive: true, force: true });
312
+
313
+ const info = readPlist(o.plutil, dest) || { version: latest, bundleId: null };
314
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
315
+ fs.writeFileSync(stateFile, JSON.stringify({ path: dest, version: info.version, bundleId: info.bundleId, installedAt: new Date().toISOString() }, null, 2));
316
+ const message = `${replacing ? 'updated' : 'installed'} Codenotch ${info.version} at ${dest} (ad-hoc signed; quarantine removed)`;
317
+ log.success(`Codenotch: ${message}`);
318
+ return { status: replacing ? 'updated' : 'installed', message, path: dest, version: info.version };
319
+ } catch (e) {
320
+ return fail('error', e.message);
321
+ } finally {
322
+ if (mounted) {
323
+ try { o.hdiutil(['detach', mountPoint]); } catch {
324
+ try { o.hdiutil(['detach', mountPoint, '-force']); } catch { /* leave to the OS */ }
325
+ }
326
+ }
327
+ fs.rmSync(work, { recursive: true, force: true });
328
+ }
329
+ }
330
+
331
+ // Pure planning for aos-uninstall: only the recorded install, only if still the recorded build.
332
+ function planCodenotchUninstall({ stateFile = stateFilePath(), plutil = run('plutil'), reg = run('reg'), exists = fs.existsSync } = {}) {
333
+ const state = readState(stateFile);
334
+ if (!state) return null;
335
+ if (state.platform === 'win32') {
336
+ if (!state.uninstallPath) return null;
337
+ if (!exists(state.uninstallPath)) return { state, action: 'gone' };
338
+ const cur = readWinInstall(reg);
339
+ return { state, action: cur && cur.version === state.version && cur.uninstallPath === state.uninstallPath ? 'remove' : 'keep' };
340
+ }
341
+ if (!state.path) return null;
342
+ if (!fs.existsSync(state.path)) return { state, action: 'gone' };
343
+ const info = readPlist(plutil, state.path);
344
+ const same = info && info.bundleId === state.bundleId && info.version === state.version;
345
+ return { state, action: same ? 'remove' : 'keep' };
346
+ }
347
+
348
+ function uninstallCodenotch(plan, { stateFile = stateFilePath(), runUninstaller = (exe, args, t) => runWinProgram(require('child_process').spawnSync, exe, args, t) } = {}) {
349
+ if (!plan) return 'none';
350
+ if (plan.action === 'remove') {
351
+ if (plan.state.platform === 'win32') {
352
+ try { runUninstaller(plan.state.uninstallPath, ['/S'], INSTALLER_TIMEOUT_MS); } catch { return 'error'; }
353
+ } else fs.rmSync(plan.state.path, { recursive: true, force: true });
354
+ }
355
+ if (plan.action !== 'keep') fs.rmSync(stateFile, { force: true });
356
+ return plan.action;
357
+ }
358
+
359
+ // Never throws and never affects the caller's exit code. Returns a result or null (not run).
360
+ async function runCodenotchStep({ platform = process.platform, argv = process.argv, env = process.env, interactive, dryRun, ask, log = {}, install = installCodenotch, ...rest } = {}) {
361
+ if (platform !== 'darwin' && platform !== 'win32') return null;
362
+ try {
363
+ const mode = codenotchMode(argv, env);
364
+ if (mode === 'off') return { status: 'opted-out' };
365
+ if (interactive && mode === 'default' && ask && !(await ask('Install BDB AO Codenotch? (SHA-256 verified)'))) return { status: 'declined' };
366
+ if (dryRun) { if (log.info) log.info('[dry-run] would install BDB AO Codenotch'); return null; }
367
+ return await install({ platform, interactive, confirm: ask && ((m) => ask(m, false)), log, ...rest });
368
+ } catch (e) {
369
+ if (log.warn) log.warn(`Codenotch: ${e.message}`);
370
+ return { status: 'error', message: e.message };
371
+ }
372
+ }
373
+
374
+ function codenotchSummaryLine(r) {
375
+ if (!r || r.status === 'skipped-platform') return null;
376
+ const how = (p) => `remove with aos-uninstall${p ? ` (app: ${p})` : ''}`;
377
+ switch (r.status) {
378
+ case 'installed': case 'updated': return `Codenotch: ${r.status} ${r.version || ''} (${r.path}); ${how(r.path)}`.replace(' ', ' ');
379
+ case 'up-to-date': case 'kept-foreign': return `Codenotch: skipped (${r.message}); ${how()}`;
380
+ case 'opted-out': return 'Codenotch: skipped (opted out)';
381
+ case 'declined': return 'Codenotch: skipped (declined)';
382
+ default: return `Codenotch: FAILED (${r.message || r.status}); AOS install is unaffected, rerun later or use --no-codenotch to opt out`;
383
+ }
384
+ }
385
+
386
+ module.exports = {
387
+ RELEASES_URL, selectRelease, stateFilePath, readWinInstall, runWinProgram, uninstallExeFrom, codenotchMode, runCodenotchStep, codenotchSummaryLine, compareVersions, readPlist,
388
+ installCodenotch, planCodenotchUninstall, uninstallCodenotch,
389
+ };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+ // Retires skills that left the package from installed copies. Pure of installer state: every path
3
+ // derives from `home` or the passed roots. Only manifest-listed skill dirs are touched, and every
4
+ // file that is not a hash-identical installer copy is backed up before its directory is removed.
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+
9
+ const RETIRED_SKILLS = {
10
+ bdbsaastraining: '4.4.2',
11
+ 'bdb-dev-os-skill': '4.4.2',
12
+ bdbsaashost: '4.4.2',
13
+ 'bdb-ecosystem-health': '4.4.2',
14
+ 'bdbsaas-ops': '4.4.2',
15
+ 'visual-edit': '4.17.0',
16
+ 'visual-plan': '4.17.0',
17
+ 'visual-recap': '4.17.0',
18
+ };
19
+
20
+ const hashOf = (file) => {
21
+ try { return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); } catch { return null; }
22
+ };
23
+
24
+ const defaultRoots = (home) => [
25
+ path.join(home, '.agents', 'skills'),
26
+ path.join(home, '.claude', 'skills'),
27
+ path.join(home, '.codex', 'skills'),
28
+ path.join(home, '.cursor', 'skills'),
29
+ path.join(home, '.roo', 'skills'),
30
+ path.join(home, '.gemini', 'config', 'skills'),
31
+ ];
32
+
33
+ // Regular files of a dir tree; `unsafe` is true when a symlink or special entry is inside.
34
+ function walkFiles(dir, out = { files: [], unsafe: false }) {
35
+ let entries;
36
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { out.unsafe = true; return out; }
37
+ for (const e of entries) {
38
+ const abs = path.join(dir, e.name);
39
+ if (e.isDirectory()) walkFiles(abs, out);
40
+ else if (e.isFile()) out.files.push(abs);
41
+ else out.unsafe = true;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ function backupFile(home, backupDir, abs) {
47
+ const rel = path.relative(home, abs);
48
+ const dest = path.join(backupDir, rel.startsWith('..') || path.isAbsolute(rel) ? path.basename(abs) : rel);
49
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
50
+ fs.copyFileSync(abs, dest);
51
+ }
52
+
53
+ function pruneRetiredSkills({ home, manifest, roots = defaultRoots(home), backupDir, log = {} }) {
54
+ const res = { removed: [], backedUp: [] };
55
+ if (!manifest) return res;
56
+ const note = (level, msg) => { if (typeof log[level] === 'function') log[level](msg); };
57
+
58
+ const dirs = new Map();
59
+ for (const [file, entry] of Object.entries(manifest)) {
60
+ if (typeof file !== 'string' || !path.isAbsolute(file)) continue;
61
+ const root = roots.find((r) => file.startsWith(r + path.sep));
62
+ if (!root) continue;
63
+ const parts = file.slice(root.length + 1).split(path.sep);
64
+ if (parts.includes('..') || !Object.hasOwn(RETIRED_SKILLS, parts[0])) continue;
65
+ const dir = path.join(root, parts[0]);
66
+ if (!dirs.has(dir)) dirs.set(dir, new Map());
67
+ dirs.get(dir).set(path.resolve(file), { key: file, sha256: entry && entry.sha256 });
68
+ }
69
+
70
+ for (const [dir, owned] of dirs) {
71
+ let st;
72
+ try { st = fs.lstatSync(dir); } catch { for (const o of owned.values()) delete manifest[o.key]; continue; }
73
+ if (!st.isDirectory()) continue;
74
+ const { files, unsafe } = walkFiles(dir);
75
+ if (unsafe) { note('warn', `[retired-skills] ${dir} holds a symlink or special entry; left in place.`); continue; }
76
+
77
+ const toBackup = files.filter((f) => {
78
+ const o = owned.get(path.resolve(f));
79
+ return !o || hashOf(f) !== o.sha256;
80
+ });
81
+ try {
82
+ for (const f of toBackup) backupFile(home, backupDir, f);
83
+ } catch (e) {
84
+ note('warn', `[retired-skills] backup of ${dir} failed (${e.code || e.message}); left in place.`);
85
+ continue;
86
+ }
87
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (e) {
88
+ note('warn', `[retired-skills] could not remove ${dir} (${e.code || e.message}).`);
89
+ continue;
90
+ }
91
+ res.backedUp.push(...toBackup);
92
+ res.removed.push(dir);
93
+ for (const o of owned.values()) delete manifest[o.key];
94
+ }
95
+
96
+ if (res.backedUp.length) note('warn', `[retired-skills] ${res.backedUp.length} edited or foreign file(s) from retired skills backed up to ${backupDir}.`);
97
+ if (res.removed.length) note('step', `Removed ${res.removed.length} retired skill install(s) no longer shipped.`);
98
+ return res;
99
+ }
100
+
101
+ module.exports = { RETIRED_SKILLS, pruneRetiredSkills, defaultRoots };
@@ -7,7 +7,7 @@ native Claude Code subagents) are installed and authenticated on a machine, and
7
7
  routes delegated subtasks to the right one via a capability-tier YAML rulebook
8
8
  — never a pinned model ID, since free-tier model IDs churn within days.
9
9
 
10
- Status: Core package implemented and tested. Includes an **MCP Server** (`packages/mcp/server.js`) that allows any MCP-compatible client to securely delegate tasks to local agent CLIs. The architecture includes rigorous recursion protection (via `MCSC_CALLER` environment variables) to prevent autonomous agents from calling themselves in infinite loops.
10
+ Status: Core package implemented and tested. Includes an **MCP Server** (`packages/mcp/server.js`) that allows any MCP-compatible client to securely delegate tasks to local agent CLIs. The architecture includes rigorous recursion protection (via `MCSC_CALLER` environment variables) to prevent autonomous agents from calling themselves in infinite loops. `MCSC_DEPTH` (raised by one in every adapter's child env) limits delegation to one level: a server that starts at depth 1 or more refuses all calls. `delegate_agy` is documented read-only; its `write` flag (`--mode accept-edits`) is untested.
11
11
 
12
12
  See [.openwiki/architecture.md](.openwiki/architecture.md) for the validated design.
13
13
 
@@ -1,6 +1,7 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import { emitTrail } from '../trail.js';
4
+ import { childEnv } from '../depth.js';
4
5
  const execFileAsync = promisify(execFile);
5
6
 
6
7
  /**
@@ -22,6 +23,7 @@ export async function delegate(req) {
22
23
  try {
23
24
  const args = [];
24
25
  if (req.model) args.push('--model', req.model);
26
+ if (req.write === true) args.push('--mode', 'accept-edits');
25
27
  args.push(
26
28
  '--print-timeout',
27
29
  '15m',
@@ -33,7 +35,7 @@ export async function delegate(req) {
33
35
  req.prompt
34
36
  );
35
37
 
36
- const env = { ...process.env, MCSC_CALLER: 'agy' };
38
+ const env = childEnv('agy');
37
39
 
38
40
  const run = execFileAsync(
39
41
  'agy',
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { emitTrail } from '../trail.js';
3
+ import { childEnv } from '../depth.js';
3
4
 
4
5
  const MAX_OUTPUT = 1024 * 1024 * 50; // 50MB, mirrors the old execFile maxBuffer
5
6
 
@@ -39,7 +40,7 @@ export async function delegate(req) {
39
40
  };
40
41
  emitTrail({ ...trailEvent, hook_event_name: 'SessionStart' });
41
42
 
42
- const env = { ...process.env, MCSC_CALLER: 'codex' };
43
+ const env = childEnv('codex');
43
44
  const args = ['exec', '--json', '--skip-git-repo-check'];
44
45
  if (req.model) args.push('-m', req.model);
45
46
  args.push(req.prompt);
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import { emitTrail } from '../trail.js';
4
+ import { childEnv } from '../depth.js';
4
5
 
5
6
  const MAX_OUTPUT = 1024 * 1024 * 50; // 50MB, mirrors the old execFile maxBuffer
6
7
 
@@ -25,7 +26,7 @@ export async function delegate(req) {
25
26
  };
26
27
  emitTrail({ ...trailEvent, hook_event_name: 'SessionStart' });
27
28
 
28
- const env = { ...process.env, MCSC_CALLER: 'opencode' };
29
+ const env = childEnv('opencode');
29
30
  const args = ['run', '--auto', '--format', 'json'];
30
31
  if (req.model) args.push('--model', req.model);
31
32
  if (req.variant) args.push('--variant', String(req.variant));
@@ -0,0 +1,16 @@
1
+ // One level of delegation, like a fork: every adapter raises MCSC_DEPTH for its child,
2
+ // and a server that starts with depth >= MAX_DEPTH refuses to delegate.
3
+ export const MAX_DEPTH = 1;
4
+
5
+ export const currentDepth = (env = process.env) => {
6
+ const n = Number.parseInt(env.MCSC_DEPTH ?? '', 10);
7
+ return Number.isInteger(n) && n > 0 ? n : 0;
8
+ };
9
+
10
+ export const childEnv = (caller, env = process.env) => ({
11
+ ...env,
12
+ MCSC_CALLER: caller,
13
+ MCSC_DEPTH: String(currentDepth(env) + 1),
14
+ });
15
+
16
+ export const depthExceeded = (env = process.env) => currentDepth(env) >= MAX_DEPTH;
@@ -11,6 +11,7 @@ import { getCached, setCached } from "../core/src/detect/cache.js";
11
11
  import { delegate as agyDelegate } from "../core/src/adapters/agy.js";
12
12
  import { delegate as opencodeDelegate } from "../core/src/adapters/opencode.js";
13
13
  import { delegate as codexDelegate } from "../core/src/adapters/codex.js";
14
+ import { depthExceeded } from "../core/src/depth.js";
14
15
  import { decide } from "../core/src/rulebook/resolve.js";
15
16
  import { loadRulebook } from "../core/src/rulebook/load.js";
16
17
  import { buildOfferTable } from "../core/src/capabilities/buildOfferTable.js";
@@ -92,13 +93,14 @@ const server = new Server(
92
93
  // --- Tool Registration ---
93
94
  server.setRequestHandler(ListToolsRequestSchema, async () => {
94
95
  const tools = [];
96
+ if (depthExceeded()) return { tools };
95
97
 
96
98
  // delegate_agy
97
99
  const agyHealth = inventory.get('agy');
98
100
  if (agyHealth && agyHealth.status === 'ready' && caller !== 'agy') {
99
101
  tools.push({
100
102
  name: "delegate_agy",
101
- description: "Delegate a task to Antigravity CLI (Gemini 3.8 Flash or Claude 3.7 Sonnet). Provides full coding capabilities.",
103
+ description: "Delegate a READ-ONLY task to Antigravity CLI (Gemini 3.8 Flash or Claude 3.7 Sonnet). Write tasks are untested: do not rely on it for edits.",
102
104
  inputSchema: {
103
105
  type: "object",
104
106
  properties: {
@@ -110,6 +112,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
110
112
  type: "string",
111
113
  description: "Optional model hint for the subagent (e.g., 'flash', 'pro'). Default varies by tool.",
112
114
  default: "flash"
115
+ },
116
+ write: {
117
+ type: "boolean",
118
+ description: "UNTESTED. Adds --mode accept-edits for a write task. Leave unset: delegate_agy is documented read-only."
113
119
  }
114
120
  },
115
121
  required: ["prompt"]
@@ -194,7 +200,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
194
200
 
195
201
  // --- Tool Execution ---
196
202
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
197
- const { name, arguments: args } = request.params;
203
+ const { name, arguments: args = {} } = request.params;
204
+ if (depthExceeded()) {
205
+ return {
206
+ content: [{ type: "text", text: "Delegation refused: MCSC_DEPTH limit reached (one level of delegation, like a fork). Do the task yourself." }],
207
+ isError: true,
208
+ };
209
+ }
198
210
  const signal = extra.signal;
199
211
 
200
212
  const req = {
@@ -202,6 +214,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
202
214
  prompt: args.prompt,
203
215
  model: args.model,
204
216
  variant: args.variant,
217
+ write: args.write === true,
205
218
  signal
206
219
  };
207
220