@hybridlabor-api/aos 4.10.0 → 4.12.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/.opencode/commands/startcycle-graph.md +57 -0
- package/.opencode/plugins/bdb-aos.js +130 -5
- package/bin/aos-store.mjs +29 -59
- package/installer.js +153 -59
- package/lib/ecc-store-index.json +1 -1
- package/lib/store-shared.mjs +133 -0
- package/lib/store-ui/index.html +150 -0
- package/lib/store-ui/server.mjs +180 -0
- package/package.json +2 -2
- package/scripts/build-ecc-store-index.mjs +17 -1
- package/skills/bdb-aos/SKILL.md +46 -0
- package/skills/global_config/aos-store/SKILL.md +71 -0
|
@@ -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)`,
|
package/bin/aos-store.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { fetchSkillFiles, fileTargets, localAgents, localSkills, targetsFor as sharedTargetsFor, validateFiles, writeSkillFiles } from '../lib/store-shared.mjs';
|
|
8
8
|
|
|
9
9
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
-
const INDEX_PATH = join(ROOT, 'lib', 'ecc-store-index.json');
|
|
10
|
+
const INDEX_PATH = process.env.AOS_STORE_INDEX || join(ROOT, 'lib', 'ecc-store-index.json');
|
|
11
|
+
const RAW_BASE = (process.env.AOS_STORE_RAW_BASE || 'https://raw.githubusercontent.com/affaan-m/ECC').replace(/\/+$/, '');
|
|
11
12
|
const index = JSON.parse(readFileSync(INDEX_PATH, 'utf8'));
|
|
12
13
|
const args = process.argv.slice(2);
|
|
13
14
|
const dryRun = args.includes('--dry-run');
|
|
@@ -33,49 +34,8 @@ function findItem(name) {
|
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const visit = (dir) => {
|
|
39
|
-
if (!existsSync(dir)) return;
|
|
40
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
41
|
-
const fullPath = join(dir, entry.name);
|
|
42
|
-
if (entry.isDirectory()) visit(fullPath);
|
|
43
|
-
else if (entry.name === 'SKILL.md') names.add(basename(dirname(fullPath)));
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
visit(join(ROOT, 'skills'));
|
|
47
|
-
return names;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function localAgentNames() {
|
|
51
|
-
const names = new Set();
|
|
52
|
-
for (const dir of [join(ROOT, '.claude', 'agents'), join(ROOT, '.agents', 'agents'), join(ROOT, 'agents')]) {
|
|
53
|
-
if (!existsSync(dir)) continue;
|
|
54
|
-
for (const entry of readdirSync(dir)) names.add(entry.replace(/\.md$/i, ''));
|
|
55
|
-
}
|
|
56
|
-
return names;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function globalRoots(type) {
|
|
60
|
-
const home = homedir();
|
|
61
|
-
return type === 'skills'
|
|
62
|
-
? [
|
|
63
|
-
join(home, '.agents', 'skills'),
|
|
64
|
-
join(home, '.claude', 'skills'),
|
|
65
|
-
join(home, '.codex', 'skills'),
|
|
66
|
-
join(home, '.cursor', 'skills'),
|
|
67
|
-
join(home, '.roo', 'skills'),
|
|
68
|
-
join(home, '.gemini', 'config', 'skills'),
|
|
69
|
-
]
|
|
70
|
-
: [
|
|
71
|
-
join(home, '.agents', 'agents'),
|
|
72
|
-
join(home, '.claude', 'agents'),
|
|
73
|
-
join(home, '.codex', 'agents'),
|
|
74
|
-
join(home, '.cursor', 'agents'),
|
|
75
|
-
join(home, '.roo', 'agents'),
|
|
76
|
-
join(home, '.gemini', 'config', 'agents'),
|
|
77
|
-
];
|
|
78
|
-
}
|
|
37
|
+
const localSkillNames = () => new Set(localSkills(ROOT).keys());
|
|
38
|
+
const localAgentNames = () => new Set(localAgents(ROOT).keys());
|
|
79
39
|
|
|
80
40
|
function printItems(items) {
|
|
81
41
|
if (items.length === 0) {
|
|
@@ -90,22 +50,13 @@ function printItems(items) {
|
|
|
90
50
|
}
|
|
91
51
|
|
|
92
52
|
async function download(upstreamPath) {
|
|
93
|
-
const url =
|
|
53
|
+
const url = `${RAW_BASE}/${index.pinned_commit}/${upstreamPath}`;
|
|
94
54
|
const response = await fetch(url, { headers: { 'user-agent': 'aos-store' } });
|
|
95
55
|
if (!response.ok) throw new Error(`Download failed (${response.status}): ${url}`);
|
|
96
56
|
return Buffer.from(await response.arrayBuffer());
|
|
97
57
|
}
|
|
98
58
|
|
|
99
|
-
|
|
100
|
-
if (project) {
|
|
101
|
-
return type === 'skills'
|
|
102
|
-
? [join(process.cwd(), 'skills', name, 'SKILL.md')]
|
|
103
|
-
: [join(process.cwd(), 'agents', `${name}.md`)];
|
|
104
|
-
}
|
|
105
|
-
return globalRoots(type).map((root) => type === 'skills'
|
|
106
|
-
? join(root, name, 'SKILL.md')
|
|
107
|
-
: join(root, `${name}.md`));
|
|
108
|
-
}
|
|
59
|
+
const targetsFor = (type, name) => sharedTargetsFor(type, name, { project });
|
|
109
60
|
|
|
110
61
|
async function install(name) {
|
|
111
62
|
if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error(`Invalid store name: ${name}`);
|
|
@@ -118,13 +69,26 @@ async function install(name) {
|
|
|
118
69
|
return;
|
|
119
70
|
}
|
|
120
71
|
const targets = targetsFor(type, name);
|
|
72
|
+
const multi = type === 'skills' && item.files?.length > 1;
|
|
73
|
+
if (multi) validateFiles(item.files);
|
|
121
74
|
if (dryRun) {
|
|
122
|
-
for (const target of targets)
|
|
75
|
+
for (const target of targets) {
|
|
76
|
+
if (multi) for (const file of fileTargets(target, item.files)) console.log(`[dry-run] write ${file}`);
|
|
77
|
+
else console.log(`[dry-run] write ${target}`);
|
|
78
|
+
}
|
|
123
79
|
return;
|
|
124
80
|
}
|
|
125
81
|
if (!net) {
|
|
126
82
|
throw new Error(`Downloading '${name}' requires internet access to fetch from upstream GitHub. Pass --net to confirm: aos store install ${name} --net`);
|
|
127
83
|
}
|
|
84
|
+
if (multi) {
|
|
85
|
+
const files = await fetchSkillFiles(RAW_BASE, index.pinned_commit, dirname(item.upstream_path), item.files);
|
|
86
|
+
for (const target of targets) {
|
|
87
|
+
const written = writeSkillFiles(target, files);
|
|
88
|
+
console.log(`Installed ${name} (${written.length} files) -> ${dirname(target)}`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
128
92
|
const content = await download(item.upstream_path);
|
|
129
93
|
const digest = createHash('sha256').update(content).digest('hex');
|
|
130
94
|
if (digest !== item.sha256) throw new Error(`SHA-256 mismatch for ${name}: expected ${item.sha256}, got ${digest}`);
|
|
@@ -140,6 +104,7 @@ function usage() {
|
|
|
140
104
|
console.log(' aos store list [--type=skills|agents]');
|
|
141
105
|
console.log(' aos store search <query>');
|
|
142
106
|
console.log(' aos store install <name> [--project] [--dry-run] [--net]');
|
|
107
|
+
console.log(' aos store ui [--port=N] [--no-open]');
|
|
143
108
|
}
|
|
144
109
|
|
|
145
110
|
async function main() {
|
|
@@ -156,6 +121,11 @@ async function main() {
|
|
|
156
121
|
printItems(items);
|
|
157
122
|
return;
|
|
158
123
|
}
|
|
124
|
+
if (command === 'ui') {
|
|
125
|
+
const { startUi } = await import('../lib/store-ui/server.mjs');
|
|
126
|
+
await startUi({ argv: args });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
159
129
|
if (command === 'install') {
|
|
160
130
|
if (!positional[1]) throw new Error('Install requires a store item name.');
|
|
161
131
|
await install(positional[1]);
|
package/installer.js
CHANGED
|
@@ -3674,6 +3674,106 @@ function injectHarnessRules() {
|
|
|
3674
3674
|
}
|
|
3675
3675
|
}
|
|
3676
3676
|
|
|
3677
|
+
// Copy the OpenCode plugin + command payload and register both in
|
|
3678
|
+
// opencode.jsonc. This is the single copy site: the Quick Update path
|
|
3679
|
+
// (injectHarnessRules -> installGlobalHooks) and the fresh-install path
|
|
3680
|
+
// (universalHarnessSync -> syncOpencodeConfig) both reach it, so a version that
|
|
3681
|
+
// adds or changes the plugin can no longer ship to one path and miss the other.
|
|
3682
|
+
//
|
|
3683
|
+
// `data` is mutated in place when the caller owns the config write (Universal
|
|
3684
|
+
// Sync merges MCP servers in the same pass and writes once). When `data` is
|
|
3685
|
+
// omitted the function loads and saves the config itself, which is what lets the
|
|
3686
|
+
// Quick Update path register the plugin as well as copy it.
|
|
3687
|
+
function installOpencodePlugin({ targetHome = homeDir, configPath = null, data = null } = {}) {
|
|
3688
|
+
const opencodeDir = configPath
|
|
3689
|
+
? path.dirname(configPath)
|
|
3690
|
+
: (process.platform === 'win32'
|
|
3691
|
+
? path.join(process.env.APPDATA || targetHome, 'opencode')
|
|
3692
|
+
: path.join(targetHome, '.config', 'opencode'));
|
|
3693
|
+
|
|
3694
|
+
const pluginSrc = path.join(srcDir, '.opencode', 'plugins', 'bdb-aos.js');
|
|
3695
|
+
let pluginInstalled = false;
|
|
3696
|
+
if (fs.existsSync(pluginSrc)) {
|
|
3697
|
+
const pluginDest = path.join(opencodeDir, 'plugins', 'bdb-aos.js');
|
|
3698
|
+
try {
|
|
3699
|
+
fs.mkdirSync(path.dirname(pluginDest), { recursive: true });
|
|
3700
|
+
fs.copyFileSync(pluginSrc, pluginDest);
|
|
3701
|
+
try { fs.chmodSync(pluginDest, 0o644); } catch (e) { logDebug(e, 'chmod opencode plugin'); }
|
|
3702
|
+
pluginInstalled = true;
|
|
3703
|
+
log.step(`Installed OpenCode plugin to ${pluginDest}`);
|
|
3704
|
+
} catch (e) {
|
|
3705
|
+
log.warn(`Could not install OpenCode plugin: ${e.message}`);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
// Slash-command payloads. Without these the /startcycle-graph command has no
|
|
3710
|
+
// native resolution and only survives as a raw-text match in the plugin.
|
|
3711
|
+
const commandsSrc = path.join(srcDir, '.opencode', 'commands');
|
|
3712
|
+
if (fs.existsSync(commandsSrc)) {
|
|
3713
|
+
try {
|
|
3714
|
+
copyDirRecursiveSync(commandsSrc, path.join(opencodeDir, 'commands'));
|
|
3715
|
+
log.step(`Installed OpenCode commands to ${path.join(opencodeDir, 'commands')}`);
|
|
3716
|
+
} catch (e) {
|
|
3717
|
+
log.warn(`Could not install OpenCode commands: ${e.message}`);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
if (!pluginInstalled) return data;
|
|
3722
|
+
|
|
3723
|
+
const ownsWrite = data === null;
|
|
3724
|
+
let beforeSerialized = null;
|
|
3725
|
+
if (ownsWrite) {
|
|
3726
|
+
if (!configPath) return data;
|
|
3727
|
+
const existing = readJsoncFile(configPath) || {};
|
|
3728
|
+
// Snapshot before mutating: `data` becomes the same object, so
|
|
3729
|
+
// comparing afterwards would always report "unchanged".
|
|
3730
|
+
beforeSerialized = JSON.stringify(existing, null, 2);
|
|
3731
|
+
data = existing;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
// Register the plugin.
|
|
3735
|
+
if (!Array.isArray(data.plugin)) data.plugin = [];
|
|
3736
|
+
const pluginPathNormalized = path.join(opencodeDir, 'plugins', 'bdb-aos.js').replace(/\\/g, '/');
|
|
3737
|
+
const alreadyRegistered = data.plugin.some(p => {
|
|
3738
|
+
const str = typeof p === 'string' ? p : (Array.isArray(p) ? p[0] : '');
|
|
3739
|
+
return str.includes('bdb-aos');
|
|
3740
|
+
});
|
|
3741
|
+
if (!alreadyRegistered) data.plugin.push(pluginPathNormalized);
|
|
3742
|
+
|
|
3743
|
+
// Skill paths. `.agents/skills` stays project-relative on purpose: it is the
|
|
3744
|
+
// per-project contract, and OpenCode resolves it against each project, so it
|
|
3745
|
+
// is meaningful in a bootstrapped project and inert elsewhere. The absolute
|
|
3746
|
+
// ~/.agents/skills is added only when it is really there -- registering a
|
|
3747
|
+
// path that does not exist is the defect this replaces.
|
|
3748
|
+
const homeAgentsSkills = path.join(targetHome, '.agents', 'skills').replace(/\\/g, '/');
|
|
3749
|
+
const desired = ['.agents/skills'];
|
|
3750
|
+
if (fs.existsSync(homeAgentsSkills)) desired.unshift(homeAgentsSkills);
|
|
3751
|
+
if (!data.skills || typeof data.skills !== 'object' || !Array.isArray(data.skills.paths)) {
|
|
3752
|
+
data.skills = { paths: desired.slice() };
|
|
3753
|
+
} else {
|
|
3754
|
+
for (const p of desired) {
|
|
3755
|
+
if (!data.skills.paths.includes(p)) data.skills.paths.push(p);
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
if (ownsWrite) {
|
|
3760
|
+
// Write only on a real change. readJsoncFile strips comments, so
|
|
3761
|
+
// serialising an unchanged config would silently eat a user's comments
|
|
3762
|
+
// for no benefit. When a write is genuinely needed the comments in the
|
|
3763
|
+
// file are lost -- acceptable, and worth saying out loud in the PR.
|
|
3764
|
+
const serialized = JSON.stringify(data, null, 2);
|
|
3765
|
+
if (beforeSerialized === serialized) return data;
|
|
3766
|
+
try {
|
|
3767
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
3768
|
+
fs.writeFileSync(configPath, serialized, { mode: 0o600 });
|
|
3769
|
+
try { fs.chmodSync(configPath, 0o600); } catch (e) { logDebug(e, 'chmod opencode config'); }
|
|
3770
|
+
} catch (e) {
|
|
3771
|
+
log.warn(`Could not write ${configPath}: ${e.message}`);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return data;
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3677
3777
|
// Deliver the hook scripts to ~/.claude/hooks/ and wire them in settings.json.
|
|
3678
3778
|
// Both the full install and Quick Update funnel through here. Quick Update used
|
|
3679
3779
|
// to do neither: it refreshes skills and submodules, but hooks are harness
|
|
@@ -3733,22 +3833,15 @@ function installGlobalHooks({ targetHome = homeDir, targetGemini = geminiDir } =
|
|
|
3733
3833
|
mergeCodexTomlHooks(path.join(targetHome, '.codex', 'config.toml'));
|
|
3734
3834
|
|
|
3735
3835
|
// 4. OpenCode CLI
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
: path.join(
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
fs.copyFileSync(opencodePluginSrc, opencodePluginDest);
|
|
3746
|
-
try { fs.chmodSync(opencodePluginDest, 0o644); } catch (e) { logDebug(e, 'chmod opencode plugin'); }
|
|
3747
|
-
log.step(`Installed OpenCode plugin to ${opencodePluginDest}`);
|
|
3748
|
-
} catch (e) {
|
|
3749
|
-
log.warn(`Could not install OpenCode plugin: ${e.message}`);
|
|
3750
|
-
}
|
|
3751
|
-
}
|
|
3836
|
+
installOpencodePlugin({
|
|
3837
|
+
targetHome,
|
|
3838
|
+
configPath: path.join(
|
|
3839
|
+
process.platform === 'win32'
|
|
3840
|
+
? path.join(process.env.APPDATA || targetHome, 'opencode')
|
|
3841
|
+
: path.join(targetHome, '.config', 'opencode'),
|
|
3842
|
+
'opencode.jsonc'
|
|
3843
|
+
),
|
|
3844
|
+
});
|
|
3752
3845
|
|
|
3753
3846
|
// 5. Global CLI launcher binaries (aos-config, aos-dashboard, aos-uninstall)
|
|
3754
3847
|
installGlobalBinaries();
|
|
@@ -4427,6 +4520,16 @@ function generateAndOpenLaunchpad(installedModules = []) {
|
|
|
4427
4520
|
background-color: var(--green);
|
|
4428
4521
|
box-shadow: 0 0 10px rgba(63, 185, 80, 0.6);
|
|
4429
4522
|
}
|
|
4523
|
+
.card-hint {
|
|
4524
|
+
display: none;
|
|
4525
|
+
font-size: 11px;
|
|
4526
|
+
color: var(--text-muted);
|
|
4527
|
+
margin-top: 4px;
|
|
4528
|
+
font-style: italic;
|
|
4529
|
+
}
|
|
4530
|
+
.card.offline .card-hint {
|
|
4531
|
+
display: block;
|
|
4532
|
+
}
|
|
4430
4533
|
.footer {
|
|
4431
4534
|
margin-top: 28px;
|
|
4432
4535
|
text-align: center;
|
|
@@ -4598,6 +4701,25 @@ function generateAndOpenLaunchpad(installedModules = []) {
|
|
|
4598
4701
|
<span class="status-dot" id="dot-openwiki" title="Checking..."></span>
|
|
4599
4702
|
</div>
|
|
4600
4703
|
</a>
|
|
4704
|
+
<a class="card" href="http://127.0.0.1:4322" target="_blank">
|
|
4705
|
+
<div class="card-content">
|
|
4706
|
+
<div class="card-icon"><svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
4707
|
+
<defs><linearGradient id="grad-store" x1="50%" y1="0%" x2="50%" y2="100%"><stop offset="0%" stop-color="#FFFFFF"/><stop offset="100%" stop-color="#9b30c4"/></linearGradient></defs>
|
|
4708
|
+
<rect x="20" y="20" width="60" height="60" rx="4" fill="url(#grad-store)" opacity="0.85"/>
|
|
4709
|
+
<rect x="28" y="28" width="12" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/><rect x="45" y="28" width="12" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/><rect x="62" y="28" width="12" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/>
|
|
4710
|
+
<rect x="28" y="48" width="12" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/><rect x="45" y="48" width="12" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/><rect x="62" y="48" width="12" height="14" rx="1.5" fill="#0a0a0a" opacity="0.6"/>
|
|
4711
|
+
</svg></div>
|
|
4712
|
+
<div class="card-info">
|
|
4713
|
+
<h2>AOS Store</h2>
|
|
4714
|
+
<p>Browse & install skills and agents</p>
|
|
4715
|
+
<div class="card-hint">offline — run: aos-store ui</div>
|
|
4716
|
+
</div>
|
|
4717
|
+
</div>
|
|
4718
|
+
<div class="card-meta">
|
|
4719
|
+
<span class="port-pill">:4322</span>
|
|
4720
|
+
<span class="status-dot" id="dot-store" title="Checking..."></span>
|
|
4721
|
+
</div>
|
|
4722
|
+
</a>
|
|
4601
4723
|
</div>
|
|
4602
4724
|
<div class="footer">
|
|
4603
4725
|
<span>Autostart Daemons • 127.0.0.1</span>
|
|
@@ -4607,9 +4729,16 @@ function generateAndOpenLaunchpad(installedModules = []) {
|
|
|
4607
4729
|
function checkHealth(url, dotId) {
|
|
4608
4730
|
const dot = document.getElementById(dotId);
|
|
4609
4731
|
if (!dot) return;
|
|
4732
|
+
const card = dot.closest('.card');
|
|
4610
4733
|
fetch(url, { mode: 'no-cors' })
|
|
4611
|
-
.then(() =>
|
|
4612
|
-
|
|
4734
|
+
.then(() => {
|
|
4735
|
+
dot.classList.add('online');
|
|
4736
|
+
if (card) card.classList.remove('offline');
|
|
4737
|
+
})
|
|
4738
|
+
.catch(() => {
|
|
4739
|
+
dot.classList.remove('online');
|
|
4740
|
+
if (card) card.classList.add('offline');
|
|
4741
|
+
});
|
|
4613
4742
|
}
|
|
4614
4743
|
function checkAllHealth() {
|
|
4615
4744
|
checkHealth('http://127.0.0.1:8088', 'dot-memb');
|
|
@@ -4617,6 +4746,7 @@ function generateAndOpenLaunchpad(installedModules = []) {
|
|
|
4617
4746
|
checkHealth('http://127.0.0.1:3101', 'dot-ao');
|
|
4618
4747
|
checkHealth('http://127.0.0.1:9080', 'dot-remote');
|
|
4619
4748
|
checkHealth('http://127.0.0.1:4321', 'dot-openwiki');
|
|
4749
|
+
checkHealth('http://127.0.0.1:4322', 'dot-store');
|
|
4620
4750
|
}
|
|
4621
4751
|
checkAllHealth();
|
|
4622
4752
|
setInterval(checkAllHealth, 5000);
|
|
@@ -4778,47 +4908,10 @@ async function universalHarnessSync(primaryMcpConfigPath, installedModules = [])
|
|
|
4778
4908
|
}
|
|
4779
4909
|
}
|
|
4780
4910
|
|
|
4781
|
-
// Wire BDB AOS Plugin for OpenCode
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
const pluginSrc = path.join(srcDir, '.opencode', 'plugins', 'bdb-aos.js');
|
|
4786
|
-
|
|
4787
|
-
try {
|
|
4788
|
-
if (fs.existsSync(pluginSrc)) {
|
|
4789
|
-
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
4790
|
-
fs.copyFileSync(pluginSrc, pluginFile);
|
|
4791
|
-
try { fs.chmodSync(pluginFile, 0o644); } catch (e) { logDebug(e, 'chmod pluginFile'); }
|
|
4792
|
-
}
|
|
4793
|
-
} catch (pluginErr) {
|
|
4794
|
-
log.warn(`Could not install OpenCode plugin: ${pluginErr.message}`);
|
|
4795
|
-
}
|
|
4796
|
-
|
|
4797
|
-
// Register plugin in opencode.jsonc if plugin file exists
|
|
4798
|
-
if (fs.existsSync(pluginFile)) {
|
|
4799
|
-
if (!Array.isArray(data.plugin)) {
|
|
4800
|
-
data.plugin = [];
|
|
4801
|
-
}
|
|
4802
|
-
const pluginPathNormalized = pluginFile.replace(/\\/g, '/');
|
|
4803
|
-
const alreadyRegistered = data.plugin.some(p => {
|
|
4804
|
-
const str = typeof p === 'string' ? p : (Array.isArray(p) ? p[0] : '');
|
|
4805
|
-
return str.includes('bdb-aos');
|
|
4806
|
-
});
|
|
4807
|
-
if (!alreadyRegistered) {
|
|
4808
|
-
data.plugin.push(pluginPathNormalized);
|
|
4809
|
-
}
|
|
4810
|
-
}
|
|
4811
|
-
|
|
4812
|
-
// Register skills paths for OpenCode
|
|
4813
|
-
if (!data.skills || typeof data.skills !== 'object') {
|
|
4814
|
-
data.skills = { paths: [".agents/skills"] };
|
|
4815
|
-
} else if (Array.isArray(data.skills.paths)) {
|
|
4816
|
-
if (!data.skills.paths.includes(".agents/skills")) {
|
|
4817
|
-
data.skills.paths.push(".agents/skills");
|
|
4818
|
-
}
|
|
4819
|
-
} else {
|
|
4820
|
-
data.skills.paths = [".agents/skills"];
|
|
4821
|
-
}
|
|
4911
|
+
// Wire BDB AOS Plugin for OpenCode. `data` is handed in because this
|
|
4912
|
+
// function owns the write -- it merges MCP servers in the same pass.
|
|
4913
|
+
// homeDir matches how the opencode harness entry builds its path.
|
|
4914
|
+
installOpencodePlugin({ targetHome: homeDir, configPath: targetPath, data });
|
|
4822
4915
|
|
|
4823
4916
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
4824
4917
|
fs.writeFileSync(targetPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
@@ -5497,6 +5590,7 @@ module.exports = {
|
|
|
5497
5590
|
mergeCodexHooks: mergeCodexTomlHooks,
|
|
5498
5591
|
mergeCodexTomlMcpServers,
|
|
5499
5592
|
installGlobalHooks,
|
|
5593
|
+
installOpencodePlugin,
|
|
5500
5594
|
installProjectHarness,
|
|
5501
5595
|
promptMcpSelection,
|
|
5502
5596
|
mirrorMcpServersTo,
|