@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.
- package/.agents/{agents.md → AGENTS.md} +2 -0
- package/.agents/nodes.json +2 -0
- package/.claude/hooks/memb-inject.mjs +29 -1
- package/.claude/workflows/startcycle-dispatch.mjs +23 -1
- package/.opencode/commands/startcycle-graph.md +57 -0
- package/.opencode/plugins/bdb-aos.js +130 -5
- package/CLAUDE.md +0 -571
- package/THIRD_PARTY_NOTICES.md +38 -0
- package/bin/aos-doctor.mjs +1 -1
- package/installer.js +127 -66
- package/package.json +3 -3
- package/skills/basic/bdb-eventagency-skill/SKILL.md +252 -0
- package/skills/basic/bdb-shipping-skill/SKILL.md +161 -0
- package/skills/basic/godmode-eventtech/SKILL.md +4 -1
- package/skills/global_config/aos-project-init/SKILL.md +2 -0
- package/skills/global_config/aos-project-init/assets/AGENTS.template.md +1 -1
- package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +1 -1
- package/skills/global_config/aos-setup/SKILL.md +1 -1
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
- package/skills/global_config/ask-tim/SKILL.md +3 -3
- package/skills/global_config/deja-memory/SKILL.md +3 -1
- package/skills/global_config/plan-canvas/SKILL.md +9 -2
- package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +10 -2
- package/skills/global_config/plan-canvas/scripts/plan-canvas.js +1 -1
- package/skills/global_config/read-the-damn-docs/SKILL.md +175 -0
- package/skills/global_config/writing-plans/SKILL.md +12 -12
- package/skills/global_config/writing-plans-legacy/SKILL.md +15 -2
- package/.claude/CLAUDE.md +0 -12
- package/mcps/RhinoMCP/docs/content/docs/getting-started/gemini.md +0 -61
- /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`
|
package/.agents/nodes.json
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
const goal = (
|
|
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
|
|
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)`,
|