@hybridlabor-api/aos 4.13.2 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.agents/AGENTS.md +8 -0
  2. package/.agents/nodes.json +5 -2
  3. package/.claude/hooks/conventional-commits.mjs +14 -15
  4. package/.claude/hooks/env-file-protection.mjs +14 -15
  5. package/.claude/hooks/go-gate.mjs +152 -10
  6. package/.claude/hooks/go-token.mjs +55 -0
  7. package/.claude/hooks/memb-inject.mjs +75 -62
  8. package/.claude/hooks/trail-autostart.mjs +27 -0
  9. package/.claude/settings.json +18 -0
  10. package/.claude/workflows/startcycle-dispatch.mjs +11 -4
  11. package/.opencode/plugins/bdb-aos.js +98 -121
  12. package/.opencode/plugins/lib/trail-autostart.js +38 -0
  13. package/CLAUDE.md +1 -1
  14. package/README.de.md +6 -6
  15. package/README.md +6 -6
  16. package/README.pt.md +6 -6
  17. package/THIRD_PARTY_NOTICES.md +19 -3
  18. package/assets/header-v5.png +0 -0
  19. package/bin/aos-acp.mjs +211 -0
  20. package/bin/aos-doctor.mjs +1 -1
  21. package/bin/aos-uninstall.mjs +2 -2
  22. package/docs/master-session-acp.md +51 -0
  23. package/installer.js +252 -35
  24. package/mcps/mcsc/packages/mcp/server.js +6 -7
  25. package/package.json +4 -3
  26. package/scripts/validate-skills.mjs +76 -0
  27. package/skills/basic/bdbmediastorm/SKILL.md +1 -1
  28. package/skills/basic/godmode-shipping/SKILL.md +3 -0
  29. package/skills/basic/master-session/SKILL.md +89 -0
  30. package/skills/basic/startcycle/SKILL.md +1 -1
  31. package/skills/basic/startcycle-graph/SKILL.md +2 -2
  32. package/skills/basic/startcycle-graph-user/SKILL.md +1 -1
  33. package/skills/basic/teamwork-preview/SKILL.md +1 -1
  34. package/skills/bdbrainstorm/SKILL.md +7 -1
  35. package/skills/global_config/agentic-harness-patterns/SKILL.md +257 -0
  36. package/skills/global_config/agentic-harness-patterns/metadata.json +10 -0
  37. package/skills/global_config/agentic-harness-patterns/references/agent-orchestration-pattern.md +97 -0
  38. package/skills/global_config/agentic-harness-patterns/references/bootstrap-sequence-pattern.md +106 -0
  39. package/skills/global_config/agentic-harness-patterns/references/context-engineering/compress-pattern.md +78 -0
  40. package/skills/global_config/agentic-harness-patterns/references/context-engineering/isolate-pattern.md +82 -0
  41. package/skills/global_config/agentic-harness-patterns/references/context-engineering/select-pattern.md +86 -0
  42. package/skills/global_config/agentic-harness-patterns/references/context-engineering-pattern.md +29 -0
  43. package/skills/global_config/agentic-harness-patterns/references/hook-lifecycle-pattern.md +111 -0
  44. package/skills/global_config/agentic-harness-patterns/references/memory-persistence-pattern.md +109 -0
  45. package/skills/global_config/agentic-harness-patterns/references/permission-gate-pattern.md +111 -0
  46. package/skills/global_config/agentic-harness-patterns/references/skill-runtime-pattern.md +104 -0
  47. package/skills/global_config/agentic-harness-patterns/references/task-decomposition-pattern.md +92 -0
  48. package/skills/global_config/agentic-harness-patterns/references/tool-registry-pattern.md +101 -0
  49. package/skills/global_config/agenttrail/SKILL.md +8 -0
  50. package/skills/global_config/agenttrail/bin/agenttrail.mjs +14 -0
  51. package/skills/global_config/agenttrail/bin/ensure.mjs +118 -0
  52. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  53. package/skills/global_config/bdb-visual-edit/SKILL.md +51 -0
  54. package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +59 -0
  55. package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +27 -0
  56. package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +123 -0
  57. package/skills/global_config/factory-collect/SKILL.md +74 -0
  58. package/skills/global_config/factory-human-digest/SKILL.md +92 -0
  59. package/skills/global_config/factory-lookback/SKILL.md +95 -0
  60. package/skills/global_config/factory-review-prs/SKILL.md +63 -0
  61. package/skills/global_config/git-pr-review/SKILL.md +3 -0
  62. package/skills/global_config/grilling/SKILL.md +2 -0
  63. package/skills/global_config/mcsc/SKILL.md +1 -1
  64. package/skills/global_config/plan-arbiter/SKILL.md +125 -0
  65. package/skills/global_config/plan-canvas/SKILL.md +62 -5
  66. package/skills/global_config/plan-canvas/scripts/lib/loopback-guard.js +19 -3
  67. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +285 -0
  68. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/agent-trail.js +129 -0
  69. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/board-client.js +124 -0
  70. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/canvas.mdx +19 -0
  71. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/plan.mdx +18 -0
  72. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/recap-demo/plan.mdx +72 -0
  73. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/README.md +29 -0
  74. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/architecture.json +30 -0
  75. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/00_architecture.html +14950 -0
  76. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/canvas.mdx +511 -0
  77. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/plan.mdx +208 -0
  78. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/recap/plan.mdx +102 -0
  79. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/standard/plan.md +136 -0
  80. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/canvas.mdx +124 -0
  81. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/plan.mdx +37 -0
  82. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/index.js +188 -0
  83. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/kit.js +123 -0
  84. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/mdx.js +411 -0
  85. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +1291 -0
  86. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/meta.json +1 -0
  87. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/plan.mdx +195 -0
  88. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/standard.md +95 -0
  89. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/meta.json +1 -0
  90. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/plan.mdx +105 -0
  91. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/standard.md +76 -0
  92. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/canvas.mdx +81 -0
  93. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/meta.json +1 -0
  94. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/plan.mdx +145 -0
  95. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/standard.md +76 -0
  96. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/meta.json +1 -0
  97. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/plan.mdx +172 -0
  98. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/standard.md +100 -0
  99. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/meta.json +1 -0
  100. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/plan.mdx +67 -0
  101. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/standard.md +49 -0
  102. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/canvas.mdx +63 -0
  103. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/meta.json +1 -0
  104. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/plan.mdx +49 -0
  105. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/standard.md +39 -0
  106. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/meta.json +1 -0
  107. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/plan.mdx +118 -0
  108. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/standard.md +57 -0
  109. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/meta.json +1 -0
  110. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/plan.mdx +173 -0
  111. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/standard.md +96 -0
  112. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/meta.json +1 -0
  113. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/plan.mdx +91 -0
  114. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/standard.md +54 -0
  115. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/canvas.mdx +53 -0
  116. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/meta.json +1 -0
  117. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/plan.mdx +225 -0
  118. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/standard.md +111 -0
  119. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/theme.css +472 -0
  120. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/trail.js +216 -0
  121. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/markdown.js +1 -1
  122. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +37 -4
  123. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +125 -29
  124. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +196 -8
  125. package/skills/global_config/pr-recap/SKILL.md +47 -0
  126. package/skills/global_config/pr-recap/scripts/pr-recap.mjs +200 -0
  127. package/skills/global_config/quick-recap/SKILL.md +55 -0
  128. package/skills/global_config/stay-within-limits/SKILL.md +85 -0
  129. package/skills/global_config/triage/SKILL.md +3 -0
  130. package/skills/global_config/visual-edit/README.md +96 -0
  131. package/skills/global_config/visual-edit/SKILL.md +615 -0
  132. package/skills/global_config/visual-plan/README.md +93 -0
  133. package/skills/global_config/visual-plan/SKILL.md +544 -0
  134. package/skills/global_config/visual-plan/references/canvas.md +139 -0
  135. package/skills/global_config/visual-plan/references/connection.md +51 -0
  136. package/skills/global_config/visual-plan/references/document-quality.md +186 -0
  137. package/skills/global_config/visual-plan/references/exemplar.md +62 -0
  138. package/skills/global_config/visual-plan/references/local-files.md +99 -0
  139. package/skills/global_config/visual-plan/references/wireframe.md +319 -0
  140. package/skills/global_config/visual-recap/README.md +103 -0
  141. package/skills/global_config/visual-recap/SKILL.md +560 -0
  142. package/skills/global_config/visual-recap/references/connection.md +51 -0
  143. package/skills/global_config/visual-recap/references/local-files.md +99 -0
  144. package/skills/global_config/visual-recap/references/wireframe.md +319 -0
  145. package/skills/playbooks/pb-ci-fix/SKILL.md +49 -0
  146. package/skills/playbooks/pb-event-tracker/SKILL.md +45 -0
  147. package/skills/playbooks/pb-meeting-actions/SKILL.md +42 -0
  148. package/skills/playbooks/pb-project-new/SKILL.md +48 -0
  149. package/skills/playbooks/pb-week-plan/SKILL.md +45 -0
  150. package/assets/header-v4.jpg +0 -0
