@hybridlabor-api/aos 4.10.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/.opencode/commands/startcycle-graph.md +57 -0
- package/.opencode/plugins/bdb-aos.js +130 -5
- package/installer.js +114 -57
- package/package.json +2 -2
|
@@ -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/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();
|
|
@@ -4778,47 +4871,10 @@ async function universalHarnessSync(primaryMcpConfigPath, installedModules = [])
|
|
|
4778
4871
|
}
|
|
4779
4872
|
}
|
|
4780
4873
|
|
|
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
|
-
}
|
|
4874
|
+
// Wire BDB AOS Plugin for OpenCode. `data` is handed in because this
|
|
4875
|
+
// function owns the write -- it merges MCP servers in the same pass.
|
|
4876
|
+
// homeDir matches how the opencode harness entry builds its path.
|
|
4877
|
+
installOpencodePlugin({ targetHome: homeDir, configPath: targetPath, data });
|
|
4822
4878
|
|
|
4823
4879
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
4824
4880
|
fs.writeFileSync(targetPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
@@ -5497,6 +5553,7 @@ module.exports = {
|
|
|
5497
5553
|
mergeCodexHooks: mergeCodexTomlHooks,
|
|
5498
5554
|
mergeCodexTomlMcpServers,
|
|
5499
5555
|
installGlobalHooks,
|
|
5556
|
+
installOpencodePlugin,
|
|
5500
5557
|
installProjectHarness,
|
|
5501
5558
|
promptMcpSelection,
|
|
5502
5559
|
mirrorMcpServersTo,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hybridlabor-api/aos",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.0",
|
|
4
4
|
"description": "AOS — A Curated AI AGENT OS. Optimized agent skills and add-ons like memB, OpenWiki, Heimdall Token Saver, and Godmode architectures.",
|
|
5
5
|
"main": "installer.js",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"plugin:build": "node scripts/build-plugin-manifest.mjs",
|
|
28
28
|
"plugin:check": "node scripts/build-plugin-manifest.mjs --check",
|
|
29
29
|
"doctor": "node bin/aos-doctor.mjs",
|
|
30
|
-
"test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node tests/aos-store.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs"
|
|
30
|
+
"test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node --test tests/opencode-graph-gate.test.js && node --test tests/cross-harness-hooks.test.js && node tests/aos-store.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|