@hybridlabor-api/aos 4.17.0 → 4.18.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 (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 +129 -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
@@ -68,17 +68,21 @@ export function touchSession(name, pid = process.pid) {
68
68
 
69
69
  const alive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; } };
70
70
 
71
- export function listSessions() {
71
+ // prune: also delete registrations that are dead, stale or corrupt (registration files only, never inboxes).
72
+ export function listSessions({ prune = false } = {}) {
72
73
  const dir = join(homedir(), ".aos", "bus", "sessions");
73
74
  const out = [];
74
75
  let files = [];
75
76
  try { files = readdirSync(dir).filter((f) => f.endsWith(".json")); } catch { /* none */ }
76
77
  for (const f of files) {
78
+ const file = join(dir, f);
79
+ let live = false;
77
80
  try {
78
- const file = join(dir, f);
79
81
  const r = JSON.parse(readFileSync(file, "utf8"));
80
- if (Number.isInteger(r.pid) && alive(r.pid) && Date.now() - lstatSync(file).mtimeMs < STALE_MS) out.push(r);
82
+ live = Number.isInteger(r.pid) && alive(r.pid) && Date.now() - lstatSync(file).mtimeMs < STALE_MS;
83
+ if (live) out.push(r);
81
84
  } catch { /* corrupt entry */ }
85
+ if (prune && !live) { try { unlinkSync(file); } catch { /* already gone */ } }
82
86
  }
83
87
  return out;
84
88
  }
@@ -134,7 +138,7 @@ if (isMain()) {
134
138
  if (cmd === "send" && words.length >= 2) {
135
139
  console.log(sendMessage(words[0], words.slice(1).join(" "), opts));
136
140
  } else if (cmd === "list") {
137
- for (const s of listSessions()) console.log(`${s.name}\tpid ${s.pid}\t${s.cwd}`);
141
+ for (const s of listSessions({ prune: true })) console.log(`${s.name}\tpid ${s.pid}\t${s.cwd}`);
138
142
  } else {
139
143
  console.error("usage: aos-bus send <name> <text...> [--from x] [--wake] | aos-bus list");
140
144
  process.exit(2);
@@ -751,7 +751,7 @@ export function statusText(key, eff, now = Date.now()) {
751
751
  export const listStateKeys = () => {
752
752
  try { return readdirSync(gateDir()).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)); } catch { return []; }
753
753
  };
754
- const TOKEN_TTL_MS = 10 * 60 * 1000;
754
+ export const TOKEN_TTL_MS = 10 * 60 * 1000;
755
755
 
756
756
  function ownSessionName(transcriptPath) {
757
757
  let name = "";
@@ -3,11 +3,11 @@
3
3
  // master session, record a single-use token that go-gate.mjs in <session-name>
4
4
  // accepts. Writes nothing else and never blocks the prompt.
5
5
 
6
- import { readFileSync, writeFileSync, mkdirSync, realpathSync } from "node:fs";
6
+ import { readFileSync, writeFileSync, mkdirSync, realpathSync, readdirSync, unlinkSync } from "node:fs";
7
7
  import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import { slug } from "./go-gate.mjs";
10
+ import { slug, TOKEN_TTL_MS } from "./go-gate.mjs";
11
11
 
12
12
  function sessionName(transcriptPath) {
13
13
  let name = "";
@@ -24,6 +24,20 @@ function sessionName(transcriptPath) {
24
24
  return name || process.env.AOS_SESSION_NAME || "";
25
25
  }
26
26
 
27
+ // Expired or unreadable tokens are never read again by anyone, so a write is the moment to drop them.
28
+ export function pruneExpiredTokens(dir, now = Date.now()) {
29
+ let files = [];
30
+ try { files = readdirSync(dir).filter((f) => f.endsWith(".token")); } catch { return 0; }
31
+ let removed = 0;
32
+ for (const f of files) {
33
+ let age = NaN;
34
+ try { age = now - Date.parse(JSON.parse(readFileSync(join(dir, f), "utf8"))?.issued_at); } catch { /* unreadable: drop */ }
35
+ if (age >= 0 && age < TOKEN_TTL_MS) continue;
36
+ try { unlinkSync(join(dir, f)); removed++; } catch { /* already gone */ }
37
+ }
38
+ return removed;
39
+ }
40
+
27
41
  // Returns the token path, or null. Never throws. An aos-acp worker is a worker
28
42
  // by definition and must not mint a token for itself.
29
43
  export function issueGoToken(prompt, { transcript_path, session_id, message_id } = {}) {
@@ -38,6 +52,7 @@ export function issueGoToken(prompt, { transcript_path, session_id, message_id }
38
52
  if (!target || !tok) return null;
39
53
  const dir = join(homedir(), ".aos", "go");
40
54
  mkdirSync(dir, { recursive: true });
55
+ pruneExpiredTokens(dir);
41
56
  const file = join(dir, `${target}.token`);
42
57
  writeFileSync(file, JSON.stringify(tok));
43
58
  return file;
@@ -1,32 +1,41 @@
1
1
  #!/usr/bin/env node
2
- // aos-hook-version: 7
2
+ // aos-hook-version: 8
3
3
  /**
4
4
  * memB ambient memory hook for Claude Code, Google Antigravity, and OpenAI Codex.
5
5
  *
6
6
  * Stamped with aos-hook-version for /aos-setup doctor validation.
7
7
  * Reads ~/.MemBDB/memb.db directly through node:sqlite and injects relevant
8
- * project & user memories as ephemeral context in tri-format JSON:
9
- * - hookSpecificOutput (Claude Code UserPromptSubmit)
8
+ * project & user memories as ephemeral context, emitting only the key the
9
+ * calling harness reads:
10
+ * - hookSpecificOutput.additionalContext (Claude Code, OpenAI Codex)
10
11
  * - injectSteps (Google Antigravity PreInvocation)
11
- * - systemMessage (OpenAI Codex UserPromptSubmit)
12
+ *
13
+ * Reads rows of the configured user (MEMB_USER_ID, fallback $USER) plus the
14
+ * group ids in MEMB_GROUP_IDS (comma list, default bdb_developer). Rows bound
15
+ * to the current project are injected whatever their category (max 5, 180
16
+ * chars each); global rows still need category `godmode`.
12
17
  *
13
18
  * Event-aware: SessionStart injects the persona file, the global godmode
14
19
  * identity rows, and the current project's cards once per session; every
15
20
  * other prompt (UserPromptSubmit) recalls only FTS keyword hits so 40
16
21
  * identity facts are not re-sent on every message. Harnesses without a
17
22
  * session hook (e.g. Antigravity PreInvocation) get the full block plus
18
- * identity on each invocation.
23
+ * identity on the first invocation of a conversation (cached per
24
+ * conversationId); later invocations recall FTS hits only.
19
25
  *
20
26
  * Fails open: any error exits 0 silently.
21
27
  */
22
28
 
23
- import { readFileSync, existsSync, realpathSync } from 'node:fs';
29
+ import { readFileSync, existsSync, realpathSync, mkdirSync, writeFileSync } from 'node:fs';
24
30
  import os from 'node:os';
25
31
  import path from 'node:path';
26
32
  import { fileURLToPath } from 'node:url';
27
33
  import { execFileSync } from 'node:child_process';
28
34
 
29
35
  const COLLECTION = 'bdb_agent_memory';
36
+ const DEFAULT_GROUP_IDS = 'bdb_developer';
37
+ const PROJECT_ROW_CAP = 5;
38
+ const ROW_CHARS = 180;
30
39
  const failOpen = () => process.exit(0);
31
40
 
32
41
  // `deja wip --json` run in the project root, rendered as context lines.
@@ -51,6 +60,14 @@ export function dejaWip(cwd, run = execFileSync) {
51
60
  }
52
61
  }
53
62
 
63
+ // Rows are read for the user plus the configured group ids; nothing is
64
+ // hard-coded beyond the documented default group.
65
+ export function resolveReaderIds({ userId, env = process.env } = {}) {
66
+ const user = env.MEMB_USER_ID || userId || env.USER || env.LOGNAME || '';
67
+ const groups = String(env.MEMB_GROUP_IDS ?? DEFAULT_GROUP_IDS).split(',').map((g) => g.trim());
68
+ return Array.from(new Set([user, ...groups].filter(Boolean)));
69
+ }
70
+
54
71
  // Filler words (German + English) that must never become FTS query terms —
55
72
  // they match virtually any document and would inject unrelated projects.
56
73
  const STOPWORDS = new Set([
@@ -160,14 +177,7 @@ export async function buildMemoryBlock({ event = '', prompt = '', cwd, userId }
160
177
  const projectRoot = findProjectRoot(rawCandidateDir, home);
161
178
  const candidateProjectIds = resolveProjectIdentifiers(projectRoot, home);
162
179
 
163
- // User identification: reconcile active user and bdb_developer baseline
164
- const activeUser =
165
- process.env.MEMB_USER_ID ||
166
- userId ||
167
- process.env.USER ||
168
- process.env.LOGNAME ||
169
- 'bdb_developer';
170
- const candidateUsers = Array.from(new Set([activeUser, 'bdb_developer'])).filter(Boolean);
180
+ const candidateUsers = resolveReaderIds({ userId });
171
181
 
172
182
  const contextItems = [];
173
183
 
@@ -259,12 +269,10 @@ export async function buildMemoryBlock({ event = '', prompt = '', cwd, userId }
259
269
  OR json_extract(payload, '$.user_id') IN (${uPlaceholders})
260
270
  OR json_extract(payload, '$.metadata.user_id') IN (${uPlaceholders})
261
271
  )
262
- AND (
263
- json_extract(payload, '$.category') IN ('project_card','godmode')
264
- OR json_extract(payload, '$.metadata.category') IN ('project_card','godmode')
265
- )
266
- ORDER BY rowid DESC
267
- LIMIT 5
272
+ ORDER BY (COALESCE(json_extract(payload, '$.category'),
273
+ json_extract(payload, '$.metadata.category')) = 'project_card') DESC,
274
+ rowid DESC
275
+ LIMIT ${PROJECT_ROW_CAP * 4}
268
276
  `;
269
277
  const params = [
270
278
  COLLECTION,
@@ -276,11 +284,14 @@ export async function buildMemoryBlock({ event = '', prompt = '', cwd, userId }
276
284
  ...candidateUsers,
277
285
  ];
278
286
  const rows = db.prepare(sql).all(...params);
287
+ let taken = 0;
279
288
  for (const r of rows) {
289
+ if (taken >= PROJECT_ROW_CAP) break;
280
290
  const text = textOf(r.payload);
281
- if (text.length > 10) {
282
- contextItems.push(`- Project [${candidateProjectIds[0]}]: ${text.slice(0, 180)}`);
283
- }
291
+ if (text.length <= 10 || /^\[[^\]|]+\|[^\]|]+\|/.test(text)) continue; // too short / imported file chunk
292
+ if (contextItems.some((e) => e.includes(text.slice(0, 50)))) continue;
293
+ contextItems.push(`- Project [${candidateProjectIds[0]}]: ${text.slice(0, ROW_CHARS)}`);
294
+ taken++;
284
295
  }
285
296
  } catch { /* project query is best-effort */ }
286
297
  }
@@ -316,7 +327,7 @@ export async function buildMemoryBlock({ event = '', prompt = '', cwd, userId }
316
327
  const hitProject = projectOf(r.payload);
317
328
  if (hitProject && !candidateProjectIds.includes(hitProject)) continue;
318
329
  const cat = categoryOf(r.payload);
319
- if (cat !== 'project_card' && cat !== 'godmode') continue;
330
+ if (!hitProject && cat !== 'project_card' && cat !== 'godmode') continue;
320
331
  if (!contextItems.some((e) => e.includes(text.slice(0, 50)))) {
321
332
  contextItems.push(`- Domain memory: ${text.slice(0, 180)}`);
322
333
  }
@@ -338,6 +349,19 @@ export async function buildMemoryBlock({ event = '', prompt = '', cwd, userId }
338
349
  }
339
350
  }
340
351
 
352
+ // Antigravity has no session hook: remember per conversation that identity and
353
+ // project rows were already sent, so later invocations do only the FTS recall
354
+ // and skip `deja wip`. Stamps under ~/.aos/cache are disposable.
355
+ export function agyIdentitySent(conversationId, base = path.join(os.homedir(), '.aos', 'cache')) {
356
+ const id = String(conversationId || '').replace(/[^\w.-]/g, '_').slice(0, 80);
357
+ if (!id) return false;
358
+ const dir = path.join(base, 'memb-agy');
359
+ const stamp = path.join(dir, id);
360
+ if (existsSync(stamp)) return true;
361
+ try { mkdirSync(dir, { recursive: true }); writeFileSync(stamp, ''); } catch {}
362
+ return false;
363
+ }
364
+
341
365
  // Main logic runs only when the file is executed directly as a hook; importing
342
366
  // the module (e.g. from tests) must stay side-effect free.
343
367
  async function main() {
@@ -351,26 +375,26 @@ async function main() {
351
375
 
352
376
  // Discover candidate directory across harnesses:
353
377
  // Claude / Codex: eventData.cwd; Antigravity: eventData.workspacePaths[0] or eventData.cwd
378
+ const isAgy = !eventData.hook_event_name && (Array.isArray(eventData.workspacePaths) || !!eventData.conversationId);
379
+ const event = isAgy && agyIdentitySent(eventData.conversationId)
380
+ ? 'UserPromptSubmit'
381
+ : (eventData.hook_event_name || '');
354
382
  const memoryBlock = await buildMemoryBlock({
355
- event: eventData.hook_event_name || '',
383
+ event,
356
384
  prompt: eventData.prompt || eventData.userPrompt || '',
357
385
  cwd: (Array.isArray(eventData.workspacePaths) && eventData.workspacePaths[0]) || eventData.cwd,
358
386
  userId: eventData.user_id || eventData.userId,
359
387
  });
360
388
 
361
389
  if (memoryBlock) {
362
- process.stdout.write(JSON.stringify({
363
- hookSpecificOutput: {
364
- hookEventName: eventData.hook_event_name || 'UserPromptSubmit',
365
- additionalContext: memoryBlock,
366
- },
367
- injectSteps: [
368
- {
369
- ephemeralMessage: memoryBlock,
370
- }
371
- ],
372
- systemMessage: memoryBlock,
373
- }) + '\n');
390
+ process.stdout.write(JSON.stringify(isAgy
391
+ ? { injectSteps: [{ ephemeralMessage: memoryBlock }] }
392
+ : {
393
+ hookSpecificOutput: {
394
+ hookEventName: eventData.hook_event_name || 'UserPromptSubmit',
395
+ additionalContext: memoryBlock,
396
+ },
397
+ }) + '\n');
374
398
  }
375
399
  process.exit(0);
376
400
  } catch {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bdb-aos",
3
- "version": "4.17.0",
3
+ "version": "4.18.0",
4
4
  "description": "Full BDB Agent OS suite for Codex: command entry points ($bdb-aos:setup, $bdb-aos:init, ...) generated from plugin-commands.json.",
5
5
  "author": {
6
6
  "name": "Tim Rennings / Hybridlabor Global LLC",
@@ -2,4 +2,4 @@
2
2
  description: "Plan end to end: draft, render in plan-canvas or Plan Builder, annotate, await approval, then hand off."
3
3
  ---
4
4
 
5
- Run the full planning pipeline: 1) draft the plan with `concise-planning`; 2) always offer the choice between plan-canvas and Plan Builder (`visual-plan`) for rendering; 3) collect annotations and wait for the human's approval; 4) only after approval continue with `writing-plans` or `/bdb-aos-graph`. Topic: $ARGUMENTS
5
+ Run the full planning pipeline: 1) draft the plan with `concise-planning`; 2) always offer the choice between plan-canvas and Plan Builder (`aos-plan-canvas open <dir> --mode bdb-plan-builder`) for rendering; 3) collect annotations and wait for the human's approval; 4) only after approval continue with `writing-plans` or `/bdb-aos-graph`. Topic: $ARGUMENTS
package/README.md CHANGED
@@ -118,6 +118,7 @@ What the installer writes for each target. Paths are the defaults; the installer
118
118
  | Roo Code / Cline | `~/.roo/skills` | — | — | `.roomodes` (project) |
119
119
  | Aider | `~/.aider/bdb-skills` | — | — | — |
120
120
  | AOS CLI (`pi`) | reads `~/.agents/skills` | `~/.agents/AGENTS.md` as system prompt | — | no MCP; separate install, Node >= 22.19 — see [packages/aos-cli](packages/aos-cli/README.md) |
121
+ | BDB AO Codenotch (macOS app, Windows installer) | macOS `/Applications` or `~/Applications`; Windows per-user NSIS install (`/S`, no admin) | — | — | installed by default on macOS and Windows (never Linux); opt out with `--no-codenotch` or `AOS_CODENOTCH=0`; failures only warn; DMG from the public releases repo created by Tim, SHA-256 verified, ad-hoc signed with quarantine removed, see [docs/codenotch.md](docs/codenotch.md) |
121
122
 
122
123
  Every install also writes the universal copy to `~/.agents/skills`, which is what the AOS CLI and the `skills` CLI read.
123
124
 
@@ -309,7 +309,7 @@ dependency.
309
309
 
310
310
  <https://github.com/BuilderIO/skills> — MIT. Copyright (c) 2026 Builder.io.
311
311
 
312
- Eleven skills are adapted under `skills/global_config/`: `read-the-damn-docs`, `stay-within-limits`, `quick-recap`, `plan-arbiter`, `factory-human-digest`, `factory-collect`, `factory-lookback`, `factory-review-prs`, `visual-plan`, `visual-recap`, and `visual-edit`. The four Factory skills and `plan-arbiter` have `installer-group` dropped and `category`/`source` added; in `factory-collect`, `factory-lookback` and `factory-review-prs` reply, close, approve, merge and publish actions are reduced to drafts or readiness reports that need the user's GO, and all of them carry an "AOS safety rules" section. `plan-arbiter` drops `agents/openai.yaml` and adds an AOS section on double plans. `stay-within-limits` and `quick-recap` are carried over with `category:` and `source:` added; `stay-within-limits` pins `ccusage` to 20.0.26 instead of `@latest`, and `quick-recap` no longer asks the installer to inject a managed `AGENTS.md` / `CLAUDE.md` block. The three visual skills (`visual-plan`, `visual-recap`, `visual-edit`) are optional and drive the Agent-Native Plan/Design services through MCP connectors (`plan` and `design`) that AOS does NOT register; you connect them yourself if needed. Each carries an "AOS notes" section. `read-the-damn-docs` is described below:
312
+ Eight skills are adapted under `skills/global_config/`: `read-the-damn-docs`, `stay-within-limits`, `quick-recap`, `plan-arbiter`, `factory-human-digest`, `factory-collect`, `factory-lookback`, and `factory-review-prs`. The four Factory skills and `plan-arbiter` have `installer-group` dropped and `category`/`source` added; in `factory-collect`, `factory-lookback` and `factory-review-prs` reply, close, approve, merge and publish actions are reduced to drafts or readiness reports that need the user's GO, and all of them carry an "AOS safety rules" section. `plan-arbiter` drops `agents/openai.yaml` and adds an AOS section on double plans. `stay-within-limits` and `quick-recap` are carried over with `category:` and `source:` added; `stay-within-limits` pins `ccusage` to 20.0.26 instead of `@latest`, and `quick-recap` no longer asks the installer to inject a managed `AGENTS.md` / `CLAUDE.md` block. `read-the-damn-docs` is described below:
313
313
  the docs-first trigger list, the source hierarchy, the required workflow, the
314
314
  must-trigger examples, and the "if docs are unavailable" rule. AOS additionally
315
315
  adds a Verification section, a `category:` key (Builder's own validator forbids
@@ -318,7 +318,7 @@ guesswork* rule, and a pointer to the installed `firecrawl-search` /
318
318
  `firecrawl-scrape` tools. Prose only — the upstream `agents/openai.yaml` and
319
319
  README were not carried over, and no executable from the repo is referenced.
320
320
 
321
- In the three visual skills, five path-like mentions (`/sign-in`, `/live-edit-bridge`, `/read-file`, `/write-file`, `/visualize-repo`) were reworded so the AOS validator does not read them as skill references. The other 13 skills in that repo (as of commit 0dba9ef) are **not** vendored. Three of them
321
+ The other 13 skills in that repo (as of commit 0dba9ef) are **not** vendored. Three of them
322
322
  (`an`, `turn-into-app`, `rewind`) plus three others
323
323
  are build artefacts synced from a different upstream by
324
324
  `scripts/sync-agent-native-skills.mjs`, and the repo's own documentation says
package/bin/aos-acp.mjs CHANGED
@@ -14,10 +14,11 @@
14
14
  import { spawn } from "node:child_process";
15
15
  import { appendFileSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
16
16
  import { homedir } from "node:os";
17
- import { dirname, join } from "node:path";
17
+ import { dirname, isAbsolute, join, resolve } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { createInterface } from "node:readline";
20
20
  import { GUARDED_PATTERNS, slug, tokenGrantsGo } from "../.claude/hooks/go-gate.mjs";
21
+ import { emitTrail } from "../mcps/mcsc/packages/core/src/trail.js";
21
22
 
22
23
  export const ADAPTERS = {
23
24
  claude: { cmd: ["npx", "-y", "@agentclientprotocol/claude-agent-acp"], consume: false },
@@ -113,6 +114,25 @@ export class AcpClient {
113
114
  notify(method, params) { this.write({ jsonrpc: "2.0", method, params }); }
114
115
  }
115
116
 
117
+ // Same agenttrail protocol as mcsc's adapters (SessionStart/SessionEnd, PreToolUse/PostToolUse),
118
+ // so the GO board reads ACP workers and mcsc runs from one source.
119
+ const cap = (t) => (t ? t.charAt(0).toUpperCase() + t.slice(1) : "Tool");
120
+
121
+ export function trailToolEvent(u, seen, cwd) {
122
+ const prev = seen.get(u.toolCallId) || {};
123
+ const info = { title: u.title ?? prev.title, kind: u.kind ?? prev.kind };
124
+ seen.set(u.toolCallId, info);
125
+ const done = u.status === "completed" || u.status === "failed";
126
+ if (u.sessionUpdate === "tool_call_update" && !done) return null;
127
+ const tool_input = {};
128
+ const file = u.locations?.[0]?.path ?? prev.file;
129
+ if (typeof file === "string" && file) tool_input.file_path = isAbsolute(file) ? file : resolve(cwd, file);
130
+ info.file = tool_input.file_path;
131
+ const command = commandOf(u);
132
+ if (command) tool_input.command = command;
133
+ return { hook_event_name: done ? "PostToolUse" : "PreToolUse", tool_name: cap(info.kind || info.title), tool_input };
134
+ }
135
+
116
136
  function textOf(block) {
117
137
  return block && block.type === "text" ? block.text : "";
118
138
  }
@@ -134,6 +154,9 @@ export async function run(opts) {
134
154
  const consume = opts.consume ?? adapter?.consume ?? true;
135
155
  log("start", { adapter: opts.adapter, cmd: cmd.join(" "), cwd: opts.cwd, allow_default: opts.allowDefault, consume, model: opts.model || "adapter default" });
136
156
 
157
+ const trail = { session_id: `aos-acp-${process.pid}-${Date.now()}`, cwd: opts.cwd, agent: `${opts.adapter}:${opts.name}` };
158
+ const seenTools = new Map();
159
+ emitTrail({ ...trail, hook_event_name: "SessionStart" });
137
160
  let sessionId;
138
161
  const out = opts.stdout || process.stdout;
139
162
  const client = new AcpClient(cmd, {
@@ -148,6 +171,8 @@ export async function run(opts) {
148
171
  if (t) { out.write(t); log("text", { text: t }); }
149
172
  } else if (u.sessionUpdate === "tool_call" || u.sessionUpdate === "tool_call_update") {
150
173
  log(u.sessionUpdate, { id: u.toolCallId, title: u.title, kind: u.kind, status: u.status, command: commandOf(u) || undefined });
174
+ const ev = trailToolEvent(u, seenTools, opts.cwd);
175
+ if (ev) emitTrail({ ...trail, ...ev });
151
176
  } else log("update", { kind: u.sessionUpdate });
152
177
  },
153
178
  onRequest: (method, p) => {
@@ -182,6 +207,7 @@ export async function run(opts) {
182
207
  } finally {
183
208
  if (timer) clearTimeout(timer);
184
209
  client.child.kill();
210
+ await emitTrail({ ...trail, hook_event_name: "SessionEnd" });
185
211
  }
186
212
  }
187
213
 
@@ -169,6 +169,40 @@ function checkAosCore() {
169
169
  }
170
170
  }
171
171
 
172
+ // JSONC to JSON: drops comments (not inside strings) and trailing commas.
173
+ function parseJsonc(text) {
174
+ let out = '';
175
+ for (let i = 0, str = false; i < text.length; i++) {
176
+ const c = text[i];
177
+ if (str) { out += c; if (c === '\\') out += text[++i] ?? ''; else if (c === '"') str = false; continue; }
178
+ if (c === '"') { str = true; out += c; continue; }
179
+ if (c === '/' && text[i + 1] === '/') { while (i < text.length && text[i] !== '\n') i++; out += '\n'; continue; }
180
+ if (c === '/' && text[i + 1] === '*') { i += 2; while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++; i++; continue; }
181
+ out += c;
182
+ }
183
+ try { return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1')); } catch { return null; }
184
+ }
185
+
186
+ // Zen gateway limit: a tool name (OpenCode builds it from the MCP server name) is at most 64 characters.
187
+ // Names only, no network: the tool part of the name is not known here.
188
+ const ZEN_TOOL_NAME_MAX = 64;
189
+
190
+ function checkOpencodeMcpNames() {
191
+ const dirs = [process.platform === 'win32' ? path.join(process.env.APPDATA || HOME, 'opencode') : h('.config', 'opencode')];
192
+ const file = firstExisting(dirs.flatMap((d) => ['opencode.jsonc', 'opencode.json'].map((f) => path.join(d, f))));
193
+ if (!file) return;
194
+ const conf = parseJsonc(readFileSync(file, 'utf8'));
195
+ if (!conf) {
196
+ add('harnesses', 'OpenCode MCP names', false, `${tilde(file)} is not valid JSON(C); names not checked`, 'Fix the OpenCode config syntax.', true);
197
+ return;
198
+ }
199
+ const names = Object.keys(conf.mcp && typeof conf.mcp === 'object' ? conf.mcp : {});
200
+ const tooLong = names.filter((n) => n.length > ZEN_TOOL_NAME_MAX);
201
+ add('harnesses', 'OpenCode MCP names', tooLong.length === 0,
202
+ tooLong.length ? `${tooLong.length} MCP name(s) over ${ZEN_TOOL_NAME_MAX} characters (Zen gateway limit): ${tooLong.join(', ')}` : `${names.length} MCP name(s) in ${tilde(file)}, none over ${ZEN_TOOL_NAME_MAX} characters`,
203
+ `Rename the MCP server(s) in ${tilde(file)} to a shorter name; tool names are built from them and a name over ${ZEN_TOOL_NAME_MAX} characters is rejected by the Zen gateway. Keeping the MCP set lean also helps.`, true);
204
+ }
205
+
172
206
  // ---------------------------------------------------------------- 3. Harness Placement & Skills Sync
173
207
  function checkHarnesses() {
174
208
  const harnesses = [
@@ -199,12 +233,13 @@ function checkHarnesses() {
199
233
  add('harnesses', 'OpenCode Plugin', !!opencodePlugin,
200
234
  opencodePlugin ? tilde(opencodePlugin) : 'bdb-aos.js not installed in OpenCode plugins',
201
235
  'Run the AOS installer to wire OpenCode telemetry plugin.', true);
236
+ checkOpencodeMcpNames();
202
237
  }
203
238
 
204
239
  // ---------------------------------------------------------------- 4. Hooks & Security Gates
205
240
  function checkHooks() {
206
241
  const claudeHooksDir = h('.claude', 'hooks');
207
- const EXPECTED_VERSION = { 'memb-inject.mjs': 7 };
242
+ const EXPECTED_VERSION = { 'memb-inject.mjs': 8 };
208
243
  const versionOf = (text) => {
209
244
  const m = /^\/\/\s*aos-hook-version:\s*(\d+)/m.exec(text);
210
245
  return m ? Number(m[1]) : null;
@@ -25,6 +25,7 @@ import path from 'node:path';
25
25
  import { createRequire } from 'node:module';
26
26
 
27
27
  const pm = createRequire(import.meta.url)('../lib/plugin-migration.js');
28
+ const cn = createRequire(import.meta.url)('../lib/codenotch.js');
28
29
 
29
30
  const HOME = os.homedir();
30
31
  const h = (...p) => path.join(HOME, ...p);
@@ -103,7 +104,9 @@ function plan() {
103
104
  const legacy = LEGACY_MARKERS.filter(existsSync);
104
105
  const data = PURGE ? DATA_PATHS.filter((d) => existsSync(d.path)) : [];
105
106
 
106
- return { manifest, ours, edited, gone, agents, modules, legacy, data };
107
+ const codenotch = cn.planCodenotchUninstall({ stateFile: cn.stateFilePath(HOME) });
108
+
109
+ return { manifest, ours, edited, gone, agents, modules, legacy, data, codenotch };
107
110
  }
108
111
 
109
112
  function describe(p) {
@@ -140,6 +143,12 @@ function describe(p) {
140
143
  for (const l of legacy) console.log(` ${tilde(l)}`);
141
144
  }
142
145
 
146
+ if (p.codenotch) {
147
+ const c = p.codenotch;
148
+ const note = { remove: 'is removed (still the recorded build)', keep: 'is kept (replaced or modified since AOS installed it)', gone: 'is already gone' }[c.action];
149
+ console.log(`\nBDB AO Codenotch ${tilde(c.state.path || c.state.uninstallPath)} ${note}`);
150
+ }
151
+
143
152
  console.log('\nBleibt erhalten:');
144
153
  if (!PURGE) for (const d of DATA_PATHS.filter((d) => existsSync(d.path))) console.log(` ${tilde(d.path).padEnd(18)} ${d.what}`);
145
154
  console.log(' jede Datei ohne Manifest-Eintrag — eigene Skills, fremde Hooks, alles Selbstgeschriebene');
@@ -169,7 +178,7 @@ function dirSize(p) {
169
178
 
170
179
  // ------------------------------------------------------------------ execute
171
180
  function execute(p) {
172
- const { ours, edited, agents, modules, legacy, data } = p;
181
+ const { ours, edited, agents, modules, legacy, data, codenotch } = p;
173
182
  const stamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
174
183
  let removed = 0, backed = 0;
175
184
 
@@ -242,6 +251,11 @@ function execute(p) {
242
251
  } catch { console.log(` ${tilde(codexToml)} nicht lesbar — von Hand prüfen`); }
243
252
  }
244
253
 
254
+ if (codenotch) {
255
+ const r = cn.uninstallCodenotch(codenotch, { stateFile: cn.stateFilePath(HOME) });
256
+ console.log(` Codenotch: ${r === 'remove' ? 'removed' : r === 'keep' ? 'kept, not the recorded build' : r === 'error' ? 'uninstaller failed, left in place' : 'already gone'}`);
257
+ }
258
+
245
259
  for (const l of legacy) { try { rmSync(l); } catch { /* already gone */ } }
246
260
  if (legacy.length) console.log(` ${legacy.length} Installations-Marker entfernt`);
247
261
 
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // go-check: one fixed answer to "may this worker run this guarded command?" for callers that
3
+ // are not a harness hook (AO). Installed at ~/.aos/bin/go-check.mjs.
4
+ //
5
+ // node go-check.mjs --session <Name> --command "<shell string>" [--consume]
6
+ // node go-check.mjs --write-patterns <file>
7
+ //
8
+ // stdout: {"guarded":bool,"ok":bool,"scope":[...]|null,"reason":"..."}
9
+ // exit 0 allowed (not guarded, or a valid GO token; --consume uses it up), 1 guarded without a
10
+ // valid GO, 2 error (the caller must treat 2 as denied). Only `GO <Name>` tokens count: mode
11
+ // soft grants and mode off belong to the human session and are never read here. Nothing in
12
+ // this file writes a token; the tool is synchronous, offline and reads no stdin.
13
+
14
+ import { existsSync, realpathSync, writeFileSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+
20
+ async function loadGate() {
21
+ for (const f of [join(here, "go-gate.mjs"), join(here, "..", ".claude", "hooks", "go-gate.mjs")]) {
22
+ if (existsSync(f)) return import(pathToFileURL(f).href);
23
+ }
24
+ throw new Error("go-gate.mjs not found next to go-check.mjs or in ../.claude/hooks");
25
+ }
26
+
27
+ export const patternsJson = (gate) => ({
28
+ version: 1,
29
+ source: "go-gate.mjs GUARDED_PATTERNS (JavaScript RegExp syntax, lookaheads included; not RE2)",
30
+ scopes: gate.SCOPES,
31
+ patterns: gate.GUARDED_PATTERNS.map((r) => ({ source: r.source, flags: r.flags })),
32
+ });
33
+
34
+ export function check(gate, { session, command, consume = false }) {
35
+ if (typeof command !== "string" || !command.trim()) return { code: 2, out: { guarded: null, ok: false, scope: null, reason: "--command is empty" } };
36
+ const storeReason = gate.gateStoreReason(command);
37
+ if (storeReason) return { code: 1, out: { guarded: true, ok: false, scope: null, reason: storeReason } };
38
+ if (!gate.isGuardedCommand(command)) return { code: 0, out: { guarded: false, ok: true, scope: [], reason: "" } };
39
+ const scope = gate.commandScopes(command);
40
+ if (typeof session !== "string" || !gate.slug(session)) {
41
+ return { code: 2, out: { guarded: true, ok: false, scope, reason: "--session is missing or has no usable name" } };
42
+ }
43
+ const r = gate.tokenGrantsGo(session, { consume });
44
+ return r.ok
45
+ ? { code: 0, out: { guarded: true, ok: true, scope, reason: consume ? "GO token consumed" : "GO token valid (not consumed)" } }
46
+ : { code: 1, out: { guarded: true, ok: false, scope, reason: r.reason } };
47
+ }
48
+
49
+ export function parseArgs(argv) {
50
+ const o = {};
51
+ for (let i = 0; i < argv.length; i++) {
52
+ const a = argv[i];
53
+ if (a === "--session") o.session = argv[++i];
54
+ else if (a === "--command") o.command = argv[++i];
55
+ else if (a === "--write-patterns") o.writePatterns = argv[++i];
56
+ else if (a === "--consume") o.consume = true;
57
+ else throw new Error(`unknown argument ${a}`);
58
+ }
59
+ return o;
60
+ }
61
+
62
+ const done = (code, out) => process.stdout.write(JSON.stringify(out) + "\n", () => process.exit(code));
63
+
64
+ const invokedAs = (() => { try { return realpathSync(process.argv[1] || ""); } catch { return ""; } })();
65
+ if (invokedAs === fileURLToPath(import.meta.url)) {
66
+ try {
67
+ const o = parseArgs(process.argv.slice(2));
68
+ const gate = await loadGate();
69
+ if (o.writePatterns) {
70
+ writeFileSync(o.writePatterns, JSON.stringify(patternsJson(gate), null, 2) + "\n");
71
+ done(0, { ok: true, reason: `wrote ${o.writePatterns}` });
72
+ } else {
73
+ const { code, out } = check(gate, o);
74
+ done(code, out);
75
+ }
76
+ } catch (e) {
77
+ done(2, { guarded: null, ok: false, scope: null, reason: String(e?.message || e).slice(0, 300) });
78
+ }
79
+ }