package/.agents/AGENTS.md CHANGED
@@ -19,6 +19,7 @@ next. This file defines *what each agent is*, not *what calls what*.
19
19
  - `bdbrainstorm`
20
20
  - `planning-with-files`
21
21
  - `concise-planning`
22
+ - `plan-arbiter`
22
23
  - **MCP Servers**:
23
24
  - `openwiki-skill`
24
25
  - `memb_mcp`
@@ -39,6 +40,7 @@ next. This file defines *what each agent is*, not *what calls what*.
39
40
  - `startcycle-graph`
40
41
  - `agent-pipeline`
41
42
  - `subagent-driven-development`
43
+ - `plan-arbiter`
42
44
  - **MCP Servers**:
43
45
  - `memb_mcp`
44
46
  - **Output Artifact**: capability-map approval recorded in `state.json` (no separate markdown file — this is a gate, not a deliverable)
@@ -59,6 +61,8 @@ next. This file defines *what each agent is*, not *what calls what*.
59
61
  - `react-best-practices`
60
62
  - `ui-component`
61
63
  - `ui-tokens`
64
+ - `bdbdesignpro`
65
+ - `editable-design`
62
66
  - **MCP Servers**:
63
67
  - `open_design_mcp`
64
68
  - `chrome-devtools`
@@ -79,6 +83,7 @@ next. This file defines *what each agent is*, not *what calls what*.
79
83
  - `postgres-best-practices`
