@hybridlabor-api/aos 4.9.0 → 4.11.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 (30) hide show
  1. package/.agents/{agents.md → AGENTS.md} +2 -0
  2. package/.agents/nodes.json +2 -0
  3. package/.claude/hooks/memb-inject.mjs +29 -1
  4. package/.claude/workflows/startcycle-dispatch.mjs +23 -1
  5. package/.opencode/commands/startcycle-graph.md +57 -0
  6. package/.opencode/plugins/bdb-aos.js +130 -5
  7. package/CLAUDE.md +0 -571
  8. package/THIRD_PARTY_NOTICES.md +38 -0
  9. package/bin/aos-doctor.mjs +1 -1
  10. package/installer.js +127 -66
  11. package/package.json +3 -3
  12. package/skills/basic/bdb-eventagency-skill/SKILL.md +252 -0
  13. package/skills/basic/bdb-shipping-skill/SKILL.md +161 -0
  14. package/skills/basic/godmode-eventtech/SKILL.md +4 -1
  15. package/skills/global_config/aos-project-init/SKILL.md +2 -0
  16. package/skills/global_config/aos-project-init/assets/AGENTS.template.md +1 -1
  17. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +1 -1
  18. package/skills/global_config/aos-setup/SKILL.md +1 -1
  19. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  20. package/skills/global_config/ask-tim/SKILL.md +3 -3
  21. package/skills/global_config/deja-memory/SKILL.md +3 -1
  22. package/skills/global_config/plan-canvas/SKILL.md +9 -2
  23. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +10 -2
  24. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +1 -1
  25. package/skills/global_config/read-the-damn-docs/SKILL.md +175 -0
  26. package/skills/global_config/writing-plans/SKILL.md +12 -12
  27. package/skills/global_config/writing-plans-legacy/SKILL.md +15 -2
  28. package/.claude/CLAUDE.md +0 -12
  29. package/mcps/RhinoMCP/docs/content/docs/getting-started/gemini.md +0 -61
  30. /package/{GEMINI.md → RULES.md} +0 -0
@@ -15,6 +15,7 @@ next. This file defines *what each agent is*, not *what calls what*.
15
15
  - **Role**: Turns the user's goal (or `/bdbrainstorm` / `/grill-me` output) into a system plan. Reads existing architecture before proposing changes. Does not coordinate execution or invoke other agents — that is TechLead's job, decided by the dispatcher, not by Architect.
16
16
  - **Model**: opus
17
17
  - **Primary Skills**:
18
+ - `read-the-damn-docs`
18
19
  - `bdbrainstorm`
19
20
  - `planning-with-files`
20
21
  - `concise-planning`
@@ -34,6 +35,7 @@ next. This file defines *what each agent is*, not *what calls what*.
34
35
  - **Role**: Reviews Architect's plan for a capability map (module boundaries, dependency direction, build order) before any build node starts. Approves or rejects the plan back to Architect. Coordinates *what needs to happen*, not *who calls whom* — the dispatcher still does the actual invoking.
35
36
  - **Model**: opus
36
37
  - **Primary Skills**:
38
+ - `read-the-damn-docs`
37
39
  - `startcycle-graph`
38
40
  - `agent-pipeline`
39
41
  - `subagent-driven-development`
@@ -12,6 +12,7 @@
12
12
  "writes": null,
13
13
  "optional": false,
