diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 3224d26..4ea4c74 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -83,6 +83,7 @@ the README.
One source, four manifests: `.claude-plugin/marketplace.json` (+ `forgebee/.claude-plugin/plugin.json`),
`.codex-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `gemini-extension.json`,
-plus `openclaw/install-openclaw.js` (converts agents/commands to OpenClaw skills).
+plus `openclaw/install-openclaw.js` (converts agents/commands to OpenClaw skills)
+and `opencode/install-opencode.js` (converts skills, agents, commands to OpenCode format).
All version-bearing files are listed in `.version-bump.json` and kept in sync by
`bump-version.sh`. Surface **counts** in manifest/README prose are maintained by hand.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 905af45..597c65f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,14 @@ The format roughly follows [Keep a Changelog](https://keepachangelog.com/) and t
---
+## [Unreleased]
+
+### Added
+
+- **OpenCode installer.** `node opencode/install-opencode.js [target-dir]` converts skills, agents, commands, and rules to OpenCode format. Hooks do not convert. `npm run check:opencode` tests it.
+
+---
+
## [7.1.1] — 2026-10-20
### Fixed
diff --git a/README.md b/README.md
index 51134f1..657bc32 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@
46 specialist agents. 35 skills. 39 slash commands. 28 lifecycle hooks.
Three execution modes: inline skills, context:fork, subagents.
Adaptive pipeline. Karpathy principles. Adversarial debate. Continuous learning.
- Works with Claude Code, Codex, Cursor, Gemini, and OpenClaw.
+ Works with Claude Code, Codex, Cursor, Gemini, OpenClaw, and OpenCode.
@@ -36,7 +36,8 @@
Learning •
Growth OS •
PM System •
- OpenClaw
+ OpenClaw •
+ OpenCode
---
@@ -421,6 +422,8 @@ The **skills, agents, and commands** are portable — they ship to Claude Code,
| Continuous learning (observe → instincts) | ✅ | ❌ |
| Auto project-triage routing | ✅ | ❌ |
+OpenCode gets skills, agents, commands, and rules through its [own installer](#opencode). It gets no hooks.
+
On non-Claude harnesses you get the full prompt library; the guardrails, governance, and learning loop are Claude Code-only today.
---
@@ -615,6 +618,51 @@ This converts every agent and command into `SKILL.md` files in `~/.openclaw/work
---
+## OpenCode
+
+ForgeBee converts to [OpenCode](https://opencode.ai) format. Skills, agents, commands, and rules work. Hooks do not.
+
+```bash
+# Global install (default target: ~/.config/opencode)
+node ForgeBee/opencode/install-opencode.js
+
+# Project install
+node ForgeBee/opencode/install-opencode.js ./.opencode
+```
+
+
+How the conversion works
+
+- Each skill goes to `skills/forgebee-/` with the whole skill directory. The `name:` field matches the directory.
+- Each agent goes to `agents/forgebee-.md` with `mode: subagent`. The `tools:` list becomes a `permission:` map.
+- `orchestrator` and `direct` are skipped. They need Claude Code hooks.
+- Each command goes to `commands/.md`. Only `description`, `agent`, and `subtask` stay.
+- The `forgebee/` tree (without `eval/`) goes to `/forgebee/`. `${CLAUDE_PLUGIN_ROOT}` becomes that absolute path.
+- `forgebee:` references become `forgebee-`.
+- The installer writes `forgebee/.opencode-install.json`. A reinstall deletes only the paths in that file.
+
+**Not converted:** hooks. Orchestrator mode, guards, learning, and PM sync do not run on OpenCode.
+
+**Existing files.** The installer keeps your own files. If a skill, agent, or command already exists at the target, it skips that file and lists it in the summary. It does not overwrite it.
+
+**Rules.** The installer does not edit `opencode.json`. Add this entry yourself. Use the absolute path the installer prints. Do not use `~`.
+
+```json
+{
+ "instructions": ["/absolute/path/to/opencode/forgebee/rules/common/general.md"]
+}
+```
+
+Add `rules/php/conventions.md`, `rules/python/conventions.md`, or `rules/typescript/conventions.md` for your stack. Do not load `rules/common/hooks.md`. It documents Claude Code hooks.
+
+OpenCode reads `CLAUDE.md` when no `AGENTS.md` exists.
+
+**Models.** The installer drops `model:` from agents. Claude aliases such as `opus` are not valid in OpenCode. Subagents use the model of the calling agent. To pin a model, add `model: provider/model-id` to the agent file.
+
+
+
+---
+
## Contributing
Contributions welcome! ForgeBee is markdown surfaces and Node.js scripts. See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for the surface contracts (agent/skill/command structure, the load-bearing Status + adversarial-hardening contract, the model-tier policy, output modes) and **[ARCHITECTURE.md](./ARCHITECTURE.md)** for how the pieces fit together.
diff --git a/forgebee/README.md b/forgebee/README.md
index 64e695b..8e0aa66 100644
--- a/forgebee/README.md
+++ b/forgebee/README.md
@@ -76,7 +76,7 @@ forgebee/
### Other capabilities
- **Orchestrator by default** — the main thread runs as `forgebee:orchestrator`, delegates real work, keeps tiny edits inline; opt out with `claude --agent forgebee:direct`
-- **Multi-platform** — works with Claude Code, Codex, Cursor, Gemini, OpenClaw (separate manifests)
+- **Multi-platform** — works with Claude Code, Codex, Cursor, Gemini, OpenClaw, OpenCode (separate manifests or installers)
- **Adaptive pipeline** — `/workflow` scrum phase is promptable (full sprint planning OR direct delegation)
- **Decision logs + addenda** — `/workflow` and `/plan` persist decisions across runs
- **Continuous learning** — Stop hook asks Claude to save instincts; SessionStart digest applies them and auto-activates ≥0.7; `/learn` and `/evolve` stay manual
diff --git a/opencode/install-opencode.js b/opencode/install-opencode.js
new file mode 100644
index 0000000..047dde2
--- /dev/null
+++ b/opencode/install-opencode.js
@@ -0,0 +1,213 @@
+#!/usr/bin/env node
+
+/**
+ * ForgeBee → OpenCode installer (skills, agents, commands, rules; no hooks)
+ * Usage: node opencode/install-opencode.js [target-dir]
+ * Default target: ~/.config/opencode. For a project install pass ./.opencode
+ */
+
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const SRC = path.join(path.dirname(path.dirname(path.resolve(__filename))), 'forgebee');
+const TARGET = path.resolve(process.argv[2] || path.join(os.homedir(), '.config', 'opencode'));
+const ROOT = path.join(TARGET, 'forgebee');
+const MANIFEST = path.join(ROOT, '.opencode-install.json');
+
+const SKIP_AGENTS = { orchestrator: 'main-thread agent, needs Claude Code hooks', direct: 'main-thread agent, needs Claude Code hooks' };
+const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
+const COLOR_RE = /^(#[0-9a-fA-F]{6}|primary|secondary|accent|success|warning|error|info)$/;
+const TOOL_MAP = { Read: 'read', Write: 'edit', Edit: 'edit', Glob: 'glob', Grep: 'grep', Bash: 'bash', Task: 'task', WebSearch: 'websearch', WebFetch: 'webfetch' };
+const PERMISSION_KEYS = ['read', 'edit', 'glob', 'grep', 'list', 'bash', 'task', 'webfetch', 'websearch'];
+
+function fail(msg) {
+ throw new Error(msg);
+}
+
+// Split a markdown file into frontmatter lines and body text.
+function splitFrontmatter(text, file) {
+ const lines = text.split('\n');
+ if (lines[0].trim() !== '---') fail(`${file}: missing frontmatter`);
+ const end = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
+ if (end < 0) fail(`${file}: unterminated frontmatter`);
+ return { fm: lines.slice(1, end), body: lines.slice(end + 1).join('\n') };
+}
+
+function fmFields(fm) {
+ const fields = {};
+ for (const line of fm) {
+ const m = line.match(/^([A-Za-z-]+):\s*(.*)$/);
+ if (m) fields[m[1]] = m[2].trim();
+ }
+ return fields;
+}
+
+const join = (fm, body) => `---\n${fm.join('\n')}\n---\n${body}`;
+
+function rewrite(text) {
+ return text.replaceAll('${CLAUDE_PLUGIN_ROOT}', ROOT).replaceAll('forgebee:', 'forgebee-');
+}
+
+function mdFiles(dir) {
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => {
+ const p = path.join(dir, e.name);
+ return e.isDirectory() ? mdFiles(p) : e.name.endsWith('.md') ? [p] : [];
+ });
+}
+
+function rewriteTree(dir) {
+ for (const f of mdFiles(dir)) fs.writeFileSync(f, rewrite(fs.readFileSync(f, 'utf-8')));
+}
+
+function removeOldInstall() {
+ if (!fs.existsSync(MANIFEST)) return;
+ const old = JSON.parse(fs.readFileSync(MANIFEST, 'utf-8'));
+ for (const rel of old.created || []) {
+ const abs = path.resolve(TARGET, rel);
+ // Guard: a tampered manifest must not delete outside the target or the resource root.
+ if (path.isAbsolute(rel) || !abs.startsWith(TARGET + path.sep) || abs === ROOT || abs.startsWith(ROOT + path.sep)) {
+ fail(`manifest has unsafe path: ${rel}`);
+ }
+ fs.rmSync(abs, { recursive: true, force: true });
+ }
+}
+
+function copyResources() {
+ fs.mkdirSync(ROOT, { recursive: true });
+ // shortcut: stale files from older versions stay in forgebee/, upgrade when files are renamed often.
+ fs.cpSync(SRC, ROOT, {
+ recursive: true,
+ force: true,
+ filter: src => src !== path.join(SRC, 'eval'),
+ });
+ rewriteTree(ROOT);
+}
+
+// removeOldInstall() ran first, so any path that still exists is user-owned.
+const kept = [];
+function isUserOwned(rel) {
+ if (!fs.existsSync(path.join(TARGET, rel))) return false;
+ kept.push(path.join(TARGET, rel));
+ return true;
+}
+
+function listDir(dir, pred) {
+ return fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).filter(pred).map(e => e.name) : [];
+}
+
+function installSkills(created) {
+ const srcDir = path.join(SRC, 'skills');
+ let count = 0;
+ for (const name of listDir(srcDir, e => e.isDirectory())) {
+ const file = path.join(srcDir, name, 'SKILL.md');
+ if (!fs.existsSync(file)) continue;
+ const full = `forgebee-${name}`;
+ if (full.length > 64 || !NAME_RE.test(full)) fail(`skill ${name}: invalid OpenCode name "${full}"`);
+ const { fm, body } = splitFrontmatter(fs.readFileSync(file, 'utf-8'), file);
+ const desc = fmFields(fm).description || '';
+ if (desc.length < 1 || desc.length > 1024) fail(`skill ${name}: description must be 1-1024 chars (got ${desc.length})`);
+ if (isUserOwned(path.join('skills', full))) continue;
+ const dest = path.join(TARGET, 'skills', full);
+ fs.cpSync(path.join(srcDir, name), dest, { recursive: true });
+ const newFm = fm.map(l => (/^name:/.test(l) ? `name: ${full}` : l));
+ fs.writeFileSync(path.join(dest, 'SKILL.md'), rewrite(join(newFm, body)));
+ rewriteTree(dest);
+ created.push(path.join('skills', full));
+ count++;
+ }
+ return count;
+}
+
+function permissionBlock(toolsLine) {
+ const allowed = new Set(toolsLine.split(',').map(t => TOOL_MAP[t.trim()]).filter(Boolean));
+ if (allowed.has('glob')) allowed.add('list');
+ return ['permission:', ...PERMISSION_KEYS.map(k => ` ${k}: ${allowed.has(k) ? 'allow' : 'deny'}`)];
+}
+
+function installAgents(created) {
+ const srcDir = path.join(SRC, 'agents');
+ const skipped = [];
+ let count = 0;
+ for (const file of listDir(srcDir, e => e.isFile() && e.name.endsWith('.md'))) {
+ const name = path.basename(file, '.md');
+ if (SKIP_AGENTS[name]) {
+ skipped.push(`${name} (${SKIP_AGENTS[name]})`);
+ continue;
+ }
+ const { fm, body } = splitFrontmatter(fs.readFileSync(path.join(srcDir, file), 'utf-8'), file);
+ const f = fmFields(fm);
+ if (!f.description) fail(`agent ${name}: description is required`);
+ // model: is dropped on purpose. Claude aliases like `opus` are invalid in OpenCode,
+ // so the subagent inherits the caller's model. Users can set `model: provider/model-id` by hand.
+ const out = [`description: ${f.description}`, 'mode: subagent'];
+ if (f.color && COLOR_RE.test(f.color.replace(/^["']|["']$/g, ''))) out.push(`color: ${f.color}`);
+ if (f.tools !== undefined) out.push(...permissionBlock(f.tools));
+ const rel = path.join('agents', `forgebee-${file}`);
+ if (isUserOwned(rel)) continue;
+ fs.mkdirSync(path.join(TARGET, 'agents'), { recursive: true });
+ fs.writeFileSync(path.join(TARGET, rel), rewrite(join(out, body)));
+ created.push(rel);
+ count++;
+ }
+ return { count, skipped };
+}
+
+function installCommands(created) {
+ const srcDir = path.join(SRC, 'commands');
+ const keep = new Set(['description', 'agent', 'subtask']);
+ let count = 0;
+ for (const file of listDir(srcDir, e => e.isFile() && e.name.endsWith('.md'))) {
+ const { fm, body } = splitFrontmatter(fs.readFileSync(path.join(srcDir, file), 'utf-8'), file);
+ const kept = fm.filter(l => keep.has((l.match(/^([A-Za-z-]+):/) || [])[1]));
+ const rel = path.join('commands', file);
+ if (isUserOwned(rel)) continue;
+ fs.mkdirSync(path.join(TARGET, 'commands'), { recursive: true });
+ fs.writeFileSync(path.join(TARGET, rel), rewrite(join(kept, body)));
+ created.push(rel);
+ count++;
+ }
+ return count;
+}
+
+function printSummary({ skills, agents, commands }) {
+ const lines = [
+ '',
+ 'ForgeBee installed for OpenCode.',
+ ` Target: ${TARGET}`,
+ ` Skills: ${skills}`,
+ ` Agents: ${agents.count}`,
+ ` Commands: ${commands}`,
+ ` Skipped agents: ${agents.skipped.join('; ') || 'none'}`,
+ ...(kept.length ? [` Kept your existing files: ${kept.join(', ')}`] : []),
+ '',
+ 'Hooks do not run on OpenCode: orchestrator mode, guards, learning, and PM sync are off.',
+ '',
+ 'Rules: add this to your opencode.json (the installer does not edit it):',
+ JSON.stringify({ instructions: [path.join(ROOT, 'rules', 'common', 'general.md')] }, null, 2),
+ `Optional, add for your stack: ${['php', 'python', 'typescript'].map(l => path.join(ROOT, 'rules', l, 'conventions.md')).join(', ')}`,
+ 'OpenCode falls back to CLAUDE.md when no AGENTS.md exists.',
+ 'Model: agents have no model set. They inherit the model of the calling agent.',
+ ];
+ console.log(lines.join('\n'));
+}
+
+function main() {
+ const version = JSON.parse(fs.readFileSync(path.join(SRC, '.claude-plugin', 'plugin.json'), 'utf-8')).version;
+ console.log(`ForgeBee ${version} -> OpenCode\n Source: ${SRC}\n Target: ${TARGET}`);
+ removeOldInstall();
+ copyResources();
+ const created = [];
+ const skills = installSkills(created);
+ const agents = installAgents(created);
+ const commands = installCommands(created);
+ fs.writeFileSync(MANIFEST, JSON.stringify({ version, created }, null, 2) + '\n');
+ printSummary({ skills, agents, commands });
+}
+
+try {
+ main();
+} catch (err) {
+ console.error(`Install failed: ${err.message}`);
+ process.exit(1);
+}
diff --git a/opencode/test-install-opencode.js b/opencode/test-install-opencode.js
new file mode 100644
index 0000000..ab1ca85
--- /dev/null
+++ b/opencode/test-install-opencode.js
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+// Self-check: install twice into a temp dir, then assert the OpenCode output shape.
+
+const assert = require('node:assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const INSTALLER = path.join(__dirname, 'install-opencode.js');
+const SRC = path.join(__dirname, '..', 'forgebee');
+const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
+
+const frontmatter = f => fs.readFileSync(f, 'utf-8').split('\n---')[0].split('\n').slice(1);
+const field = (fm, key) => (fm.find(l => l.startsWith(`${key}:`)) || '').slice(key.length + 1).trim();
+const walk = dir => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => {
+ const p = path.join(dir, e.name);
+ return e.isDirectory() ? walk(p) : [p];
+});
+
+const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'forgebee-opencode-'));
+try {
+ for (let i = 0; i < 2; i++) execFileSync('node', [INSTALLER, tmp], { stdio: 'pipe' });
+
+ const skillsDir = path.join(tmp, 'skills');
+ const skills = fs.readdirSync(skillsDir);
+ assert.ok(skills.length > 0, 'no skills installed');
+ for (const dir of skills) {
+ const name = field(frontmatter(path.join(skillsDir, dir, 'SKILL.md')), 'name');
+ assert.strictEqual(name, dir, `skill name != dir: ${dir}`);
+ assert.match(name, NAME_RE);
+ assert.ok(name.length <= 64);
+ }
+
+ const mds = walk(tmp).filter(f => f.endsWith('.md'));
+ for (const f of mds) {
+ const text = fs.readFileSync(f, 'utf-8');
+ assert.ok(!text.includes('${CLAUDE_PLUGIN_ROOT}'), `CLAUDE_PLUGIN_ROOT left in ${f}`);
+ assert.ok(!text.includes('forgebee:'), `forgebee: left in ${f}`);
+ }
+
+ const agentsDir = path.join(tmp, 'agents');
+ const agents = fs.readdirSync(agentsDir);
+ assert.ok(agents.length > 0, 'no agents installed');
+ for (const a of agents) {
+ const fm = frontmatter(path.join(agentsDir, a));
+ assert.ok(field(fm, 'description'), `${a}: no description`);
+ assert.strictEqual(field(fm, 'mode'), 'subagent', `${a}: mode`);
+ assert.ok(!field(fm, 'model') && !field(fm, 'tools'), `${a}: model/tools left`);
+ }
+ for (const skip of ['orchestrator', 'direct']) {
+ assert.ok(!agents.includes(`forgebee-${skip}.md`), `${skip} must be absent`);
+ }
+
+ const srcCmds = fs.readdirSync(path.join(SRC, 'commands')).filter(f => f.endsWith('.md')).length;
+ assert.strictEqual(fs.readdirSync(path.join(tmp, 'commands')).length, srcCmds, 'command count');
+
+ assert.ok(fs.existsSync(path.join(tmp, 'forgebee', '.opencode-install.json')), 'manifest missing');
+ assert.ok(!fs.existsSync(path.join(tmp, 'forgebee', 'eval')), 'eval must not be copied');
+
+ const own = fs.mkdtempSync(path.join(os.tmpdir(), 'forgebee-opencode-own-'));
+ try {
+ fs.mkdirSync(path.join(own, 'commands'));
+ const mine = path.join(own, 'commands', 'review.md');
+ fs.writeFileSync(mine, 'mine');
+ for (let i = 0; i < 2; i++) execFileSync('node', [INSTALLER, own], { stdio: 'pipe' });
+ assert.strictEqual(fs.readFileSync(mine, 'utf-8'), 'mine', 'user file overwritten');
+ const { created } = JSON.parse(fs.readFileSync(path.join(own, 'forgebee', '.opencode-install.json'), 'utf-8'));
+ assert.ok(!created.includes(path.join('commands', 'review.md')), 'user file in manifest');
+ } finally {
+ fs.rmSync(own, { recursive: true, force: true });
+ }
+
+ console.log(`OK: ${skills.length} skills, ${agents.length} agents, ${srcCmds} commands`);
+} finally {
+ fs.rmSync(tmp, { recursive: true, force: true });
+}
diff --git a/package.json b/package.json
index bb34b2b..ecc7239 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,9 @@
"check:references": "node scripts/check-references.js",
"check:agents": "node scripts/check-agent-contract.js",
"check:version": "bash scripts/bump-version.sh --check",
+ "check:opencode": "node opencode/test-install-opencode.js",
"eval": "node forgebee/eval/harness/run.js",
"eval:golden": "node forgebee/eval/golden/run-golden.js",
- "check": "npm run index:check && npm run check:references && npm run check:agents && npm run check:version && npm run eval"
+ "check": "npm run index:check && npm run check:references && npm run check:agents && npm run check:version && npm run eval && npm run check:opencode"
}
}