80
84
  - `typescript-pro`
81
85
  - `python-pro`
86
+ - `bdbresilience`
82
87
  - **MCP Servers**:
83
88
  - `github`
84
89
  - `memb_mcp`
@@ -138,6 +143,9 @@ next. This file defines *what each agent is*, not *what calls what*.
138
143
  - `wcag-audit-patterns`
139
144
  - `github-repo`
140
145
  - `clean-code`
146
+ - `brag`
147
+ - `bdbresilience`
148
+ - `bdb-security-audit`
141
149
  - **MCP Servers**:
142
150
  - `github`
143
151
  - `chrome-devtools`
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
3
  "description": "Declarative node registry for /startcycle-graph. The dispatcher reads this instead of hardcoding BUILD_NODES and NODE_ENUM. Adding a build domain = adding an entry here, not editing the dispatcher.",
4
+ "common": ["read-the-damn-docs", "agenttrail", "archify", "plan-canvas", "stay-within-limits", "quick-recap", "verification-before-completion"],
4
5
  "nodes": {
5
6
  "architect": {
6
7
  "label": "Architect",
@@ -16,7 +17,8 @@
16
17
  "bdbrainstorm",
17
18
  "planning-with-files",
18
19
  "concise-planning",
19
- "archify"
20
+ "archify",
21
+ "plan-arbiter"
20
22
  ],
21
23
  "instructions": null
22
24
  },
@@ -33,7 +35,8 @@
33
35
  "read-the-damn-docs",
34
36
  "startcycle-graph",
35
37
  "agent-pipeline",
36
- "subagent-driven-development"
38
+ "subagent-driven-development",
39
+ "plan-arbiter"
37
40
  ],
38
41
  "instructions": null
39
42
  },
@@ -19,7 +19,8 @@
19
19
  // failure here, because the consequence (history AOS does not recognise) is
20
20
  // recoverable with a rebase, and a wedged commit flow is not.
21
21
 
22
- import { readFileSync } from "node:fs";
22
+ import { readFileSync, realpathSync } from "node:fs";
23
+ import { fileURLToPath } from "node:url";
23
24
 
24
25
  const TYPES = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "ci", "build", "revert"];
25
26
 
@@ -35,7 +36,7 @@ function readStdin() {
35
36
  }
36
37
  }
37
38
 