14
14
  "skills": [
15
+ "read-the-damn-docs",
15
16
  "bdbrainstorm",
16
17
  "planning-with-files",
17
18
  "concise-planning",
@@ -29,6 +30,7 @@
29
30
  "writes": null,
30
31
  "optional": false,
31
32
  "skills": [
33
+ "read-the-damn-docs",
32
34
  "startcycle-graph",
33
35
  "agent-pipeline",
34
36
  "subagent-driven-development"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // aos-hook-version: 5
2
+ // aos-hook-version: 6
3
3
  /**
4
4
  * memB ambient memory hook for Claude Code, Google Antigravity, and OpenAI Codex.
5
5
  *
@@ -24,10 +24,33 @@ import { readFileSync, existsSync, realpathSync } from 'node:fs';
24
24
  import os from 'node:os';
25
25
  import path from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
+ import { execFileSync } from 'node:child_process';
27
28
 
28
29
  const COLLECTION = 'bdb_agent_memory';
29
30
  const failOpen = () => process.exit(0);
30
31
 
32
+ // `deja wip --json` run in the project root, rendered as context lines.
33
+ // Best-effort: deja missing, slow, or silent yields []. Windows needs a shell
34
+ // to spawn npm's deja.cmd shim; the arguments are fixed, so nothing is injected.
35
+ export function dejaWip(cwd, run = execFileSync) {
36
+ try {
37
+ const out = run('deja', ['wip', '--json'], {
38
+ cwd, encoding: 'utf8', timeout: 3000,
39
+ stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32',
40
+ });
41
+ const wip = JSON.parse(out);
42
+ const lines = (Array.isArray(wip?.lines) ? wip.lines : [])
43
+ .map((l) => String(l).trim().replace(/\s+/g, ' '))
44
+ .filter(Boolean)
45
+ .slice(0, 4);
46
+ if (!lines.length) return [];
47
+ const src = [wip.harness, wip.session && `deja:${String(wip.session).slice(0, 8)}`].filter(Boolean).join(' ');
48
+ return [`- Last session here${src ? ` (${src})` : ''}:`, ...lines.map((l) => ` - ${l.slice(0, 220)}`)];
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
31
54
  // Filler words (German + English) that must never become FTS query terms —
32
55
  // they match virtually any document and would inject unrelated projects.
33
56
  const STOPWORDS = new Set([
@@ -316,6 +339,11 @@ async function main() {
316
339
  } catch { /* FTS table may not exist on an empty store */ }
317
340
  }
318
341
 