38
- function respond(isAgy, allowed, reason = "", message = "") {
39
+ function respond(isAgy, allowed, reason = "") {
39
40
  if (isAgy) {
40
41
  if (allowed) {
41
42
  console.log(JSON.stringify({ decision: "allow" }));
@@ -87,6 +88,14 @@ function reasonFor(message) {
87
88
  ].join("\n");
88
89
  }
89
90
 
91
+ export function checkConventionalCommit(command) {
92
+ if (typeof command !== "string" || !COMMIT.test(command)) return null;
93
+ const message = extractMessage(command);
94
+ return message === null || CONVENTIONAL.test(message) ? null : reasonFor(message);
95
+ }
96
+
97
+ const isMain = () => { try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } };
98
+
90
99
  function main() {
91
100
  const rawInput = readStdin();
92
101
  let input = {};
@@ -108,18 +117,8 @@ function main() {
108
117
  input?.command ||
109
118
  "";
110
119
 
111
- if (typeof command !== "string" || !COMMIT.test(command)) {
112
- respond(isAgy, true);
113
- return;
114
- }
115
-
116
- const message = extractMessage(command);
117
- if (message === null || CONVENTIONAL.test(message)) {
118
- respond(isAgy, true);
119
- return;
120
- }
121
-
122
- respond(isAgy, false, reasonFor(message), message);
120
+ const reason = checkConventionalCommit(command);
121
+ respond(isAgy, !reason, reason || "");
123
122
  }
124
123
 
125
- main();
124
+ if (isMain()) main();
@@ -18,7 +18,8 @@
18
18
  // every write in the session; missing a protection is recoverable, freezing
19
19
  // the agent is not.
20
20
 
21
- import { readFileSync } from "node:fs";
21
+ import { readFileSync, realpathSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
22
23
  import { basename } from "node:path";
23
24
 
24
25
  // Templates are committed on purpose and hold no live secret. Blocking them
@@ -67,6 +68,15 @@ function reasonFor(filePath) {
67
68
  ].join("\n");
68
69
  }
69
70
 
71
+ export function envFileReason(filePath) {
72
+ if (typeof filePath !== "string" || !filePath.trim()) return null;
73
+ // basename(), so a path like /srv/app/.env matches but /srv/app/env-notes.md does not.
74
+ const name = basename(filePath);
75
+ return !ENV_FILE.test(name) || TEMPLATE_FILE.test(name) ? null : reasonFor(filePath);
76
+ }
77
+
78
+ const isMain = () => { try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } };
79
+
70
80
  function main() {
71
81
  const rawInput = readStdin();
72
82
  let input = {};
@@ -87,19 +97,8 @@ function main() {
87
97
  input?.file_path ||
88
98
  "";
89
99
 
90
- if (typeof filePath !== "string" || !filePath.trim()) {
91
- respond(isAgy, true);
92
- return;
93
- }
94
-
95
- // basename(), so a path like /srv/app/.env matches but /srv/app/env-notes.md does not.
96
- const name = basename(filePath);
97
- if (!ENV_FILE.test(name) || TEMPLATE_FILE.test(name)) {
98
- respond(isAgy, true);
99
- return;
100
- }
101
-
102
- respond(isAgy, false, reasonFor(filePath));
100
+ const reason = envFileReason(filePath);
101
+ respond(isAgy, !reason, reason || "");
103
102
  }
104
103
 
105
- main();
104
+ if (isMain()) main();
@@ -15,10 +15,34 @@
15
15
  //
16
16
  // Gate validity: open ONLY if the LAST human-typed user message in the transcript
17
17
  // is the literal word "GO" (case-insensitive, trimmed). Any other message closes it.
18
+ //
19
+ // Token path (master-session): a `GO <session>` typed by the human in a master
20
+ // session is turned into ~/.aos/go/<session>.token by go-token.mjs. This gate
21
+ // honours that token once, if it is <10 min old, names this session, and the
22
+ // master transcript it points at still ends with that very `GO <session>`.
23
+ // Own session name: latest agent-name/custom-title entry in this transcript
24
+ // (what --name / ListAgents show), else env AOS_SESSION_NAME. If neither is
25
+ // available the token path stays closed.
26
+ //
27
+ // A token is single-use per master message: the master's GO entry uuid (Claude)
28
+ // or message id (OpenCode) goes into ~/.aos/go/.consumed, so a re-minted token
29
+ // on an unchanged master cannot be replayed.
30
+ //
31
+ // Threat model: this stops accidental or replayed GOs, not an agent that can
32
+ // write arbitrary files; it can forge a master transcript. Nothing in-process fixes that.
33
+ //
34
+ // OpenCode-issued tokens (issuer "opencode") are verified offline against
35
+ // opencode.db, read-only. Limits: the schema is internal and undocumented, only
36
+ // the default XDG / ~/.local/share data dir is read, and bun:sqlite is an
37
+ // unverified fallback. Any miss fails closed; the human types a literal GO.
18
38
 
19
- import { readFileSync } from "node:fs";
39
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, realpathSync } from "node:fs";
40
+ import { createRequire } from "node:module";
41
+ import { homedir } from "node:os";
42
+ import { join } from "node:path";
43
+ import { fileURLToPath } from "node:url";
20
44
 
21
- const GUARDED_PATTERNS = [
45
+ export const GUARDED_PATTERNS = [
22
46
  /(?:^|[;&|]\s*)git\s+push\b/i,
23
47
  /(?:^|[;&|]\s*)npm\s+publish\b/i,
24
48
  /(?:^|[;&|]\s*)npm\s+version\b/i,
@@ -83,12 +107,125 @@ function extractAgyText(rawContent) {
83
107
  return text.trim();
84
108
  }
85
109
 
110
+ export const slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
111
+ const TOKEN_TTL_MS = 10 * 60 * 1000;
112
+
113
+ function ownSessionName(transcriptPath) {
114
+ let name = "";
115
+ try {
116
+ for (const line of readFileSync(transcriptPath, "utf8").split("\n")) {
117
+ if (!line.includes('"agent-name"') && !line.includes('"custom-title"')) continue;
118
+ try {
119
+ const e = JSON.parse(line);
120
+ if (e.type === "agent-name" && e.agentName) name = e.agentName;
121
+ else if (e.type === "custom-title" && e.customTitle && !name) name = e.customTitle;
122
+ } catch { /* partial line */ }
123
+ }
124
+ } catch { /* unreadable */ }
125
+ return name || process.env.AOS_SESSION_NAME || "";
126
+ }
127
+
128
+ const CONSUMED_KEEP = 200;
129
+ const consumedFile = () => join(homedir(), ".aos", "go", ".consumed");
130
+ const readConsumed = () => {
131
+ try { return readFileSync(consumedFile(), "utf8").split("\n").filter(Boolean); } catch (e) { return e.code === "ENOENT" ? [] : null; }
132
+ };
133
+
134
+ function opencodeLastUser(sessionId) {
135
+ let db;
136
+ try {
137
+ const dbPath = join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "opencode", "opencode.db");
138
+ if (typeof sessionId !== "string" || !existsSync(dbPath)) return null;
139
+ const require = createRequire(import.meta.url);
140
+ try {
141
+ db = new (require("node:sqlite").DatabaseSync)(dbPath, { readOnly: true });
142
+ } catch {
143
+ db = new (require("bun:sqlite").Database)(dbPath, { readonly: true });
144
+ }
145
+ const ses = db.prepare("SELECT parent_id FROM session WHERE id = ?").get(sessionId);
146
+ if (!ses || ses.parent_id != null) return null; // subagent sessions are agent-written, never a human GO
147
+ const msg = db.prepare(
148
+ "SELECT id FROM message WHERE session_id = ? AND json_extract(data, '$.role') = 'user' ORDER BY time_created DESC, id DESC LIMIT 1"
149
+ ).get(sessionId);
150
+ if (!msg) return null;
151
+ const text = db.prepare("SELECT data FROM part WHERE message_id = ? ORDER BY id").all(msg.id)
152
+ .map((r) => JSON.parse(r.data))
153
+ .filter((p) => p?.type === "text" && !p.synthetic && typeof p.text === "string")
154
+ .map((p) => p.text)
155
+ .join("\n")
156
+ .trim();
157
+ return { id: msg.id, text };
158
+ } catch {
159
+ return null;
160
+ } finally {
161
+ try { db?.close(); } catch { /* already closed */ }
162
+ }
163
+ }
164
+
165
+ // Shared with bin/aos-acp.mjs (ACP permission requests). `consume: false`
166
+ // verifies without deleting the token, for a caller whose inner gate owns it.
167
+ export function tokenGrantsGo(own, { consume = true } = {}) {
168
+ if (typeof own !== "string" || !slug(own)) return { ok: false, reason: "no session name derivable for token path" };
169
+ const file = join(homedir(), ".aos", "go", `${slug(own)}.token`);
170
+ if (!existsSync(file)) return { ok: false, reason: "no GO token for this session" };
171
+ let tok;
172
+ try {
173
+ tok = JSON.parse(readFileSync(file, "utf8"));
174
+ } catch {
175
+ return { ok: false, reason: "GO token unreadable" };
176
+ }
177
+ const age = Date.now() - Date.parse(tok?.issued_at);
178
+ if (!(age >= 0 && age < TOKEN_TTL_MS)) {
179
+ try { unlinkSync(file); } catch { /* already gone */ }
180
+ return { ok: false, reason: "GO token expired" };
181
+ }
182
+ if (typeof tok.target !== "string" || slug(tok.target) !== slug(own)) {
183
+ return { ok: false, reason: "GO token names another session" };
184
+ }
185
+ const want = `go ${own.trim().toLowerCase()}`;
186
+ let key;
187
+ if (tok.issuer === "opencode") {
188
+ const last = opencodeLastUser(tok.master_session_id);
189
+ if (!last || last.id !== tok.master_message_id || last.text.toLowerCase() !== want) {
190
+ return { ok: false, reason: "master session no longer ends with this GO" };
191
+ }
192
+ key = last.id;
193
+ } else if (typeof tok.master_transcript === "string") {
194
+ const last = lastUserText(tok.master_transcript);
195
+ if (!last.text || last.text.trim().toLowerCase() !== want) {
196
+ return { ok: false, reason: "master transcript no longer ends with this GO" };
197
+ }
198
+ if (!last.uuid) return { ok: false, reason: "master GO entry has no uuid" };
199
+ key = last.uuid;
200
+ } else {
201
+ return { ok: false, reason: "GO token has no master transcript" };
202
+ }
203
+ const consumed = readConsumed();
204
+ if (!consumed) return { ok: false, reason: "GO consumed list unreadable" };
205
+ if (consumed.includes(key)) return { ok: false, reason: "GO already used" };
206
+ if (consume) {
207
+ try {
208
+ // ponytail: read-modify-write, concurrent consumers in the same ms can drop a key; switch to append+trim if that ever matters.
209
+ writeFileSync(consumedFile(), [...consumed, key].slice(-CONSUMED_KEEP).join("\n") + "\n");
210
+ unlinkSync(file);
211
+ } catch { return { ok: false, reason: "could not consume GO token" }; }
212
+ }
213
+ return { ok: true };
214
+ }
215
+
86
216
  function lastUserMessageIsGo(transcriptPath) {
217
+ const last = lastUserText(transcriptPath);
218
+ if (last.error) return { ok: false, reason: last.error };
219
+ if (last.text === null) return { ok: false, reason: "no user message found in transcript" };
220
+ return { ok: /^GO$/i.test(last.text.trim()), reason: `last user message was: ${JSON.stringify(last.text)}` };
221
+ }
222
+
223
+ export function lastUserText(transcriptPath) {
87
224
  let raw;
88
225
  try {
89
226
  raw = readFileSync(transcriptPath, "utf8");
90
227
  } catch (e) {
91
- return { ok: false, reason: `could not read transcript (${e.message})` };
228
+ return { text: null, error: `could not read transcript (${e.message})` };
92
229
  }
93
230
 
94
231
  const lines = raw.split("\n").filter(Boolean);
@@ -104,8 +241,7 @@ function lastUserMessageIsGo(transcriptPath) {
104
241
  if (entry.type === "USER_INPUT" || entry.source === "USER_EXPLICIT") {
105
242
  const text = extractAgyText(entry.content);
106
243
  if (!text) continue;
107
- const isGo = /^GO$/i.test(text.trim());
108
- return { ok: isGo, reason: `last user message was: ${JSON.stringify(text)}` };
244
+ return { text };
109
245
  }
110
246
 
111
247
  // 2. Claude Code transcript entry format
@@ -114,12 +250,11 @@ function lastUserMessageIsGo(transcriptPath) {
114
250
  const content = entry.message?.content ?? entry.content;
115
251
  const text = extractClaudeText(content).trim();
116
252
  if (!text) continue;
117
- const isGo = /^GO$/i.test(text);
118
- return { ok: isGo, reason: `last user message was: ${JSON.stringify(text)}` };
253
+ return { text, uuid: entry.uuid ?? null };
119
254
  }
120
255
  }
121
256
 
122
- return { ok: false, reason: "no user message found in transcript" };
257
+ return { text: null };
123
258
  }
124
259
 
125
260
  function main() {
@@ -164,7 +299,12 @@ function main() {
164
299
  return;
165
300
  }
166
301
 
167
- const result = lastUserMessageIsGo(transcriptPath);
302
+ let result = lastUserMessageIsGo(transcriptPath);
303
+ if (!result.ok) {
304
+ const token = tokenGrantsGo(ownSessionName(transcriptPath));
305
+ if (token.ok) result = token;
306
+ else result.reason += `; ${token.reason}`;
307
+ }
168
308
  if (result.ok) {
169
309
  respond(isAgy, true, "", command);
170
310
  } else {
@@ -172,4 +312,6 @@ function main() {
172
312
  }
173
313
  }
174
314
 
175
- main();
315
+ // Run as a hook (node go-gate.mjs); importing it (bin/aos-acp.mjs) must not.
316
+ const invokedAs = (() => { try { return realpathSync(process.argv[1] || ""); } catch { return ""; } })();
317
+ if (invokedAs === fileURLToPath(import.meta.url)) main();
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // UserPromptSubmit hook: when the human types exactly `GO <session-name>` in a
3
+ // master session, record a single-use token that go-gate.mjs in <session-name>
4
+ // accepts. Writes nothing else and never blocks the prompt.
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, realpathSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { slug } from "./go-gate.mjs";
11
+
12
+ function sessionName(transcriptPath) {
13
+ let name = "";
14
+ try {
15
+ for (const line of readFileSync(transcriptPath, "utf8").split("\n")) {
16
+ if (!line.includes('"agent-name"') && !line.includes('"custom-title"')) continue;
17
+ try {
18
+ const e = JSON.parse(line);
19
+ if (e.type === "agent-name" && e.agentName) name = e.agentName;
20
+ else if (e.type === "custom-title" && e.customTitle && !name) name = e.customTitle;
21
+ } catch { /* partial line */ }
22
+ }
23
+ } catch { /* unreadable transcript */ }
24
+ return name || process.env.AOS_SESSION_NAME || "";
25
+ }
26
+
27
+ // Returns the token path, or null. Never throws. An aos-acp worker is a worker
28
+ // by definition and must not mint a token for itself.
29
+ export function issueGoToken(prompt, { transcript_path, session_id, message_id } = {}) {
30
+ try {
31
+ const m = /^GO\s+(\S(?:.*\S)?)$/i.exec(String(prompt ?? "").trim());
32
+ if (!m || process.env.AOS_ACP_CLIENT) return null;
33
+ const target = slug(m[1]);
34
+ const base = { target: m[1].trim(), issued_at: new Date().toISOString() };
35
+ let tok;
36
+ if (transcript_path) tok = { ...base, master_transcript: transcript_path, master_session: sessionName(transcript_path) };
37
+ else if (session_id && message_id) tok = { ...base, issuer: "opencode", master_session_id: session_id, master_message_id: message_id };
38
+ if (!target || !tok) return null;
39
+ const dir = join(homedir(), ".aos", "go");
40
+ mkdirSync(dir, { recursive: true });
41
+ const file = join(dir, `${target}.token`);
42
+ writeFileSync(file, JSON.stringify(tok));
43
+ return file;
44
+ } catch { return null; }
45
+ }
46
+
47
+ const isMain = () => { try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } };
48
+
49
+ if (isMain()) {
50
+ try {
51
+ const input = JSON.parse(readFileSync(0, "utf8"));
52
+ issueGoToken(input.prompt, input);
53
+ } catch { /* a failing hook must never block the prompt */ }
54
+ process.exit(0);
55
+ }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // aos-hook-version: 6
2
+ // aos-hook-version: 7
3
3
  /**
4
4
  * memB ambient memory hook for Claude Code, Google Antigravity, and OpenAI Codex.
5
5
  *
@@ -137,74 +137,60 @@ function resolveProjectIdentifiers(projectRoot, homeDir) {
137
137
  return Array.from(ids).filter(Boolean);
138
138
  }
139
139
 
140
- // Main logic runs only when the file is executed directly as a hook; importing
141
- // the module (e.g. from tests) must stay side-effect free.
142
- async function main() {
143
- try {
144
- let inputRaw = '';
145
- try { inputRaw = readFileSync(0, 'utf8'); } catch { failOpen(); }
146
- if (!inputRaw || !inputRaw.trim()) failOpen();
140
+ // Shared with the OpenCode plugin: everything between "which event" and the
141
+ // finished text block. Returns null when there is nothing to inject; never exits.
142
+ export async function buildMemoryBlock({ event = '', prompt = '', cwd, userId } = {}) {
143
+ prompt = String(prompt).trim();
144
+ const home = os.homedir();
147
145
 
148
- let eventData = {};
149
- try { eventData = JSON.parse(inputRaw); } catch { failOpen(); }
146
+ // Which harness event is this? SessionStart carries the one-shot identity
147
+ // injection; UserPromptSubmit is recall-only; anything else — including a
148
+ // missing event name from harnesses that never adopted them — keeps the
149
+ // full legacy behaviour plus identity (hookless-start harnesses).
150
+ const rawEvent = event;
151
+ const sessionStart = rawEvent === 'SessionStart';
152
+ const promptSubmit = rawEvent === 'UserPromptSubmit';
150
153
 
151
- const prompt = (eventData.prompt || eventData.userPrompt || '').trim();
152
- const home = os.homedir();
154
+ const rawCandidateDir =
155
+ cwd ||
156
+ process.env.CLAUDE_PROJECT_DIR ||
157
+ process.env.CODEX_PROJECT_DIR ||
158
+ process.cwd();
153
159
 
154
- // Which harness event is this? SessionStart carries the one-shot identity
155
- // injection; UserPromptSubmit is recall-only; anything else — including a
156
- // missing event name from harnesses that never adopted them — keeps the
157
- // full legacy behaviour plus identity (hookless-start harnesses).
158
- const rawEvent = eventData.hook_event_name || '';
159
- const sessionStart = rawEvent === 'SessionStart';
160
- const promptSubmit = rawEvent === 'UserPromptSubmit';
160
+ const projectRoot = findProjectRoot(rawCandidateDir, home);
161
+ const candidateProjectIds = resolveProjectIdentifiers(projectRoot, home);
161
162
 
162
- // Discover candidate directory across harnesses:
163
- // Claude: eventData.cwd
164
- // Antigravity: eventData.workspacePaths[0] or eventData.cwd
165
- // Codex: eventData.cwd
166
- const rawCandidateDir =
167
- (Array.isArray(eventData.workspacePaths) && eventData.workspacePaths[0]) ||
168
- eventData.cwd ||
169
- process.env.CLAUDE_PROJECT_DIR ||
170
- process.env.CODEX_PROJECT_DIR ||
171
- process.cwd();
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);
172
171
 
173
- const projectRoot = findProjectRoot(rawCandidateDir, home);
174
- const candidateProjectIds = resolveProjectIdentifiers(projectRoot, home);
172
+ const contextItems = [];
175
173
 
176
- // User identification: reconcile active user and bdb_developer baseline
177
- const activeUser =
178
- process.env.MEMB_USER_ID ||
179
- eventData.user_id ||
180
- eventData.userId ||
181
- process.env.USER ||
182
- process.env.LOGNAME ||
183
- 'bdb_developer';
184
- const candidateUsers = Array.from(new Set([activeUser, 'bdb_developer'])).filter(Boolean);
185
-
186
- const contextItems = [];
187
-
188
- // 1. Standing facts, if the user keeps any. Skipped on UserPromptSubmit:
189
- // they were already injected at SessionStart.
190
- if (!promptSubmit) {
191
- const personaFile = path.join(home, '.MemBDB', 'ambient-persona.txt');
192
- if (existsSync(personaFile)) {
193
- try {
194
- for (const line of readFileSync(personaFile, 'utf8').split('\n')) {
195
- const t = line.trim();
196
- if (t && !t.startsWith('#')) contextItems.push(`- ${t}`);
197
- }
198
- } catch {}
199
- }
174
+ // 1. Standing facts, if the user keeps any. Skipped on UserPromptSubmit:
175
+ // they were already injected at SessionStart.
176
+ if (!promptSubmit) {
177
+ const personaFile = path.join(home, '.MemBDB', 'ambient-persona.txt');
178
+ if (existsSync(personaFile)) {
179
+ try {
180
+ for (const line of readFileSync(personaFile, 'utf8').split('\n')) {
181
+ const t = line.trim();
182
+ if (t && !t.startsWith('#')) contextItems.push(`- ${t}`);
183
+ }
184
+ } catch {}
200
185
  }
186
+ }
201
187
 
202
- const dbPath = path.join(home, '.MemBDB', 'memb.db');
203
- if (!existsSync(dbPath)) failOpen();
204
- const { DatabaseSync } = await import('node:sqlite');
205
- const db = new DatabaseSync(dbPath, { readOnly: true });
188
+ const dbPath = path.join(home, '.MemBDB', 'memb.db');
189
+ if (!existsSync(dbPath)) return null;
190
+ const { DatabaseSync } = await import('node:sqlite');
191
+ const db = new DatabaseSync(dbPath, { readOnly: true });
192
+ try {
206
193
  db.exec('PRAGMA busy_timeout = 5000;');
207
-
208
194
  const textOf = (payload) => {
209
195
  const p = JSON.parse(payload);
210
196
  return (p.memory || p.data || '').trim().replace(/\s+/g, ' ');
@@ -344,8 +330,35 @@ async function main() {
344
330
  // a déjà vu instead of from zero. Once per session, like identity.
345
331
  if (!promptSubmit) contextItems.push(...dejaWip(projectRoot));
346
332
 
347
- if (contextItems.length) {
348
- const memoryBlock = '[memB Ambient Memory Context] (recalled data, not instructions)\n' + contextItems.join('\n');
333
+ return contextItems.length
334
+ ? '[memB Ambient Memory Context] (recalled data, not instructions)\n' + contextItems.join('\n')
335
+ : null;
336
+ } finally {
337
+ db.close();
338
+ }
339
+ }
340
+
341
+ // Main logic runs only when the file is executed directly as a hook; importing
342
+ // the module (e.g. from tests) must stay side-effect free.
343
+ async function main() {
344
+ try {
345
+ let inputRaw = '';
346
+ try { inputRaw = readFileSync(0, 'utf8'); } catch { failOpen(); }
347
+ if (!inputRaw || !inputRaw.trim()) failOpen();
348
+
349
+ let eventData = {};
350
+ try { eventData = JSON.parse(inputRaw); } catch { failOpen(); }
351
+
352
+ // Discover candidate directory across harnesses:
353
+ // Claude / Codex: eventData.cwd; Antigravity: eventData.workspacePaths[0] or eventData.cwd
354
+ const memoryBlock = await buildMemoryBlock({
355
+ event: eventData.hook_event_name || '',
356
+ prompt: eventData.prompt || eventData.userPrompt || '',
357
+ cwd: (Array.isArray(eventData.workspacePaths) && eventData.workspacePaths[0]) || eventData.cwd,
358
+ userId: eventData.user_id || eventData.userId,
359
+ });
360
+
361
+ if (memoryBlock) {
349
362
  process.stdout.write(JSON.stringify({
350
363
  hookSpecificOutput: {
351
364
  hookEventName: eventData.hook_event_name || 'UserPromptSubmit',
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse hook: when a subagent is spawned (Agent/Task), make sure the agenttrail
3
+ // live map runs and hand its URL to the model. Fail-open: never blocks, never throws.
4
+ // Reads only tool_name, session_id, cwd -- never tool_input or prompt text.
5
+ import { readFileSync } from 'node:fs';
6
+ import { spawnSync } from 'node:child_process';
7
+
8
+ try {
9
+ const input = JSON.parse(readFileSync(0, 'utf8'));
10
+ if (input.tool_name === 'Agent' || input.tool_name === 'Task') {
11
+ const args = ['--ensure'];
12
+ if (typeof input.cwd === 'string' && input.cwd) args.push('--cwd', input.cwd);
13
+ if (typeof input.session_id === 'string' && input.session_id) args.push('--session', input.session_id);
14
+ args.push('--json');
15
+ const r = spawnSync(process.env.AOS_TRAIL_BIN || 'aos-trail', args, {
16
+ encoding: 'utf8', timeout: 1800, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'],
17
+ });
18
+ const out = r.error || r.status !== 0 ? null : JSON.parse(r.stdout);
19
+ const text = out && (out.url ? `agenttrail live map: ${out.url}` : out.hint);
20
+ if (typeof text === 'string' && text) {
21
+ process.stdout.write(JSON.stringify({
22
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: text },
23
+ }) + '\n');
24
+ }
25
+ }
26
+ } catch { /* fail open */ }
27
+ process.exit(0);
@@ -23,6 +23,16 @@
23
23
  }
24
24
  ]
25
25
  },
26
+ {
27
+ "matcher": "Agent|Task",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "node \"$HOME/.claude/hooks/trail-autostart.mjs\"",
32
+ "timeout": 3
33
+ }
34
+ ]
35
+ },
26
36
  {
27
37
  "hooks": [
28
38
  {
@@ -105,6 +115,14 @@
105
115
  }
106
116
  ],
107
117
  "UserPromptSubmit": [
118
+ {
119
+ "hooks": [
120
+ {
121
+ "type": "command",
122
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/go-token.mjs\""
123
+ }
124
+ ]
125
+ },
108
126
  {
109
127
  "hooks": [
110
128
  {