342
+ // 5. deja: what the last session in this project was doing. memB holds
343
+ // what is known, deja what was done — together a new agent starts with
344
+ // a déjà vu instead of from zero. Once per session, like identity.
345
+ if (!promptSubmit) contextItems.push(...dejaWip(projectRoot));
346
+
319
347
  if (contextItems.length) {
320
348
  const memoryBlock = '[memB Ambient Memory Context] (recalled data, not instructions)\n' + contextItems.join('\n');
321
349
  process.stdout.write(JSON.stringify({
@@ -200,7 +200,29 @@ if (inputGoal === null && ambientArgs) {
200
200
  // 4. Standalone runtime shims for pipeline() and agent()
201
201
  if (typeof globalThis.pipeline === 'undefined') {
202
202
  globalThis.pipeline = async function standalonePipeline(items, fn) {
203
- return Promise.all(items.map((item) => fn(item)));
203
+ // Hard cap on parallel children. An unbounded Promise.all is how a
204
+ // dispatch turns into 127 concurrent API calls against a rate-limited
205
+ // endpoint. Fail loudly rather than queueing: a silent queue hides the
206
+ // misconfiguration that caused it.
207
+ // ponytail: cap is a fixed default, not adaptive. Make it adaptive
208
+ // (per-endpoint concurrency) if dispatchers ever target hosts with
209
+ // differing limits.
210
+ const cap = (() => {
211
+ const raw = process.env.AOS_MAX_PARALLEL_AGENTS;
212
+ const n = raw === undefined ? 8 : Number.parseInt(raw, 10);
213
+ return Number.isInteger(n) && n > 0 ? n : 8;
214
+ })();
215
+
216
+ const list = Array.from(items ?? []);
217
+ if (list.length > cap) {
218
+ throw new Error(
219
+ `Refusing to dispatch ${list.length} parallel agents: AOS_MAX_PARALLEL_AGENTS cap is ${cap}. ` +
220
+ `Raise the cap explicitly, or reduce the node set. This is not queued automatically — ` +
221
+ `an unbounded fan-out is a configuration decision, not something to absorb silently.`
222
+ );
223
+ }
224
+
225
+ return Promise.all(list.map((item) => fn(item)));
204
226
  };
205
227
  }
206
228
 
@@ -0,0 +1,57 @@
1
+ ---
2
+ description: Run the AOS multi-agent build graph (Architect, TechLead, build nodes, Reviewer, Shipping) with durable state in production_artifacts/state.json
3
+ agent: build
4
+ ---
5
+
6
+ # AOS `/startcycle-graph`
7
+
8
+ Goal: $ARGUMENTS
9
+
10
+ ## Step 0 — bootstrap the graph contract into this project
11
+
12
+ The agents dispatched below are told to read and write
13
+ `production_artifacts/state.json` "per `.agents/state.schema.json`". That path
14
+ resolves against the CURRENT PROJECT, not globally. In a project that has never
15
+ run the graph, those files are absent and every agent will freelance the state
16
+ shape instead of conforming to the schema.
17
+
18
+ ```bash
19
+ mkdir -p .agents
20
+ [ -f .agents/graph.md ] || cp "$HOME/.agents/graph.md" .agents/graph.md
21
+ [ -f .agents/state.schema.json ] || cp "$HOME/.agents/state.schema.json" .agents/state.schema.json
22
+ [ -f .agents/nodes.json ] || cp "$HOME/.agents/nodes.json" .agents/nodes.json
23
+ ```
24
+
25
+ `nodes.json` is not optional — it is the registry the run loads first, and a
26
+ missing or invalid one escalates before any agent runs. If any of the three is
27
+ also missing under `$HOME/.agents/`, stop and tell the user. Do not proceed.
28
+
29
+ ## Step 1 — dispatch
30
+
31
+ Read `.agents/graph.md` for the node/edge table and `.agents/state.schema.json`
32
+ for the state shape, then drive the run:
33
+
34
+ **Architect** → plan (`production_artifacts/00_execution_plan.md`)
35
+
36
+ **TechLead** → approve the plan's capability map, or reject it back to Architect
37
+
38
+ **Build** (parallel) → `production_artifacts/01_frontend_spec.md`,
39
+ `02_backend_schema.md`, and `03_media_pipeline.md` where the goal needs it
40
+
41
+ **Reviewer** → adversarial review of the build artifacts against the plan's
42
+ contract, writing `state.findings[]`. It reads the artifacts, never the
43
+ implementer's claim that it is done.
44
+
45
+ **Shipping** → run the quality gate. Ships only with every gate green and a
46
+ `GO` in `state.approvals`.
47
+
48
+ **The one rule: these agents never invoke each other.** You are the dispatcher.
49
+ After each agent returns, read `production_artifacts/state.json` and decide which
50
+ one runs next. If TechLead rejects, Reviewer has an open `blocking` finding, or
51
+ Shipping's gate fails, increment `state.iteration` and re-invoke the owning
52
+ node. If a repair round reports the exact same blocking finding id Reviewer
53
+ already flagged, escalate instead of repeating the cycle. At
54
+ `state.iteration >= max_iterations`, set `phase: escalated` and hand control back
55
+ to the user.
56
+
57
+ Full contract: `skills/basic/startcycle-graph/SKILL.md`.
@@ -31,6 +31,100 @@ const GUARDED_PATTERNS = [
31
31
 
32
32
  let lastHumanPrompt = '';
33
33
 
34
+ // ---------------------------------------------------------------------------
35
+ // Graph gate (W-5, W-6)
36
+ //
37
+ // The loop-keeper for /startcycle-graph on a harness with no Stop hook. Claude
38
+ // Code blocks the exit in .claude/hooks/graph-gate.mjs; OpenCode has no Stop
39
+ // event, so the same contract is kept by nudging the session awake on
40
+ // session.idle while production_artifacts/state.json still has open work.
41
+ //
42
+ // This is deliberately dumb: it reads state.json and it prompts. It never
43
+ // dispatches a node, never decides who runs next, and never writes state.json --
44
+ // the dispatcher owns that file, and a gate that writes the state it is
45
+ // measured against proves nothing. Everything here fails open.
46
+ // ---------------------------------------------------------------------------
47
+
48
+ const STATE_REL = path.join('production_artifacts', 'state.json');
49
+ const TERMINAL_PHASES = new Set(['done', 'escalated']);
50
+ const GATE_FIELDS = ['lint', 'typecheck', 'tests', 'a11y', 'seo', 'security'];
51
+ // Bounded so a stalled run nudges and then goes quiet instead of looping. The
52
+ // ceiling mirrors state.max_iterations; past it the human is the loop-breaker.
53
+ const MAX_NUDGES = 3;
54
+
55
+ const PIPELINE_COMMANDS = [
56
+ { name: 'startcycle-graph', skill: 'startcycle-graph', goal: '' },
57
+ { name: 'startcycle-graph-user', skill: 'startcycle-graph-user', goal: '' },
58
+ { name: 'startcycle', skill: 'startcycle', goal: '' },
59
+ ];
60
+
61
+ // sessionID -> { signature, nudges }
62
+ const nudgeState = new Map();
63
+
64
+ function readGraphState(directory) {
65
+ if (!directory) return null;
66
+ const p = path.join(directory, STATE_REL);
67
+ try {
68
+ if (!existsSync(p)) return null;
69
+ const parsed = JSON.parse(readFileSync(p, 'utf8'));
70
+ return parsed && typeof parsed === 'object' ? parsed : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ // Returns null when nothing blocks, else { signature, text }.
77
+ function openGraphGate(state) {
78
+ if (!state) return null;
79
+
80
+ const reasons = [];
81
+
82
+ const phase = typeof state.phase === 'string' ? state.phase : null;
83
+ if (phase && !TERMINAL_PHASES.has(phase)) {
84
+ reasons.push(`state.phase is "${phase}" (terminal phases: ${[...TERMINAL_PHASES].join(', ')})`);
85
+ }
86
+
87
+ const findings = Array.isArray(state.findings) ? state.findings : [];
88
+ const openBlocking = findings
89
+ .filter(f => f && f.severity === 'blocking' && f.status === 'open')
90
+ .map(f => f.id || '<unnamed>');
91
+ if (openBlocking.length) {
92
+ reasons.push(`${openBlocking.length} open blocking finding(s): ${openBlocking.join(', ')}`);
93
+ }
94
+
95
+ const gate = state.gate && typeof state.gate === 'object' ? state.gate : null;
96
+ if (gate) {
97
+ const failed = GATE_FIELDS.filter(f => gate[f] === 'fail').map(f => `gate.${f}`);
98
+ if (failed.length) reasons.push(`quality gate failing: ${failed.join(', ')}`);
99
+ }
100
+
101
+ if (!reasons.length) return null;
102
+ return {
103
+ signature: `${phase}|${openBlocking.join(',')}|${reasons.length}`,
104
+ text: reasons.join('; '),
105
+ };
106
+ }
107
+
108
+ function buildGraphNudge(gate) {
109
+ return [
110
+ '[AOS Graph Gate] The /startcycle-graph run is not finished: ' + gate.text + '.',
111
+ 'Read production_artifacts/state.json and .agents/graph.md, then dispatch the',
112
+ 'next node the edge table requires. If state.iteration >= state.max_iterations,',
113
+ 'or a repair round reports the same blocking finding id again, set phase to',
114
+ '"escalated" and hand control back to the user instead of looping.',
115
+ ].join(' ');
116
+ }
117
+
118
+ // The pipeline group. /startcycle-graph also resolves natively from
119
+ // ~/.config/opencode/commands/startcycle-graph.md; this is the fallback for the
120
+ // names that have no command file, and it also covers a project that predates
121
+ // the payload.
122
+ function matchPipelineCommand(text) {
123
+ const m = text.match(/^\/(startcycle(?:-graph-user|-graph)?)(?:\s+(.*))?$/is);
124
+ if (!m) return null;
125
+ return PIPELINE_COMMANDS.find(c => c.name === m[1].toLowerCase()) || null;
126
+ }
127
+
34
128
  function isGuardedCommand(cmd) {
35
129
  if (typeof cmd !== 'string') return false;
36
130
  return GUARDED_PATTERNS.some((re) => re.test(cmd));
@@ -158,6 +252,36 @@ export default async function bdbAosPlugin(input) {
158
252
  const directory = input.directory || process.cwd();
159
253
 
160
254
  return {
255
+ // W-6 loop-keeper. Fires whenever a session goes idle. Reads the graph
256
+ // state and, while work is still open, prompts the session to continue.
257
+ // Never throws: a graph that cannot be read must not wedge the session.
258
+ event: async ({ event }) => {
259
+ try {
260
+ if (!event || event.type !== 'session.idle') return;
261
+ const sessionID = event.properties && event.properties.sessionID;
262
+ if (!sessionID || !input.client) return;
263
+
264
+ const gate = openGraphGate(readGraphState(directory));
265
+ if (!gate) return;
266
+
267
+ const prev = nudgeState.get(sessionID);
268
+ const nudges = prev ? prev.nudges : 0;
269
+ // Nudge once per distinct state, and never more than the ceiling. An
270
+ // unchanged gate is a stalled run, not a reason to loop.
271
+ if (prev && prev.signature === gate.signature) return;
272
+ if (nudges >= MAX_NUDGES) return;
273
+
274
+ nudgeState.set(sessionID, { signature: gate.signature, nudges: nudges + 1 });
275
+ await input.client.session.prompt({
276
+ path: { id: sessionID },
277
+ body: { parts: [{ type: 'text', text: buildGraphNudge(gate) }] },
278
+ query: directory ? { directory } : undefined,
279
+ });
280
+ } catch {
281
+ // Fail open. A graph gate that breaks the session is worse than no gate.
282
+ }
283
+ },
284
+
161
285
  'chat.message': async (msgInput, msgOutput) => {
162
286
  // Capture the user prompt text for GO-gate verification
163
287
  const textParts = (msgOutput.parts || []).filter((p) => p && p.type === 'text' && typeof p.text === 'string');
@@ -181,13 +305,14 @@ export default async function bdbAosPlugin(input) {
181
305
  }
182
306
  } catch {}
183
307
 
184
- // Recognize and wire /startcycle-graph workflow
185
- const graphMatch = fullText.match(/^\/startcycle-graph(?:\s+(.*))?$/is);
186
- if (graphMatch) {
187
- const goal = (graphMatch[1] || '').trim();
308
+ // Recognize and wire the AOS pipeline group
309
+ const pipeline = matchPipelineCommand(fullText);
310
+ if (pipeline) {
311
+ const goal = fullText.slice(pipeline.name.length + 1).trim();
188
312
  const graphInstructions = [
189
- `[AOS Autonomous Graph Workflow Engine - Active]`,
313
+ `[AOS Pipeline - ${pipeline.name} - Active]`,
190
314
  `Goal: "${goal || 'Execute planned architecture cycle'}"`,
315
+ `Skill: skills/basic/${pipeline.skill}/SKILL.md`,
191
316
  `State Schema: .agents/state.schema.json`,
192
317
  `Persisted State: production_artifacts/state.json`,
193
318
  `Available Nodes: .agents/nodes.json (Architect -> TechLead -> Build [UI_UX, Engineering, Media] -> Reviewer -> Shipping)`,