From 4dd05ae18d030975e14008d57727b5631d7d66ef Mon Sep 17 00:00:00 2001
From: Jaco du Preez
Date: Wed, 4 Nov 2026 20:52:30 +0200
Subject: [PATCH 1/6] test(workflow): fix reference-check false positives
Scope the AGENTS.md agent check to the Studio Hierarchy roster and recognize omp's built-in /agents hub as a non-OCGS command.
Refs: docs/omp-compatibility.md
---
tests/workflow/invariants.mjs | 25 +++++++++++++++++++------
tests/workflow/references.mjs | 2 +-
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/tests/workflow/invariants.mjs b/tests/workflow/invariants.mjs
index cbc6dc1..0d14696 100644
--- a/tests/workflow/invariants.mjs
+++ b/tests/workflow/invariants.mjs
@@ -167,18 +167,31 @@ console.log('\n=== Cross-Cutting Invariants ===\n');
});
}
-{ // I5: All AGENTS.md and other root agent files reference only existing agents
+{ // I5: The Studio Hierarchy roster references only existing agent files
const agentDocPath = join(ROOT, 'AGENTS.md');
if (existsSync(agentDocPath)) {
const content = readFileSync(agentDocPath, 'utf-8');
- const agentRefs = [...content.matchAll(/`([a-z][\w-]+)`/g)].map(m => m[1]);
- const unique = [...new Set(agentRefs)];
const agentNames = existsSync(AGENTS_DIR)
? new Set(readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md')).map(f => f.replace('.md', '')))
: new Set();
- const developmentRelated = new Set(['development']);
- const unknown = unique.filter(r => !agentNames.has(r) && !developmentRelated.has(r));
- run('I5: AGENTS.md references only existing agent files', () => {
+
+ const start = content.indexOf('## Studio Hierarchy');
+ const end = start === -1 ? -1 : content.indexOf('\n## ', start + 1);
+ const hierarchy = start === -1
+ ? ''
+ : content.slice(start, end === -1 ? content.length : end);
+
+ const roster = [];
+ for (const line of hierarchy.split('\n')) {
+ if (!/^ \S/.test(line)) continue;
+ for (const token of line.trim().split(/\s+/)) {
+ if (/^[a-z][a-z0-9-]+$/.test(token)) roster.push(token);
+ }
+ }
+
+ const unknown = [...new Set(roster)].filter(r => !agentNames.has(r));
+ run('I5: AGENTS.md studio hierarchy lists only existing agent files', () => {
+ if (!hierarchy) throw new Error('Studio Hierarchy section not found in AGENTS.md');
if (unknown.length > 0) {
throw new Error(`Unknown agents referenced: ${unknown.join(', ')}`);
}
diff --git a/tests/workflow/references.mjs b/tests/workflow/references.mjs
index d75be40..c9a3648 100644
--- a/tests/workflow/references.mjs
+++ b/tests/workflow/references.mjs
@@ -80,7 +80,7 @@ const knownNonCommands = new Set([
'tech-debt', 'playtest-report', 'onboard', 'estimate', 'adopt',
'hybrid-prototype', 'caveman', 'caveman-help', 'caveman-commit',
'caveman-compress', 'caveman-review',
- 'skill-name', 'command',
+ 'skill-name', 'command', 'agents',
]);
{ // R1: Every /command ref in skills matches a command file
From e552c5ffe6e7274f26c5a24f657b636c82eeb4a2 Mon Sep 17 00:00:00 2001
From: Jaco du Preez
Date: Wed, 4 Nov 2026 20:52:37 +0200
Subject: [PATCH 2/6] feat(modules): add per-harness MCP config writers
Author MCP fragments per harness and merge them into opencode.json,
.pi/mcp.json, .omp/mcp.json, .mcp.json, or .codex/config.toml. Removal
deletes only the entry the installer added.
Refs: docs/framework/setup-requirements.md
---
.agents/modules/art/mcp/claude.json | 15 ++
.agents/modules/art/mcp/codex.json | 16 ++
.agents/modules/art/mcp/omp.json | 16 ++
.../art/mcp/{aseprite.json => opencode.json} | 0
.agents/modules/art/mcp/pi.json | 16 ++
.opencode/modules/art/mcp/claude.json | 15 ++
.opencode/modules/art/mcp/codex.json | 16 ++
.opencode/modules/art/mcp/omp.json | 16 ++
.../art/mcp/{aseprite.json => opencode.json} | 0
.opencode/modules/art/mcp/pi.json | 16 ++
tools/lib/mcp-targets.mjs | 152 ++++++++++++++++++
tools/migrate-to-modules.mjs | 56 +++++--
12 files changed, 324 insertions(+), 10 deletions(-)
create mode 100644 .agents/modules/art/mcp/claude.json
create mode 100644 .agents/modules/art/mcp/codex.json
create mode 100644 .agents/modules/art/mcp/omp.json
rename .agents/modules/art/mcp/{aseprite.json => opencode.json} (100%)
create mode 100644 .agents/modules/art/mcp/pi.json
create mode 100644 .opencode/modules/art/mcp/claude.json
create mode 100644 .opencode/modules/art/mcp/codex.json
create mode 100644 .opencode/modules/art/mcp/omp.json
rename .opencode/modules/art/mcp/{aseprite.json => opencode.json} (100%)
create mode 100644 .opencode/modules/art/mcp/pi.json
create mode 100644 tools/lib/mcp-targets.mjs
diff --git a/.agents/modules/art/mcp/claude.json b/.agents/modules/art/mcp/claude.json
new file mode 100644
index 0000000..0f69cf1
--- /dev/null
+++ b/.agents/modules/art/mcp/claude.json
@@ -0,0 +1,15 @@
+{
+ "aseprite": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env": {
+ "ASEPRITE_PATH": "${ASEPRITE_PATH}"
+ }
+ }
+}
diff --git a/.agents/modules/art/mcp/codex.json b/.agents/modules/art/mcp/codex.json
new file mode 100644
index 0000000..2e93e67
--- /dev/null
+++ b/.agents/modules/art/mcp/codex.json
@@ -0,0 +1,16 @@
+{
+ "aseprite": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env_vars": [
+ "ASEPRITE_PATH"
+ ],
+ "enabled": true
+ }
+}
diff --git a/.agents/modules/art/mcp/omp.json b/.agents/modules/art/mcp/omp.json
new file mode 100644
index 0000000..a3ffa53
--- /dev/null
+++ b/.agents/modules/art/mcp/omp.json
@@ -0,0 +1,16 @@
+{
+ "aseprite": {
+ "type": "stdio",
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env": {
+ "ASEPRITE_PATH": "${ASEPRITE_PATH}"
+ }
+ }
+}
diff --git a/.agents/modules/art/mcp/aseprite.json b/.agents/modules/art/mcp/opencode.json
similarity index 100%
rename from .agents/modules/art/mcp/aseprite.json
rename to .agents/modules/art/mcp/opencode.json
diff --git a/.agents/modules/art/mcp/pi.json b/.agents/modules/art/mcp/pi.json
new file mode 100644
index 0000000..9326ef1
--- /dev/null
+++ b/.agents/modules/art/mcp/pi.json
@@ -0,0 +1,16 @@
+{
+ "aseprite": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env": {
+ "ASEPRITE_PATH": "${ASEPRITE_PATH}"
+ },
+ "exposure": "direct"
+ }
+}
diff --git a/.opencode/modules/art/mcp/claude.json b/.opencode/modules/art/mcp/claude.json
new file mode 100644
index 0000000..0f69cf1
--- /dev/null
+++ b/.opencode/modules/art/mcp/claude.json
@@ -0,0 +1,15 @@
+{
+ "aseprite": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env": {
+ "ASEPRITE_PATH": "${ASEPRITE_PATH}"
+ }
+ }
+}
diff --git a/.opencode/modules/art/mcp/codex.json b/.opencode/modules/art/mcp/codex.json
new file mode 100644
index 0000000..2e93e67
--- /dev/null
+++ b/.opencode/modules/art/mcp/codex.json
@@ -0,0 +1,16 @@
+{
+ "aseprite": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env_vars": [
+ "ASEPRITE_PATH"
+ ],
+ "enabled": true
+ }
+}
diff --git a/.opencode/modules/art/mcp/omp.json b/.opencode/modules/art/mcp/omp.json
new file mode 100644
index 0000000..a3ffa53
--- /dev/null
+++ b/.opencode/modules/art/mcp/omp.json
@@ -0,0 +1,16 @@
+{
+ "aseprite": {
+ "type": "stdio",
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env": {
+ "ASEPRITE_PATH": "${ASEPRITE_PATH}"
+ }
+ }
+}
diff --git a/.opencode/modules/art/mcp/aseprite.json b/.opencode/modules/art/mcp/opencode.json
similarity index 100%
rename from .opencode/modules/art/mcp/aseprite.json
rename to .opencode/modules/art/mcp/opencode.json
diff --git a/.opencode/modules/art/mcp/pi.json b/.opencode/modules/art/mcp/pi.json
new file mode 100644
index 0000000..9326ef1
--- /dev/null
+++ b/.opencode/modules/art/mcp/pi.json
@@ -0,0 +1,16 @@
+{
+ "aseprite": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "tools/aseprite-mcp",
+ "run",
+ "-m",
+ "aseprite_mcp"
+ ],
+ "env": {
+ "ASEPRITE_PATH": "${ASEPRITE_PATH}"
+ },
+ "exposure": "direct"
+ }
+}
diff --git a/tools/lib/mcp-targets.mjs b/tools/lib/mcp-targets.mjs
new file mode 100644
index 0000000..972d530
--- /dev/null
+++ b/tools/lib/mcp-targets.mjs
@@ -0,0 +1,152 @@
+/**
+ * MCP config writers for each harness.
+ *
+ * Module MCP fragments are authored per harness (`mcp/.json`) in that
+ * harness's native shape. This module only merges and removes entries; it does
+ * not translate between shapes.
+ */
+
+import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
+import { dirname } from 'path';
+
+export function loadMcpTarget(kind, file) {
+ if (kind === 'opencode-json') return jsonTarget(file, 'mcp.servers');
+ if (kind === 'mcp-servers-json') return jsonTarget(file, 'mcpServers');
+ if (kind === 'codex-toml') return codexTomlTarget(file);
+ throw new Error(`Unknown MCP target kind '${kind}'`);
+}
+
+function jsonTarget(file, path) {
+ const config = readJson(file);
+ const container = ensureContainer(config, path);
+
+ return {
+ kind: 'json',
+ file,
+ has: (name) => Boolean(container[name]),
+ merge: (name, entry) => { container[name] = entry; },
+ remove: (name) => { delete container[name]; },
+ names: () => Object.keys(container),
+ save: () => writeJson(file, config),
+ };
+}
+
+function readJson(file) {
+ if (!existsSync(file)) return {};
+ const raw = readFileSync(file, 'utf-8');
+ try {
+ return JSON.parse(raw);
+ } catch (error) {
+ throw new Error(`Cannot parse ${file}: ${error.message}`);
+ }
+}
+
+function ensureContainer(config, path) {
+ const parts = path.split('.');
+ let cursor = config;
+ for (const part of parts) {
+ if (!cursor[part] || typeof cursor[part] !== 'object') cursor[part] = {};
+ cursor = cursor[part];
+ }
+ return cursor;
+}
+
+function writeJson(file, data) {
+ mkdirSync(dirname(file), { recursive: true });
+ writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
+}
+
+function codexTomlTarget(file) {
+ let text = existsSync(file) ? readFileSync(file, 'utf-8') : '';
+ let changed = false;
+
+ return {
+ kind: 'toml',
+ file,
+ has: (name) => hasTomlTable(text, name),
+ merge: (name, entry) => {
+ if (hasTomlTable(text, name)) return;
+ text = appendTomlTable(text, name, entry);
+ changed = true;
+ },
+ remove: (name) => {
+ if (!hasTomlTable(text, name)) return;
+ text = removeTomlTable(text, name);
+ changed = true;
+ },
+ names: () => listTomlTables(text),
+ save: () => {
+ if (!changed) return;
+ mkdirSync(dirname(file), { recursive: true });
+ writeFileSync(file, text);
+ },
+ };
+}
+
+// ── Minimal TOML support (only `[mcp_servers.]` tables) ──────────────
+
+export function tomlKey(value) {
+ return /^[A-Za-z0-9_-]+$/.test(value) ? value : JSON.stringify(value);
+}
+
+export function tomlValue(value) {
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
+ if (typeof value === 'number') return String(value);
+ if (typeof value === 'string') return JSON.stringify(value);
+ if (Array.isArray(value)) return `[${value.map(tomlValue).join(', ')}]`;
+ if (value && typeof value === 'object') {
+ const pairs = Object.entries(value)
+ .map(([key, nested]) => `${tomlKey(key)} = ${tomlValue(nested)}`)
+ .join(', ');
+ return `{ ${pairs} }`;
+ }
+ throw new Error(`Unsupported TOML value: ${String(value)}`);
+}
+
+export function hasTomlTable(text, name) {
+ return tablePattern(name).test(text);
+}
+
+export function listTomlTables(text) {
+ const names = [];
+ for (const match of text.matchAll(/^\[mcp_servers\.([A-Za-z0-9_-]+)\]\s*$/gm)) {
+ names.push(match[1]);
+ }
+ return names;
+}
+
+export function appendTomlTable(text, name, entry) {
+ const header = `[mcp_servers.${tomlKey(name)}]`;
+ const rows = Object.entries(entry).map(([key, value]) => `${tomlKey(key)} = ${tomlValue(value)}`);
+ const block = [header, ...rows].join('\n');
+ const trimmed = text.trimEnd();
+ return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
+}
+
+export function removeTomlTable(text, name) {
+ const lines = text.split('\n');
+ const header = `[mcp_servers.${tomlKey(name)}]`;
+ const start = lines.findIndex((line) => line.trim() === header);
+ if (start === -1) return text;
+
+ let end = lines.length;
+ for (let i = start + 1; i < lines.length; i++) {
+ if (/^\s*\[/.test(lines[i])) {
+ end = i;
+ break;
+ }
+ }
+
+ const before = lines.slice(0, start).join('\n').replace(/\n+$/, '');
+ const after = lines.slice(end).join('\n').replace(/^\n+/, '');
+ const joined = [before, after].filter(Boolean).join('\n\n');
+ return joined ? `${joined}\n` : '';
+}
+
+function tablePattern(name) {
+ return new RegExp(`^\\[mcp_servers\\.${escapeRegex(tomlKey(name))}\\]\\s*$`, 'm');
+}
+
+function escapeRegex(value) {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
diff --git a/tools/migrate-to-modules.mjs b/tools/migrate-to-modules.mjs
index 37007ec..d94e5cc 100644
--- a/tools/migrate-to-modules.mjs
+++ b/tools/migrate-to-modules.mjs
@@ -177,16 +177,52 @@ for (const d of coreDocs) {
}
console.log(` docs: ${coreDocs.length}`);
-console.log("\n=== Creating MCP Fragment ===");
-const mcpDst = join(DST, "art", "mcp", "aseprite.json");
-writeFileSync(mcpDst, JSON.stringify({
- aseprite: {
- type: "local",
- command: ["uv", "--directory", "tools/aseprite-mcp", "run", "-m", "aseprite_mcp"],
- enabled: true,
- environment: { ASEPRITE_PATH: "{env:ASEPRITE_PATH}" },
+console.log("\n=== Creating MCP Fragments ===");
+const ASEPRITE_ARGS = ["--directory", "tools/aseprite-mcp", "run", "-m", "aseprite_mcp"];
+const MCP_FRAGMENTS = {
+ "opencode.json": {
+ aseprite: {
+ type: "local",
+ command: ["uv", ...ASEPRITE_ARGS],
+ codemode: false,
+ environment: { ASEPRITE_PATH: "{env:ASEPRITE_PATH}" },
+ },
},
-}, null, 2) + "\n");
-console.log(" art/mcp/aseprite.json created");
+ "pi.json": {
+ aseprite: {
+ command: "uv",
+ args: ASEPRITE_ARGS,
+ env: { ASEPRITE_PATH: "${ASEPRITE_PATH}" },
+ exposure: "direct",
+ },
+ },
+ "omp.json": {
+ aseprite: {
+ type: "stdio",
+ command: "uv",
+ args: ASEPRITE_ARGS,
+ env: { ASEPRITE_PATH: "${ASEPRITE_PATH}" },
+ },
+ },
+ "claude.json": {
+ aseprite: {
+ command: "uv",
+ args: ASEPRITE_ARGS,
+ env: { ASEPRITE_PATH: "${ASEPRITE_PATH}" },
+ },
+ },
+ "codex.json": {
+ aseprite: {
+ command: "uv",
+ args: ASEPRITE_ARGS,
+ env_vars: ["ASEPRITE_PATH"],
+ enabled: true,
+ },
+ },
+};
+for (const [file, fragment] of Object.entries(MCP_FRAGMENTS)) {
+ writeFileSync(join(DST, "art", "mcp", file), JSON.stringify(fragment, null, 2) + "\n");
+ console.log(` art/mcp/${file} created`);
+}
console.log(`\nTotal: ${totalCopied} files migrated.`);
From 1109ddbd06c6d781b3ff9cc1d75fd2c33f10686b Mon Sep 17 00:00:00 2001
From: Jaco du Preez
Date: Wed, 4 Nov 2026 20:52:47 +0200
Subject: [PATCH 3/6] feat(modules): add harness-aware install targets
Add --harness opencode|pi|omp|claude|codex with per-harness content
targets, install state, and agent transforms: omp Markdown, Claude name
frontmatter, and Codex TOML. Keep --pi as an alias.
Refs: docs/framework/directory-structure.md
---
.github/workflows/agent-validation.yml | 2 +
.opencode/modules/install.mjs | 541 +++++++++++++++----------
package.json | 1 +
tests/modules/test-install-harness.mjs | 375 +++++++++++++++++
tools/lib/harness-targets.mjs | 263 ++++++++++++
5 files changed, 964 insertions(+), 218 deletions(-)
create mode 100644 tests/modules/test-install-harness.mjs
create mode 100644 tools/lib/harness-targets.mjs
diff --git a/.github/workflows/agent-validation.yml b/.github/workflows/agent-validation.yml
index c2d9846..39e77d4 100644
--- a/.github/workflows/agent-validation.yml
+++ b/.github/workflows/agent-validation.yml
@@ -51,6 +51,8 @@ jobs:
node-version: 20
- name: Validate module structure
run: node tests/modules/validate-modules.mjs
+ - name: Run harness installer tests
+ run: npm run test:modules
validate-plugins:
runs-on: ubuntu-latest
diff --git a/.opencode/modules/install.mjs b/.opencode/modules/install.mjs
index 4ff498d..f616727 100644
--- a/.opencode/modules/install.mjs
+++ b/.opencode/modules/install.mjs
@@ -1,32 +1,28 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync, unlinkSync, rmSync } from 'fs';
-import { join, resolve, dirname, relative, sep } from 'path';
+import { join, resolve, dirname, relative, sep, basename } from 'path';
import { fileURLToPath } from 'url';
import { spawnSync } from 'child_process';
-import { parseModulefile, parseFlowList } from '../../tools/lib/parse-modulefile.mjs';
+import { parseModulefile } from '../../tools/lib/parse-modulefile.mjs';
+import { HARNESS_IDS, HARNESS_TARGETS, parseHarnessArgs, resolveHarness } from '../../tools/lib/harness-targets.mjs';
+import { loadMcpTarget } from '../../tools/lib/mcp-targets.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..', '..');
-// Harness target: 'opencode' (default) or 'pi' (--pi flag)
-// Controls where module files are installed and where installed.json lives.
+// Harness target controls where module files are installed, where install state
+// lives, and which file receives MCP fragments. See tools/lib/harness-targets.mjs.
let HARNESS = 'opencode';
-let MODULES_DIR = join(ROOT, '.opencode', 'modules');
-let INSTALLED_PATH = join(MODULES_DIR, 'installed.json');
-let HARNESS_TARGET = '.opencode';
-const OPENCODE_JSON = join(ROOT, 'opencode.json');
+let TARGET = HARNESS_TARGETS[HARNESS];
+let MODULES_DIR = join(ROOT, TARGET.sourceRoot);
+let INSTALLED_PATH = join(ROOT, TARGET.stateFile);
function setHarness(mode) {
- HARNESS = mode;
- if (mode === 'pi') {
- MODULES_DIR = join(ROOT, '.agents', 'modules');
- HARNESS_TARGET = '.agents';
- } else {
- MODULES_DIR = join(ROOT, '.opencode', 'modules');
- HARNESS_TARGET = '.opencode';
- }
- INSTALLED_PATH = join(MODULES_DIR, 'installed.json');
+ TARGET = resolveHarness(mode);
+ HARNESS = TARGET.id;
+ MODULES_DIR = join(ROOT, TARGET.sourceRoot);
+ INSTALLED_PATH = join(ROOT, TARGET.stateFile);
}
// ── Logging ────────────────────────────────────────────────────────────────
@@ -39,7 +35,7 @@ function log(prefix, msg) {
}
function wrapWithHeader(label, fn) {
- console.log(`\n── ${label} ─${''.padEnd(60 - label.length, '─')}\n`);
+ console.log(`\n── ${label} ─${''.padEnd(Math.max(0, 60 - label.length), '─')}\n`);
const result = fn();
console.log(`\n${''.padEnd(64, '─')}\n`);
return result;
@@ -59,6 +55,13 @@ function readJSON(p) {
catch { return null; }
}
+function readJSONStrict(p) {
+ try { return JSON.parse(readFileSync(p, 'utf-8')); }
+ catch (error) {
+ throw new Error(`Invalid JSON in ${toForward(relative(ROOT, p))}: ${error.message}`);
+ }
+}
+
function writeJSON(p, data) {
writeFileSync(p, JSON.stringify(data, null, 2) + '\n');
}
@@ -79,6 +82,17 @@ function toForward(p) {
return p.split(sep).join('/');
}
+function installedRecordTarget(record) {
+ if (typeof record !== 'string') return record.target;
+ const [top, ...rest] = record.split('/');
+ const base = TARGET.dirs[top];
+ return base ? join(base, ...rest) : join(TARGET.contentRoot, record);
+}
+
+function installedRecordSource(record) {
+ return typeof record === 'string' ? record : record.source;
+}
+
// ── list command ───────────────────────────────────────────────────────────
function cmdList() {
@@ -99,6 +113,8 @@ function cmdList() {
rows.push({ name, version, status });
}
+ console.log(` Harness: ${TARGET.label} (${TARGET.sourceRoot} → ${TARGET.stateFile})`);
+
const nameW = Math.max(...rows.map(r => r.name.length), 6);
const verW = Math.max(...rows.map(r => r.version.length), 7);
const sepLine = ''.padEnd(nameW + verW + 10, '─');
@@ -124,9 +140,6 @@ function cmdInfo(name) {
// ── add command ────────────────────────────────────────────────────────────
-const INSTALL_SUBDIRS = ['agents', 'skills', 'commands', 'rules', 'plugins', 'docs'];
-const SKIP_DIRS = new Set(['mcp']);
-
function cmdAdd(args) {
const force = args.includes('--force');
const names = args.filter(a => a !== '--force');
@@ -137,106 +150,13 @@ function cmdAdd(args) {
for (const rawName of names) {
const name = rawName.toLowerCase();
- wrapWithHeader(`Installing module: ${name}`, () => {
- const mf = readModulefile(name);
- if (!mf) {
- log('ERR', `Module '${name}' not found (no modulefile.yaml)`);
+ wrapWithHeader(`Installing module: ${name} (${TARGET.label})`, () => {
+ try {
+ if (installOne(name, names, force, installed, summary)) processedNames.push(name);
+ } catch (error) {
+ log('ERR', error.message);
summary.errors++;
- return;
- }
-
- if (installed[name] && installed[name].status === 'installed' && !force) {
- log('SKIP', `Module '${name}' is already installed`);
- summary.skipped++;
- return;
- }
-
- // Check dependencies (future-proofing)
- if (mf.depends && mf.depends.length > 0) {
- for (const dep of mf.depends) {
- const depName = dep.split('>=')[0].split('=')[0].trim();
- const depInstalled = installed[depName] && installed[depName].status === 'installed';
- const beingInstalled = names.includes(depName);
- if (!depInstalled && !beingInstalled) {
- if (depName === name) continue;
- log('WARN', `Dependency '${dep}' is not installed — installing anyway`);
- }
- }
- }
-
- // Copy files from module subdirectories
- const moduleDir = join(MODULES_DIR, name);
- const entries = readdirSync(moduleDir);
- const addedFiles = [];
-
- for (const entry of entries) {
- if (entry === 'modulefile.yaml') continue;
- if (SKIP_DIRS.has(entry)) continue;
-
- const entryPath = join(moduleDir, entry);
- if (!statSync(entryPath).isDirectory()) continue;
- if (!INSTALL_SUBDIRS.includes(entry)) {
- log('SKIP', `Unknown subdirectory '${entry}' — skipped`);
- continue;
- }
-
- walkAndCopy(entryPath, join(ROOT, HARNESS_TARGET, entry), addedFiles, force);
- }
-
- // Handle MCP fragments
- const mcpDir = join(moduleDir, 'mcp');
- const mcpServers = [];
- if (existsSync(mcpDir) && statSync(mcpDir).isDirectory()) {
- const mcpFiles = readdirSync(mcpDir).filter(f => f.endsWith('.json'));
- for (const mcpFile of mcpFiles) {
- const mcpPath = join(mcpDir, mcpFile);
- const fragment = readJSON(mcpPath);
- if (!fragment) {
- log('WARN', `MCP fragment '${mcpFile}' is not valid JSON — skipped`);
- continue;
- }
- // MCP fragments are only merged into opencode.json (OpenCode harness).
- // Pi harness manages MCP separately — skip with a warning.
- if (HARNESS === 'pi') {
- log('WARN', `MCP fragment '${mcpFile}' — Pi harness does not merge MCP via installer (skipped)`);
- summary.mcpSkipped++;
- continue;
- }
- const ocfg = readJSON(OPENCODE_JSON) || {};
- if (!ocfg.mcp) ocfg.mcp = {};
- if (!ocfg.mcp.servers) ocfg.mcp.servers = {};
- let merged = false;
- for (const serverName of Object.keys(fragment)) {
- if (ocfg.mcp.servers[serverName]) {
- log('WARN', `MCP server '${serverName}' already exists in opencode.json — skipped`);
- summary.mcpSkipped++;
- } else {
- ocfg.mcp.servers[serverName] = fragment[serverName];
- log('ADD', `MCP server '${serverName}' merged into opencode.json`);
- mcpServers.push(serverName);
- summary.mcpAdded++;
- merged = true;
- }
- }
- if (merged) writeJSON(OPENCODE_JSON, ocfg);
- }
}
-
- // Record in installed.json
- const timestamp = new Date().toISOString();
- installed[name] = {
- version: mf.version || '0.1.0',
- status: 'installed',
- timestamp,
- files: addedFiles,
- mcp: mcpServers,
- };
- writeInstalled(installed);
- processedNames.push(name);
-
- summary.added++;
-
- log('OK', `Module '${name}' v${installed[name].version} installed (${addedFiles.length} files, ${mcpServers.length} MCP servers)`);
});
}
@@ -247,38 +167,152 @@ function cmdAdd(args) {
// Summary
console.log('');
+ console.log(` Harness: ${TARGET.label}`);
console.log(` Added: ${summary.added}, Skipped: ${summary.skipped}, Errors: ${summary.errors}`);
console.log(` MCP merged: ${summary.mcpAdded}, MCP skipped: ${summary.mcpSkipped}`);
}
-function walkAndCopy(srcDir, destDir, fileList, force = false) {
+function installOne(name, names, force, installed, summary) {
+ const mf = readModulefile(name);
+ if (!mf) {
+ log('ERR', `Module '${name}' not found (no modulefile.yaml)`);
+ summary.errors++;
+ return false;
+ }
+
+ if (installed[name] && installed[name].status === 'installed' && !force) {
+ log('SKIP', `Module '${name}' is already installed`);
+ summary.skipped++;
+ return false;
+ }
+
+ // Check dependencies (future-proofing)
+ if (mf.depends && mf.depends.length > 0) {
+ for (const dep of mf.depends) {
+ const depName = dep.split('>=')[0].split('=')[0].trim();
+ const depInstalled = installed[depName] && installed[depName].status === 'installed';
+ const beingInstalled = names.includes(depName);
+ if (!depInstalled && !beingInstalled) {
+ if (depName === name) continue;
+ log('WARN', `Dependency '${dep}' is not installed — installing anyway`);
+ }
+ }
+ }
+
+ const moduleDir = join(MODULES_DIR, name);
+
+ // Validate the MCP fragment before copying anything, so a malformed
+ // fragment cannot leave an untracked partial install.
+ const fragmentPath = findMcpFragment(moduleDir);
+ const fragment = fragmentPath ? readJSONStrict(fragmentPath) : null;
+ if (fragment && (typeof fragment !== 'object' || Array.isArray(fragment))) {
+ throw new Error(`MCP fragment ${toForward(relative(ROOT, fragmentPath))} must be a JSON object of server entries`);
+ }
+
+ // Copy files from module subdirectories that this harness can consume.
+ const addedFiles = [];
+
+ for (const entry of readdirSync(moduleDir)) {
+ if (entry === 'modulefile.yaml' || entry === 'mcp') continue;
+
+ const entryPath = join(moduleDir, entry);
+ if (!statSync(entryPath).isDirectory()) continue;
+
+ const destination = TARGET.dirs[entry];
+ if (!destination) {
+ log('SKIP', `'${entry}' has no ${TARGET.label} target — skipped`);
+ continue;
+ }
+
+ walkAndCopy(
+ entryPath,
+ join(ROOT, destination),
+ moduleDir,
+ addedFiles,
+ force,
+ TARGET.transforms[entry],
+ );
+ }
+
+ // Merge MCP fragments authored for this harness.
+ const mcpServers = mergeMcpFragments(fragment, summary);
+
+ // Record in installed.json
+ const timestamp = new Date().toISOString();
+ const record = {
+ version: mf.version || '0.1.0',
+ status: 'installed',
+ timestamp,
+ files: addedFiles,
+ mcpByHarness: { ...(installed[name]?.mcpByHarness || {}), [HARNESS]: mcpServers },
+ };
+ if (HARNESS === 'opencode') record.mcp = mcpServers;
+
+ installed[name] = record;
+ writeInstalled(installed);
+
+ summary.added++;
+ log('OK', `Module '${name}' v${record.version} installed (${addedFiles.length} files, ${mcpServers.length} MCP servers)`);
+ return true;
+}
+
+function walkAndCopy(srcDir, destDir, moduleDir, fileList, force = false, spec = null) {
+ if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
+
const entries = readdirSync(srcDir);
for (const entry of entries) {
const srcPath = join(srcDir, entry);
- const destPath = join(destDir, entry);
const stat = statSync(srcPath);
if (stat.isDirectory()) {
- if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
- walkAndCopy(srcPath, destPath, fileList, force);
+ walkAndCopy(srcPath, join(destDir, entry), moduleDir, fileList, force, spec);
+ continue;
+ }
+
+ const source = toForward(relative(moduleDir, srcPath));
+ const outputName = spec?.extension
+ ? entry.replace(/\.[^.]+$/, spec.extension)
+ : entry;
+ const destPath = join(destDir, outputName);
+ const target = toForward(relative(ROOT, destPath));
+
+ let output = null;
+ if (spec?.transform) {
+ output = spec.transform(readFileSync(srcPath, 'utf-8'), entry);
+ if (output === null) {
+ log('SKIP', `${source} — not applicable to ${TARGET.label}`);
+ continue;
+ }
+ }
+
+ if (existsSync(destPath)) {
+ const same = output !== null
+ ? readFileSync(destPath, 'utf-8') === output
+ : filesMatch(srcPath, destPath);
+
+ if (!force) {
+ log('SKIP', `${source} → ${target}`);
+ continue;
+ }
+
+ if (same) {
+ log('SAME', source);
+ } else if (output !== null) {
+ writeFileSync(destPath, output);
+ log('UPDATE', `${source} → ${target}`);
+ } else {
+ copyFileSync(srcPath, destPath);
+ log('UPDATE', `${source} → ${target}`);
+ }
+ fileList.push({ source, target });
} else {
- if (existsSync(destPath)) {
- if (force) {
- if (filesMatch(srcPath, destPath)) {
- log('SAME', `${toForward(relative(MODULES_DIR, srcPath))}`);
- } else {
- copyFileSync(srcPath, destPath);
- log('UPDATE', `${toForward(relative(MODULES_DIR, srcPath))} → ${toForward(relative(ROOT, destPath))}`);
- }
- fileList.push(toForward(relative(join(ROOT, HARNESS_TARGET), destPath)));
- } else {
- log('SKIP', `${toForward(relative(MODULES_DIR, srcPath))} → ${toForward(relative(ROOT, destPath))}`);
- }
+ if (output !== null) {
+ writeFileSync(destPath, output);
} else {
copyFileSync(srcPath, destPath);
- log('ADD', `${toForward(relative(MODULES_DIR, srcPath))} → ${toForward(relative(ROOT, destPath))}`);
- fileList.push(toForward(relative(join(ROOT, HARNESS_TARGET), destPath)));
}
+ log('ADD', `${source} → ${target}`);
+ fileList.push({ source, target });
}
}
}
@@ -287,6 +321,53 @@ function filesMatch(a, b) {
return Buffer.compare(readFileSync(a), readFileSync(b)) === 0;
}
+function findMcpFragment(moduleDir) {
+ const mcpDir = join(moduleDir, 'mcp');
+ if (!existsSync(mcpDir) || !statSync(mcpDir).isDirectory()) return null;
+
+ const perHarness = join(mcpDir, `${HARNESS}.json`);
+ if (existsSync(perHarness)) return perHarness;
+
+ // Legacy fragments (`mcp/.json`) were OpenCode-only.
+ if (HARNESS === 'opencode') {
+ const legacy = readdirSync(mcpDir)
+ .filter(file => file.endsWith('.json') && file !== 'opencode.json')
+ .map(file => join(mcpDir, file));
+ if (legacy.length === 1) {
+ log('WARN', `Using legacy MCP fragment '${toForward(relative(ROOT, legacy[0]))}' for OpenCode`);
+ return legacy[0];
+ }
+ if (legacy.length > 1) {
+ log('WARN', `Multiple legacy MCP fragments in ${toForward(relative(ROOT, mcpDir))} — none used`);
+ }
+ }
+
+ return null;
+}
+
+function mergeMcpFragments(fragment, summary) {
+ if (!fragment) return [];
+
+ const targetPath = join(ROOT, TARGET.mcp.file);
+ const config = loadMcpTarget(TARGET.mcp.kind, targetPath);
+ const merged = [];
+
+ for (const [serverName, entry] of Object.entries(fragment)) {
+ if (config.has(serverName)) {
+ log('WARN', `MCP server '${serverName}' already exists in ${TARGET.mcp.file} — skipped`);
+ summary.mcpSkipped++;
+ continue;
+ }
+ config.merge(serverName, entry);
+ log('ADD', `MCP server '${serverName}' → ${TARGET.mcp.file}`);
+ merged.push(serverName);
+ summary.mcpAdded++;
+ }
+
+ if (merged.length > 0) config.save();
+ return merged;
+}
+
// ── remove command ─────────────────────────────────────────────────────────
function cmdRemove(name) {
@@ -299,7 +380,7 @@ function cmdRemove(name) {
const installed = readInstalled();
if (!installed[name]) {
- console.error(`Module '${name}' is not in installed.json. Nothing to remove.`);
+ console.error(`Module '${name}' is not in ${TARGET.stateFile}. Nothing to remove.`);
process.exit(1);
}
@@ -320,87 +401,105 @@ function cmdRemove(name) {
process.exit(1);
}
- wrapWithHeader(`Removing module: ${name}`, () => {
- const entry = installed[name];
- const files = entry.files || [];
- const mcpServers = entry.mcp || [];
-
- let delCount = 0, keepCount = 0;
+ wrapWithHeader(`Removing module: ${name} (${TARGET.label})`, () => {
+ try {
+ removeOne(name, installed);
+ } catch (error) {
+ console.error(` [ERR] ${error.message}`);
+ }
+ });
- // Remove tracked files
- for (const f of files) {
- const installedPath = join(ROOT, HARNESS_TARGET, f);
- const sourcePath = join(MODULES_DIR, name, f);
+ runValidation();
+}
- if (!existsSync(installedPath)) {
- log('SKIP', `${f} — not found on disk`);
- continue;
- }
+function removeOne(name, installed) {
+ const entry = installed[name];
+ const files = entry.files || [];
+ const mcpServers = entry.mcpByHarness?.[HARNESS]
+ ?? (HARNESS === 'opencode' ? entry.mcp || [] : []);
+ const moduleDir = join(MODULES_DIR, name);
+
+ // Parse the MCP config before deleting anything, so a malformed config
+ // aborts the removal instead of leaving tracked files deleted.
+ const mcpConfig = mcpServers.length > 0
+ ? loadMcpTarget(TARGET.mcp.kind, join(ROOT, TARGET.mcp.file))
+ : null;
+
+ let delCount = 0, keepCount = 0;
+
+ // Remove tracked files
+ for (const record of files) {
+ const target = installedRecordTarget(record);
+ const source = installedRecordSource(record);
+ const installedPath = join(ROOT, target);
+ const sourcePath = join(moduleDir, source);
+
+ if (!existsSync(installedPath)) {
+ log('SKIP', `${target} — not found on disk`);
+ continue;
+ }
- if (existsSync(sourcePath)) {
- const installedContent = readFileSync(installedPath);
- const sourceContent = readFileSync(sourcePath);
- if (Buffer.compare(installedContent, sourceContent) === 0) {
- unlinkSync(installedPath);
- log('DEL', f);
- delCount++;
- } else {
- log('KEEP', `${f} — modified by user`);
- keepCount++;
- }
+ if (existsSync(sourcePath)) {
+ const top = source.split('/')[0];
+ const spec = TARGET.transforms[top];
+ const sourceContent = readFileSync(sourcePath, 'utf-8');
+ const expected = spec?.transform
+ ? spec.transform(sourceContent, basename(sourcePath))
+ : null;
+ const unchanged = expected !== null
+ ? readFileSync(installedPath, 'utf-8') === expected
+ : Buffer.compare(readFileSync(installedPath), readFileSync(sourcePath)) === 0;
+
+ if (unchanged) {
+ unlinkSync(installedPath);
+ log('DEL', target);
+ delCount++;
} else {
- log('KEEP', `${f} — source not found, keeping`);
+ log('KEEP', `${target} — modified by user`);
keepCount++;
}
+ } else {
+ log('KEEP', `${target} — source not found, keeping`);
+ keepCount++;
}
+ }
- // Clean up empty parent directories
- const parentDirs = new Set();
- for (const f of files) {
- let dir = dirname(join(ROOT, HARNESS_TARGET, f));
- while (dir.startsWith(join(ROOT, HARNESS_TARGET))) {
- parentDirs.add(dir);
- dir = dirname(dir);
- }
- }
- const sortedDirs = [...parentDirs].sort((a, b) => b.length - a.length);
- for (const d of sortedDirs) {
- if (!existsSync(d)) continue;
- try {
- const remaining = readdirSync(d);
- if (remaining.length === 0) {
- rmSync(d, { force: true });
- }
- } catch { /* skip */ }
+ // Clean up empty parent directories
+ const parentDirs = new Set();
+ for (const record of files) {
+ const target = join(ROOT, installedRecordTarget(record));
+ let dir = dirname(target);
+ while (dir.startsWith(ROOT) && dir !== ROOT) {
+ parentDirs.add(dir);
+ dir = dirname(dir);
}
+ }
+ const sortedDirs = [...parentDirs].sort((a, b) => b.length - a.length);
+ for (const dir of sortedDirs) {
+ if (!existsSync(dir)) continue;
+ try {
+ const remaining = readdirSync(dir);
+ if (remaining.length === 0) rmSync(dir, { force: true });
+ } catch { /* skip */ }
+ }
- // Remove MCP servers from opencode.json (OpenCode-only)
- if (mcpServers.length > 0 && HARNESS !== 'pi') {
- const ocfg = readJSON(OPENCODE_JSON);
- if (ocfg && ocfg.mcp && ocfg.mcp.servers) {
- for (const serverName of mcpServers) {
- if (ocfg.mcp.servers[serverName]) {
- delete ocfg.mcp.servers[serverName];
- log('DEL', `MCP server '${serverName}' from opencode.json`);
- }
- }
- // Clean up empty servers and mcp sections
- if (Object.keys(ocfg.mcp.servers).length === 0) delete ocfg.mcp.servers;
- if (Object.keys(ocfg.mcp).length === 0) delete ocfg.mcp;
- writeJSON(OPENCODE_JSON, ocfg);
- }
+ // Remove MCP servers from this harness's config
+ let mcpRemoved = 0;
+ if (mcpConfig) {
+ for (const serverName of mcpServers) {
+ if (!mcpConfig.has(serverName)) continue;
+ mcpConfig.remove(serverName);
+ log('DEL', `MCP server '${serverName}' from ${TARGET.mcp.file}`);
+ mcpRemoved++;
}
+ mcpConfig.save();
+ }
- // Remove from installed.json
- delete installed[name];
- writeInstalled(installed);
- log('OK', `Module '${name}' removed from installed.json`);
-
- console.log(`\n Removed: ${delCount} files, Kept: ${keepCount} files, MCP removed: ${mcpServers.length}`);
- });
+ delete installed[name];
+ writeInstalled(installed);
+ log('OK', `Module '${name}' removed from ${TARGET.stateFile}`);
- // Post-remove validation
- runValidation();
+ console.log(`\n Removed: ${delCount} files, Kept: ${keepCount} files, MCP removed: ${mcpRemoved}`);
}
// ── Validation ─────────────────────────────────────────────────────────────
@@ -423,25 +522,31 @@ function runValidation() {
// ── Main dispatch ──────────────────────────────────────────────────────────
function main() {
- const args = process.argv.slice(2);
-
- // Parse --pi flag (can appear anywhere in args)
- const piIndex = args.indexOf('--pi');
- if (piIndex !== -1) {
- setHarness('pi');
- args.splice(piIndex, 1);
+ let args;
+ try {
+ const parsed = parseHarnessArgs(process.argv.slice(2));
+ setHarness(parsed.harness);
+ args = parsed.rest;
+ } catch (error) {
+ console.error(error.message);
+ process.exit(1);
}
if (args.length === 0) {
const scriptPath = '.opencode/modules/install.mjs';
console.log('Usage:');
- console.log(` node ${scriptPath} list [--pi]`);
- console.log(` node ${scriptPath} info [--pi]`);
- console.log(` node ${scriptPath} add [--pi]`);
- console.log(` node ${scriptPath} remove [--pi]`);
+ console.log(` node ${scriptPath} list [--harness ]`);
+ console.log(` node ${scriptPath} info [--harness ]`);
+ console.log(` node ${scriptPath} add [--harness ] [--force]`);
+ console.log(` node ${scriptPath} remove [--harness ]`);
console.log('');
console.log('Options:');
- console.log(' --pi Install to .agents/ instead of .opencode/ (Pi harness)');
+ console.log(` --harness Target harness: ${HARNESS_IDS.join(', ')} (default: opencode)`);
+ console.log(' --pi Shorthand for --harness pi');
+ console.log(' --omp Shorthand for --harness omp');
+ console.log(' --claude Shorthand for --harness claude');
+ console.log(' --codex Shorthand for --harness codex');
+ console.log(' --force Overwrite existing files and refresh the install record');
process.exit(0);
}
diff --git a/package.json b/package.json
index 70daa74..740562d 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"test:omp": "node --import tsx --test tests/omp/*.mjs",
"test:hooks": "node --test tests/git-hooks/test-commit-msg.mjs tests/git-hooks/test-pre-commit.mjs",
"test:framework": "node tests/agents/validate.mjs",
+ "test:modules": "node --test tests/modules/test-install-harness.mjs",
"test:parity": "node --import tsx --test tests/e2e/test-parity.test.ts"
},
"repository": {
diff --git a/tests/modules/test-install-harness.mjs b/tests/modules/test-install-harness.mjs
new file mode 100644
index 0000000..cdd7ff8
--- /dev/null
+++ b/tests/modules/test-install-harness.mjs
@@ -0,0 +1,375 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ mkdtempSync,
+ mkdirSync,
+ writeFileSync,
+ readFileSync,
+ existsSync,
+ cpSync,
+ rmSync,
+ readdirSync,
+} from 'node:fs';
+import { join, dirname } from 'node:path';
+import { tmpdir } from 'node:os';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+
+import {
+ HARNESS_IDS,
+ HARNESS_TARGETS,
+ parseHarnessArgs,
+ claudeAgentMarkdown,
+ codexAgentToml,
+ ompAgentMarkdown,
+} from '../../tools/lib/harness-targets.mjs';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const REPO = join(__dirname, '..', '..');
+const INSTALLER = join(REPO, '.opencode', 'modules', 'install.mjs');
+
+const AGENT_MD = `---
+description: "Demo agent for installer tests."
+maxTurns: 10
+---
+
+You are the Demo Agent. Follow the test instructions.
+`;
+
+const MODULEFILE = `name: demo
+version: "1.0.0"
+description: "Installer test module."
+depends: []
+`;
+
+function harnessFragment(harness) {
+ const args = ['--directory', 'tools/aseprite-mcp', 'run', '-m', 'aseprite_mcp'];
+ switch (harness) {
+ case 'opencode':
+ return {
+ demo: {
+ type: 'local',
+ command: ['uv', ...args],
+ codemode: false,
+ environment: { ASEPRITE_PATH: '{env:ASEPRITE_PATH}' },
+ },
+ };
+ case 'pi':
+ return {
+ demo: {
+ command: 'uv',
+ args,
+ env: { ASEPRITE_PATH: '${ASEPRITE_PATH}' },
+ exposure: 'direct',
+ },
+ };
+ case 'omp':
+ return {
+ demo: {
+ type: 'stdio',
+ command: 'uv',
+ args,
+ env: { ASEPRITE_PATH: '${ASEPRITE_PATH}' },
+ },
+ };
+ case 'claude':
+ return {
+ demo: {
+ command: 'uv',
+ args,
+ env: { ASEPRITE_PATH: '${ASEPRITE_PATH}' },
+ },
+ };
+ case 'codex':
+ return {
+ demo: {
+ command: 'uv',
+ args,
+ env_vars: ['ASEPRITE_PATH'],
+ enabled: true,
+ },
+ };
+ default:
+ throw new Error(`Unknown harness ${harness}`);
+ }
+}
+
+function writeModule(root, modulesRoot) {
+ const dir = join(root, modulesRoot, 'demo');
+ mkdirSync(join(dir, 'agents'), { recursive: true });
+ mkdirSync(join(dir, 'skills', 'demo'), { recursive: true });
+ mkdirSync(join(dir, 'commands'), { recursive: true });
+ mkdirSync(join(dir, 'rules'), { recursive: true });
+ mkdirSync(join(dir, 'plugins'), { recursive: true });
+ mkdirSync(join(dir, 'docs'), { recursive: true });
+ mkdirSync(join(dir, 'mcp'), { recursive: true });
+
+ writeFileSync(join(dir, 'modulefile.yaml'), MODULEFILE);
+ writeFileSync(join(dir, 'agents', 'demo-agent.md'), AGENT_MD);
+ writeFileSync(join(dir, 'skills', 'demo', 'SKILL.md'), '---\nname: demo\ndescription: "Demo skill."\n---\n\nDemo body.\n');
+ writeFileSync(join(dir, 'commands', 'demo.md'), '---\nname: demo\ndescription: "Demo command."\n---\n\nDemo command.\n');
+ writeFileSync(join(dir, 'rules', 'demo-rule.md'), '---\npaths:\n - "src/**"\n---\n\n# Demo rule\n');
+ writeFileSync(join(dir, 'plugins', 'demo.ts'), 'export const demo = true;\n');
+ writeFileSync(join(dir, 'docs', 'demo.md'), '# Demo docs\n');
+
+ for (const harness of HARNESS_IDS) {
+ writeFileSync(
+ join(dir, 'mcp', `${harness}.json`),
+ JSON.stringify(harnessFragment(harness), null, 2) + '\n',
+ );
+ }
+}
+
+function makeFixture() {
+ const root = mkdtempSync(join(tmpdir(), 'ocgs-harness-'));
+ mkdirSync(join(root, 'tools', 'lib'), { recursive: true });
+ mkdirSync(join(root, '.opencode', 'modules'), { recursive: true });
+ mkdirSync(join(root, '.agents', 'modules'), { recursive: true });
+
+ cpSync(join(REPO, 'tools', 'lib', 'parse-modulefile.mjs'), join(root, 'tools', 'lib', 'parse-modulefile.mjs'));
+ cpSync(join(REPO, 'tools', 'lib', 'harness-targets.mjs'), join(root, 'tools', 'lib', 'harness-targets.mjs'));
+ cpSync(join(REPO, 'tools', 'lib', 'mcp-targets.mjs'), join(root, 'tools', 'lib', 'mcp-targets.mjs'));
+ cpSync(join(REPO, '.opencode', 'modules', 'install.mjs'), join(root, '.opencode', 'modules', 'install.mjs'));
+
+ writeModule(root, '.opencode/modules');
+ writeModule(root, '.agents/modules');
+
+ writeFileSync(join(root, 'opencode.json'), JSON.stringify({
+ mcp: { servers: { existing: { type: 'local', command: ['keep'] } } },
+ permissions: [],
+ }, null, 2) + '\n');
+
+ mkdirSync(join(root, '.omp'), { recursive: true });
+ writeFileSync(join(root, '.omp', 'mcp.json'), JSON.stringify({
+ mcpServers: { existing: { type: 'stdio', command: 'keep' } },
+ }, null, 2) + '\n');
+
+ mkdirSync(join(root, '.codex'), { recursive: true });
+ writeFileSync(join(root, '.codex', 'config.toml'), '# keep this comment\n\n[agents]\nmax_concurrent_threads_per_session = 4\n');
+
+ return root;
+}
+
+function runInstaller(root, args) {
+ return spawnSync(process.execPath, [join(root, '.opencode', 'modules', 'install.mjs'), ...args], {
+ cwd: root,
+ encoding: 'utf-8',
+ });
+}
+
+function readJson(file) {
+ return JSON.parse(readFileSync(file, 'utf-8'));
+}
+
+test('parseHarnessArgs defaults to opencode and consumes aliases', () => {
+ assert.deepEqual(parseHarnessArgs(['list']), { harness: 'opencode', rest: ['list'] });
+ assert.deepEqual(parseHarnessArgs(['list', '--pi']), { harness: 'pi', rest: ['list'] });
+ assert.deepEqual(
+ parseHarnessArgs(['add', 'core', '--harness', 'codex', '--force']),
+ { harness: 'codex', rest: ['add', 'core', '--force'] },
+ );
+ assert.throws(() => parseHarnessArgs(['list', '--harness', 'nope']), /Unknown harness/);
+});
+
+test('every harness installs its own content and MCP file', async (t) => {
+ const cases = {
+ opencode: {
+ file: '.opencode/skills/demo/SKILL.md',
+ mcp: 'opencode.json',
+ check: (root) => {
+ const config = readJson(join(root, 'opencode.json'));
+ assert.equal(config.mcp.servers.demo.type, 'local');
+ assert.equal(config.mcp.servers.existing.command[0], 'keep');
+ assert.ok(existsSync(join(root, '.opencode/agents/demo-agent.md')));
+ assert.ok(existsSync(join(root, '.opencode/plugins/demo.ts')));
+ },
+ },
+ pi: {
+ file: '.agents/skills/demo/SKILL.md',
+ mcp: '.pi/mcp.json',
+ check: (root) => {
+ const config = readJson(join(root, '.pi/mcp.json'));
+ assert.equal(config.mcpServers.demo.exposure, 'direct');
+ assert.ok(existsSync(join(root, '.agents/agents/demo-agent.md')));
+ },
+ },
+ omp: {
+ file: '.agents/skills/demo/SKILL.md',
+ mcp: '.omp/mcp.json',
+ check: (root) => {
+ const config = readJson(join(root, '.omp/mcp.json'));
+ assert.equal(config.mcpServers.demo.type, 'stdio');
+ assert.equal(config.mcpServers.existing.command, 'keep');
+
+ const agent = readFileSync(join(root, '.omp/agents/demo-agent.md'), 'utf-8');
+ assert.match(agent, /^name: demo-agent$/m);
+ assert.match(agent, /^description: "Demo agent for installer tests\."$/m);
+ assert.match(agent, /Follow the test instructions\./);
+ assert.doesNotMatch(agent, /^maxTurns:/m);
+ assert.ok(!existsSync(join(root, '.agents/agents/demo-agent.md')));
+ },
+ },
+ claude: {
+ file: '.claude/skills/demo/SKILL.md',
+ mcp: '.mcp.json',
+ check: (root) => {
+ const config = readJson(join(root, '.mcp.json'));
+ assert.ok(config.mcpServers.demo);
+ const agent = readFileSync(join(root, '.claude/agents/demo-agent.md'), 'utf-8');
+ assert.match(agent, /^name: demo-agent$/m);
+ assert.ok(existsSync(join(root, '.claude/commands/demo.md')));
+ assert.ok(existsSync(join(root, '.claude/rules/demo-rule.md')));
+ },
+ },
+ codex: {
+ file: '.agents/skills/demo/SKILL.md',
+ mcp: '.codex/config.toml',
+ check: (root) => {
+ const toml = readFileSync(join(root, '.codex/config.toml'), 'utf-8');
+ assert.match(toml, /# keep this comment/);
+ assert.match(toml, /\[agents\]/);
+ assert.match(toml, /\[mcp_servers\.demo\]/);
+ assert.match(toml, /env_vars = \["ASEPRITE_PATH"\]/);
+ const agent = readFileSync(join(root, '.codex/agents/demo-agent.toml'), 'utf-8');
+ assert.match(agent, /^name = "demo-agent"$/m);
+ assert.match(agent, /^developer_instructions = """$/m);
+ assert.ok(!existsSync(join(root, '.codex/commands')));
+ },
+ },
+ };
+
+ for (const [harness, spec] of Object.entries(cases)) {
+ await t.test(harness, () => {
+ const root = makeFixture();
+ try {
+ const result = runInstaller(root, ['add', 'demo', '--harness', harness]);
+ assert.equal(result.status, 0, result.stderr || result.stdout);
+ assert.ok(existsSync(join(root, spec.file)), `${spec.file} missing`);
+ assert.ok(existsSync(join(root, spec.mcp)), `${spec.mcp} missing`);
+ spec.check(root);
+
+ const remove = runInstaller(root, ['remove', 'demo', '--harness', harness]);
+ assert.equal(remove.status, 0, remove.stderr || remove.stdout);
+ assert.ok(!existsSync(join(root, spec.file)), `${spec.file} not removed`);
+ if (harness === 'omp') {
+ assert.ok(!existsSync(join(root, '.omp/agents/demo-agent.md')), 'omp agent not removed');
+ }
+ if (harness === 'codex') {
+ const toml = readFileSync(join(root, '.codex/config.toml'), 'utf-8');
+ assert.doesNotMatch(toml, /\[mcp_servers\.demo\]/);
+ assert.match(toml, /\[agents\]/);
+ }
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ });
+ }
+});
+
+test('remove keeps user-modified files', () => {
+ const root = makeFixture();
+ try {
+ assert.equal(runInstaller(root, ['add', 'demo', '--harness', 'claude']).status, 0);
+ const skill = join(root, '.claude/skills/demo/SKILL.md');
+ writeFileSync(skill, 'user changed this\n');
+
+ assert.equal(runInstaller(root, ['remove', 'demo', '--harness', 'claude']).status, 0);
+ assert.ok(existsSync(skill), 'modified file should be kept');
+ assert.equal(readFileSync(skill, 'utf-8'), 'user changed this\n');
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test('legacy string install records still remove', () => {
+ const root = makeFixture();
+ try {
+ assert.equal(runInstaller(root, ['add', 'demo', '--harness', 'pi']).status, 0);
+
+ const statePath = join(root, '.agents/modules/installed.json');
+ const state = readJson(statePath);
+ state.demo.files = ['skills/demo/SKILL.md', 'agents/demo-agent.md'];
+ writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
+
+ assert.equal(runInstaller(root, ['remove', 'demo', '--harness', 'pi']).status, 0);
+ assert.ok(!existsSync(join(root, '.agents/skills/demo/SKILL.md')));
+ assert.ok(!existsSync(join(root, '.agents/agents/demo-agent.md')));
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test('malformed MCP fragment aborts before copying files', () => {
+ const root = makeFixture();
+ try {
+ writeFileSync(join(root, '.agents/modules/demo/mcp/claude.json'), '{ not json');
+ const result = runInstaller(root, ['add', 'demo', '--harness', 'claude']);
+
+ assert.equal(result.status, 0);
+ assert.match(result.stdout, /Invalid JSON/);
+ assert.ok(!existsSync(join(root, '.claude/skills/demo/SKILL.md')));
+ assert.ok(!existsSync(join(root, '.claude/modules/installed.json')));
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test('unknown harness exits non-zero with a clear message', () => {
+ const root = makeFixture();
+ try {
+ const result = runInstaller(root, ['list', '--harness', 'nope']);
+ assert.equal(result.status, 1);
+ assert.match(result.stderr, /Unknown harness 'nope'/);
+ assert.match(result.stderr, /opencode, pi, omp, claude, codex/);
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test('claude transform injects name only when missing', () => {
+ const withName = '---\nname: x\ndescription: "d"\n---\n\nbody\n';
+ assert.equal(claudeAgentMarkdown(withName, 'x.md'), withName);
+ const withoutName = '---\ndescription: "d"\n---\n\nbody\n';
+ assert.match(claudeAgentMarkdown(withoutName, 'x.md'), /^name: x$/m);
+});
+
+test('omp transform emits required frontmatter and drops maxTurns', () => {
+ const md = ompAgentMarkdown(AGENT_MD, 'demo-agent.md');
+ assert.match(md, /^name: demo-agent$/m);
+ assert.match(md, /^description: "Demo agent for installer tests\."$/m);
+ assert.doesNotMatch(md, /^maxTurns:/m);
+ assert.match(md, /Follow the test instructions\./);
+});
+
+test('codex transform emits required TOML fields', () => {
+ const toml = codexAgentToml(AGENT_MD, 'demo-agent.md');
+ assert.match(toml, /^name = "demo-agent"$/m);
+ assert.match(toml, /^description = "Demo agent for installer tests\."$/m);
+ assert.match(toml, /^developer_instructions = """$/m);
+ assert.match(toml, /Follow the test instructions\./);
+});
+
+test('registry declares a target and MCP file for every harness', () => {
+ for (const id of HARNESS_IDS) {
+ const target = HARNESS_TARGETS[id];
+ assert.equal(target.id, id);
+ assert.ok(target.sourceRoot);
+ assert.ok(target.stateFile);
+ assert.ok(target.mcp.file);
+ }
+});
+
+test('art MCP fragments are mirrored in both module source trees', () => {
+ const canonical = join(REPO, '.agents', 'modules', 'art', 'mcp');
+ const mirror = join(REPO, '.opencode', 'modules', 'art', 'mcp');
+ const files = readdirSync(canonical).sort();
+
+ assert.deepEqual(files, HARNESS_IDS.map((id) => `${id}.json`).sort());
+ for (const file of files) {
+ assert.equal(
+ readFileSync(join(mirror, file), 'utf-8'),
+ readFileSync(join(canonical, file), 'utf-8'),
+ `${file} differs between module source trees`,
+ );
+ }
+});
diff --git a/tools/lib/harness-targets.mjs b/tools/lib/harness-targets.mjs
new file mode 100644
index 0000000..dc26fb5
--- /dev/null
+++ b/tools/lib/harness-targets.mjs
@@ -0,0 +1,263 @@
+/**
+ * Harness targets for the OCGS module installer.
+ *
+ * One entry per coding-agent harness. Each entry says where module content is
+ * installed, where the install state lives, and which file receives MCP
+ * fragments. Content stays canonical in `.agents/`; harnesses that cannot read
+ * it directly get an install target.
+ *
+ * Adding a harness here is the only place the installer learns about it.
+ */
+
+export const HARNESS_IDS = ['opencode', 'pi', 'omp', 'claude', 'codex'];
+
+/** Convenience flags kept for backward compatibility (`--pi`) and symmetry. */
+export const HARNESS_ALIASES = {
+ '--opencode': 'opencode',
+ '--pi': 'pi',
+ '--omp': 'omp',
+ '--claude': 'claude',
+ '--codex': 'codex',
+};
+
+/**
+ * Module subdirectories that each harness can consume.
+ *
+ * `null`/missing means the harness has no equivalent; the installer reports it
+ * and skips the directory instead of writing inert files.
+ */
+export const HARNESS_TARGETS = {
+ opencode: {
+ id: 'opencode',
+ label: 'OpenCode',
+ sourceRoot: '.opencode/modules',
+ contentRoot: '.opencode',
+ stateFile: '.opencode/modules/installed.json',
+ dirs: {
+ agents: '.opencode/agents',
+ skills: '.opencode/skills',
+ commands: '.opencode/commands',
+ rules: '.opencode/rules',
+ plugins: '.opencode/plugins',
+ docs: '.opencode/docs',
+ },
+ transforms: {},
+ mcp: { kind: 'opencode-json', file: 'opencode.json' },
+ limitations: [],
+ },
+
+ pi: {
+ id: 'pi',
+ label: 'Pi',
+ sourceRoot: '.agents/modules',
+ contentRoot: '.agents',
+ stateFile: '.agents/modules/installed.json',
+ dirs: {
+ agents: '.agents/agents',
+ skills: '.agents/skills',
+ commands: '.agents/commands',
+ rules: '.agents/rules',
+ docs: '.agents/docs',
+ },
+ transforms: {},
+ mcp: { kind: 'mcp-servers-json', file: '.pi/mcp.json' },
+ limitations: ['Project MCP config loads only after the project is trusted.'],
+ },
+
+ omp: {
+ id: 'omp',
+ label: 'oh-my-pi (omp)',
+ sourceRoot: '.agents/modules',
+ contentRoot: '.agents',
+ stateFile: '.omp/modules/installed.json',
+ dirs: {
+ agents: '.omp/agents',
+ skills: '.agents/skills',
+ commands: '.agents/commands',
+ rules: '.agents/rules',
+ docs: '.agents/docs',
+ },
+ transforms: {
+ // omp reads project agents from `.omp/agents/`; it does not scan
+ // `.agents/agents/`. Generate definitions with omp's required fields.
+ agents: { transform: ompAgentMarkdown },
+ },
+ mcp: { kind: 'mcp-servers-json', file: '.omp/mcp.json' },
+ limitations: [
+ 'Generated agent definitions omit tool restrictions; they inherit the session tool set.',
+ ],
+ },
+
+ claude: {
+ id: 'claude',
+ label: 'Claude Code',
+ sourceRoot: '.agents/modules',
+ contentRoot: '.claude',
+ stateFile: '.claude/modules/installed.json',
+ dirs: {
+ agents: '.claude/agents',
+ skills: '.claude/skills',
+ commands: '.claude/commands',
+ rules: '.claude/rules',
+ },
+ transforms: {
+ // Claude Code requires `name` in subagent frontmatter.
+ agents: { transform: claudeAgentMarkdown },
+ },
+ mcp: { kind: 'mcp-servers-json', file: '.mcp.json' },
+ limitations: ['Project .mcp.json servers need approval in an interactive session.'],
+ },
+
+ codex: {
+ id: 'codex',
+ label: 'Codex',
+ sourceRoot: '.agents/modules',
+ contentRoot: '.codex',
+ stateFile: '.codex/modules/installed.json',
+ dirs: {
+ // Codex reads skills from `.agents/skills` natively; only agents need a
+ // project-scoped target.
+ agents: '.codex/agents',
+ skills: '.agents/skills',
+ },
+ transforms: {
+ agents: { transform: codexAgentToml, extension: '.toml' },
+ },
+ mcp: { kind: 'codex-toml', file: '.codex/config.toml' },
+ limitations: [
+ 'Codex has no project-level slash commands; skills cover those workflows.',
+ 'Path-scoped rules do not map; Codex reads AGENTS.md instead.',
+ 'Project .codex/ config loads only after the project is trusted.',
+ ],
+ },
+};
+
+export function resolveHarness(name) {
+ const value = String(name || '').replace(/^--/, '').toLowerCase();
+ if (!HARNESS_IDS.includes(value)) {
+ throw new Error(
+ `Unknown harness '${name}'. Supported: ${HARNESS_IDS.join(', ')}`,
+ );
+ }
+ return HARNESS_TARGETS[value];
+}
+
+/**
+ * Pull `--harness ` and the convenience aliases out of argv.
+ * Returns the resolved harness id plus the remaining arguments.
+ */
+export function parseHarnessArgs(args) {
+ const rest = [];
+ let harness = 'opencode';
+
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ if (arg === '--harness') {
+ const value = args[++i];
+ if (!value) {
+ throw new Error(`--harness requires a value: ${HARNESS_IDS.join(', ')}`);
+ }
+ harness = value;
+ } else if (arg.startsWith('--harness=')) {
+ harness = arg.slice('--harness='.length);
+ } else if (HARNESS_ALIASES[arg]) {
+ harness = HARNESS_ALIASES[arg];
+ } else {
+ rest.push(arg);
+ }
+ }
+
+ return { harness: resolveHarness(harness).id, rest };
+}
+
+export function describeHarness(id) {
+ const target = HARNESS_TARGETS[id];
+ return `${target.label} (${target.sourceRoot} → ${Object.values(target.dirs).join(', ')})`;
+}
+
+// ── Content transforms ─────────────────────────────────────────────────────
+
+/**
+ * Claude Code requires `name` in subagent frontmatter. OCGS agents are named by
+ * filename, so inject it when absent and leave the rest of the file untouched.
+ */
+export function claudeAgentMarkdown(content, filename) {
+ if (/^---\r?\n/.test(content) && /^name:/m.test(content.split(/\r?\n---/)[0])) {
+ return content;
+ }
+ const name = filename.replace(/\.[^.]+$/, '');
+ return content.replace(/^---\r?\n/, `---\nname: ${name}\n`);
+}
+
+/**
+ * omp custom agents live in `.omp/agents/.md`. Only `name` and
+ * `description` are required; the body is the agent's instructions. OCGS agents
+ * omit harness-owned tool and model fields, so neither is emitted.
+ */
+export function ompAgentMarkdown(content, filename) {
+ const { frontmatter, body } = splitFrontmatter(content);
+ const meta = parseFrontmatter(frontmatter);
+ const name = meta.name || filename.replace(/\.[^.]+$/, '');
+ const description = meta.description || '';
+
+ return [
+ '---',
+ `name: ${name}`,
+ `description: ${JSON.stringify(description)}`,
+ '---',
+ '',
+ body.trim(),
+ '',
+ ].join('\n');
+}
+
+/**
+ * Codex custom agents are TOML files with `name`, `description`, and
+ * `developer_instructions`. The Markdown body becomes the instructions.
+ */
+export function codexAgentToml(content, filename) {
+ const { frontmatter, body } = splitFrontmatter(content);
+ const meta = parseFrontmatter(frontmatter);
+ const name = meta.name || filename.replace(/\.[^.]+$/, '');
+ const description = meta.description || '';
+
+ return [
+ `name = ${tomlString(name)}`,
+ `description = ${tomlString(description)}`,
+ `developer_instructions = ${tomlMultiline(body.trim())}`,
+ '',
+ ].join('\n');
+}
+
+function splitFrontmatter(content) {
+ const normalized = content.replace(/\r\n/g, '\n');
+ if (!normalized.startsWith('---\n')) return { frontmatter: '', body: normalized };
+ const end = normalized.indexOf('\n---', 4);
+ if (end === -1) return { frontmatter: '', body: normalized };
+ return {
+ frontmatter: normalized.slice(4, end),
+ body: normalized.slice(end + 4).replace(/^\n/, ''),
+ };
+}
+
+function parseFrontmatter(raw) {
+ const out = {};
+ for (const line of raw.split('\n')) {
+ const match = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
+ if (!match) continue;
+ const value = match[2].trim();
+ out[match[1]] = /^(['"]).*\1$/.test(value) ? value.slice(1, -1) : value;
+ }
+ return out;
+}
+
+function tomlString(value) {
+ return JSON.stringify(String(value));
+}
+
+function tomlMultiline(value) {
+ const escaped = String(value)
+ .replace(/\\/g, '\\\\')
+ .replace(/"""/g, '\\"\\"\\"');
+ return `"""\n${escaped}\n"""`;
+}
From def338e83e6c1aed853d8c5bbeee8404268479d0 Mon Sep 17 00:00:00 2001
From: Jaco du Preez
Date: Wed, 4 Nov 2026 20:52:55 +0200
Subject: [PATCH 4/6] feat(skills): select a harness during template init
Add a harness question and --harness argument, harness-aware module
installation, config validation, Windows symlink repair, and opt-in
pruning of unused harness directories.
Refs: docs/framework/setup-requirements.md
---
.../core/skills/init-template/SKILL.md | 143 ++++++++++++++----
.agents/skills/init-template/SKILL.md | 143 ++++++++++++++----
.../core/skills/init-template/SKILL.md | 143 ++++++++++++++----
3 files changed, 342 insertions(+), 87 deletions(-)
diff --git a/.agents/modules/core/skills/init-template/SKILL.md b/.agents/modules/core/skills/init-template/SKILL.md
index ff0dfcd..d716d2c 100644
--- a/.agents/modules/core/skills/init-template/SKILL.md
+++ b/.agents/modules/core/skills/init-template/SKILL.md
@@ -1,7 +1,7 @@
---
name: init-template
-description: "First-time repo setup for new projects. Transforms the cloned OCGS template into a clean, ready-to-use game project with your own identity."
-argument-hint: "[--reset-git] [--name \"My Game\"] [--engine godot|unity|unreal]"
+description: "First-time repo setup for new projects. Transforms the cloned OCGS template into a clean, ready-to-use game project with your own identity and harness."
+argument-hint: "[--reset-git] [--name \"My Game\"] [--engine godot|unity|unreal|sfml3|raylib|bevy] [--harness opencode|pi|omp|claude|codex]"
user-invocable: true
allowed-tools: Read, Glob, Grep, Write, Edit, Bash, question, Task
---
@@ -13,10 +13,11 @@ When this skill is invoked:
Check if CLI arguments were passed (from `argument-hint`):
- `--name "My Game"` → sets game name (skips the name question below)
-- `--engine godot|unity|unreal` → sets engine (skips engine question)
+- `--engine godot|unity|unreal|sfml3|raylib|bevy` → sets engine (skips engine question)
+- `--harness opencode|pi|omp|claude|codex` → sets harness (skips harness question)
- `--reset-git` → automatically offers git reset (skips the prompt)
-If `--name` and `--engine` are both provided, skip the interactive prompt entirely and proceed to Phase 2 using the provided values.
+If `--name`, `--engine`, and `--harness` are all provided, skip the interactive prompt entirely and proceed to Phase 2 using the provided values.
Otherwise, use `question` to gather missing details:
@@ -25,27 +26,44 @@ Otherwise, use `question` to gather missing details:
- **What is your game's one-line description?** (e.g., "A 2D platformer about a cat in space")
### Tab 2: Engine & Genre
-- **Which engine are you using?** (godot / unity / unreal)
+- **Which engine are you using?** (godot / unity / unreal / sfml3 / raylib / bevy)
- **What genre best describes your game?** (e.g., platformer, RPG, puzzle, FPS, strategy)
### Tab 3: Team
- **Team size** (solo / small 2-5 / medium 6-15 / large 16+)
-### Modular Installation
+### Tab 4: Agent Harness
+- **Prompt**: "Which agent harness will you use for this project?"
+- **Options**: `OpenCode` / `Pi` / `oh-my-pi (omp)` / `Claude Code` / `Codex`
-After setting project identity, install the desired framework modules:
+The template ships configuration for all five. The answer selects the module install target, the MCP config file, and the harness config this skill validates. It does not delete the others unless the user asks in Phase 7.
+
+| Harness | Content target | MCP file | Notes |
+|---------|----------------|----------|-------|
+| OpenCode | `.opencode/{agents,skills,commands,rules}` | `opencode.json` (`mcp.servers`) | Windows checkouts may need `node tools/fix-opencode-symlinks.mjs` |
+| Pi | `.agents/{agents,skills,commands,rules}` | `.pi/mcp.json` (`mcpServers`) | Project MCP loads only after the project is trusted |
+| omp | `.omp/agents` + `.agents/{skills,commands,rules}` | `.omp/mcp.json` (`mcpServers`) | Agents are generated into `.omp/agents/`; they inherit the session tool set |
+| Claude Code | `.claude/{agents,skills,commands,rules}` | `.mcp.json` (`mcpServers`) | Agents gain `name:` frontmatter; project MCP needs approval |
+| Codex | `.codex/agents` + `.agents/skills` | `.codex/config.toml` (`[mcp_servers.*]`) | Agents are converted to TOML; commands and path-rules do not map |
+
+## Phase 2: Install Modules
+
+Install the desired framework modules for the selected harness. Pass `--harness` on every command. `add` is idempotent: modules already present in the harness install state are skipped.
```bash
-# OpenCode target (default): .opencode/
-node .opencode/modules/install.mjs add core
-node .opencode/modules/install.mjs add engine-godot
-node .opencode/modules/install.mjs add art design ui qa release prototyping
+# Required
+node .opencode/modules/install.mjs add core --harness
-# Pi target: add --pi to install into .agents/
-node .opencode/modules/install.mjs add core --pi
+# Engine specialists (choose the module matching --engine)
+node .opencode/modules/install.mjs add engine-godot --harness # or engine-unity / engine-unreal / engine-sfml3 / engine-raylib / engine-bevy
+
+# Common starter set
+node .opencode/modules/install.mjs add art design ui qa release prototyping --harness
```
-Available modules (run `node .opencode/modules/install.mjs list` to see all):
+Use `--force` only to refresh files after an upgrade; it overwrites existing module files and refreshes the install record.
+
+Available modules (run `node .opencode/modules/install.mjs list --harness ` to see all):
| Module | Description |
|--------|-------------|
@@ -72,11 +90,18 @@ Available modules (run `node .opencode/modules/install.mjs list` to see all):
| `engine-bevy` | Bevy specialists |
| `data` | Data file conventions |
-## Phase 2: Replace README.md
+If a module has an MCP fragment for the selected harness, the installer merges it into that harness's MCP file. If the server already exists, it is left untouched and reported as skipped.
+
+Agent installation differs by harness: omp definitions are generated into
+`.omp/agents/`, Claude Code definitions gain the required `name:` frontmatter in
+`.claude/agents/`, and Codex definitions are converted to
+`.codex/agents/*.toml`. OpenCode and Pi use `.agents/agents/` content directly.
+
+## Phase 3: Replace README.md
Write a fresh README.md to the project root. Use a template structure like:
-```markdown
+````markdown
# [Game Name]
> [One-line description]
@@ -86,7 +111,7 @@ Built with [Engine] using [OpenCode Game Studios](https://github.com/striderZA/O
## Quick Start
```bash
-opencode
+[harness command]
```
Type `/start` for onboarding, or browse all skills with `/`.
@@ -105,26 +130,55 @@ Type `/start` for onboarding, or browse all skills with `/`.
## License
[Choose a license]
-```
+````
-Replace `[Game Name]`, `[One-line description]`, and `[Engine]` with the user's answers from Phase 1.
+Replace `[Game Name]`, `[One-line description]`, `[Engine]`, and `[harness command]` with the user's answers from Phase 1. Harness commands: `opencode`, `pi`, `omp`, `claude`, `codex`.
-## Phase 3: Update AGENTS.md
+## Phase 4: Update AGENTS.md
Read AGENTS.md and update:
- Set the engine to the user's choice by changing the `## Technology Stack` section
+- Add `- **Agent Harness**: [OpenCode 2.x | Pi 1.x | oh-my-pi 18.16.0+ | Claude Code | Codex]` to `## Technology Stack`
- Remove or update any project-specific settings
- If AGENTS.md is missing or malformed, warn and skip this phase
-## Phase 4: Update opencode.json
+## Phase 5: Configure the Selected Harness
+
+Read and validate only the config for the selected harness. If a file is missing or malformed, warn and skip that item.
+
+### OpenCode
+- Read `opencode.json`; remove internal-only plugin paths and keep `./.opencode/plugins/ccgs-hooks.ts` only if that file exists.
+- On Windows, check whether `.opencode/{agents,skills,commands,rules}` are directories. A git checkout without symlink support writes them as one-line text stubs. If any is a file, run:
+ ```bash
+ node tools/fix-opencode-symlinks.mjs
+ ```
+- Re-check that all four paths are directories before reporting success.
+
+### Pi
+- Read `.pi/settings.json`; ensure `"enableSkillCommands": true`.
+- MCP entries live in `.pi/mcp.json`. Tell the user that Pi reads this file only after the project is trusted.
-Read opencode.json and clean it up:
-- Remove any internal-only plugin paths
-- Set project name appropriately
-- Keep the ccgs-hooks.ts plugin reference only if the file actually exists: `if [ -f .opencode/plugins/ccgs-hooks.ts ]; then ...`
-- If opencode.json is missing or malformed, warn and skip this phase
+### oh-my-pi (omp)
+- Read `.omp/config.yml`; ensure the OCGS extensions are listed.
+- MCP entries live in `.omp/mcp.json`.
+- `add --harness omp` generates `.omp/agents/*.md` from `.agents/agents/`. Each definition carries `name` and `description` and inherits the session tool set.
+- Reload the agent hub with `Ctrl`+`R` in `/agents` after generation.
-## Phase 5: Remove Internal Files
+### Claude Code
+- Claude Code reads `AGENTS.md` natively from v2.1.277. For older versions, and as a harmless fallback, create `CLAUDE.md` at the project root containing only:
+ ```markdown
+ @AGENTS.md
+ ```
+- Do not create `CLAUDE.md` if one already exists.
+- MCP entries live in `.mcp.json`. Tell the user the first interactive session asks for approval.
+
+### Codex
+- Codex reads `AGENTS.md` natively; no instruction file is needed.
+- Custom agents live in `.codex/agents/*.toml`; `add --harness codex` generates them from `.agents/agents/`.
+- MCP entries live in `.codex/config.toml` under `[mcp_servers.]`. Codex loads project `.codex/` config only after the project is trusted.
+- Codex has no project-level slash commands and no path-scoped rule equivalent; skills and `AGENTS.md` carry those workflows.
+
+## Phase 6: Remove Internal Files
For each file/directory below, check existence first before deleting. If the directory already has user-created content, warn and skip rather than destroying it:
@@ -134,7 +188,37 @@ For each file/directory below, check existence first before deleting. If the dir
- Clear `production/` contents only if empty of user files: similar guard
- On any error (file locked, permission denied), warn and continue to next item
-## Phase 6: Optional Git Reset
+## Phase 7: Optional Harness Pruning
+
+Ask with `question`:
+
+- **Prompt**: "This project is set up for [harness]. Do you want to keep the other harness configs so you can switch later, or remove them?"
+- **Options**:
+ - `Keep all harness configs` — recommended; nothing is deleted.
+ - `Remove the other harness configs` — show the exact list first, then delete only after confirmation.
+ - `Show me the list first` — print the paths and ask again.
+
+Never delete these shared paths, whichever harness was selected:
+
+- `AGENTS.md`
+- `.agents/` (canonical content and module sources)
+- `.opencode/modules/` and `.opencode/plugins/` (module CLI, framework plugins, tests)
+- `.pi/extensions/` (shared by Pi and omp)
+- `tools/`, `tests/`, `docs/`, `src/`, `assets/`, `design/`, `production/`
+
+Prune list per selected harness:
+
+| Selected | Safe to remove |
+|----------|----------------|
+| OpenCode | `.pi/settings.json`, `.omp/config.yml`, `.omp/mcp.json`, `.omp/agents/`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| Pi | `.opencode/{agents,skills,commands,rules}`, `.omp/`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| omp | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| Claude Code | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.omp/`, `.codex/`, `.cursor/rules/` |
+| Codex | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.omp/`, `.claude/{agents,skills,commands,rules,modules}/`, `.cursor/rules/` |
+
+Show the resolved paths before deleting. Do not delete files that are not on this list. Remove empty parent directories only after their contents are gone.
+
+## Phase 8: Optional Git Reset
If the user selected `--reset-git` or agrees when prompted:
- Offer to reset git history to a single commit
@@ -144,7 +228,7 @@ If the user selected `--reset-git` or agrees when prompted:
- Delete all old tags (optional) — warn: if tags were previously pushed to remote, deletion requires `git push origin --delete ` for each one
- Force push if needed (warn: this rewrites remote history for anyone who has cloned this repo)
-## Phase 7: Summary
+## Phase 9: Summary
Print a completion summary:
@@ -154,6 +238,7 @@ Print a completion summary:
Project: [Game Name]
Engine: [Engine]
Team: [Size]
+ Harness: [Harness]
What's next:
- Run /setup-engine [engine] to configure your engine docs
diff --git a/.agents/skills/init-template/SKILL.md b/.agents/skills/init-template/SKILL.md
index ff0dfcd..d716d2c 100644
--- a/.agents/skills/init-template/SKILL.md
+++ b/.agents/skills/init-template/SKILL.md
@@ -1,7 +1,7 @@
---
name: init-template
-description: "First-time repo setup for new projects. Transforms the cloned OCGS template into a clean, ready-to-use game project with your own identity."
-argument-hint: "[--reset-git] [--name \"My Game\"] [--engine godot|unity|unreal]"
+description: "First-time repo setup for new projects. Transforms the cloned OCGS template into a clean, ready-to-use game project with your own identity and harness."
+argument-hint: "[--reset-git] [--name \"My Game\"] [--engine godot|unity|unreal|sfml3|raylib|bevy] [--harness opencode|pi|omp|claude|codex]"
user-invocable: true
allowed-tools: Read, Glob, Grep, Write, Edit, Bash, question, Task
---
@@ -13,10 +13,11 @@ When this skill is invoked:
Check if CLI arguments were passed (from `argument-hint`):
- `--name "My Game"` → sets game name (skips the name question below)
-- `--engine godot|unity|unreal` → sets engine (skips engine question)
+- `--engine godot|unity|unreal|sfml3|raylib|bevy` → sets engine (skips engine question)
+- `--harness opencode|pi|omp|claude|codex` → sets harness (skips harness question)
- `--reset-git` → automatically offers git reset (skips the prompt)
-If `--name` and `--engine` are both provided, skip the interactive prompt entirely and proceed to Phase 2 using the provided values.
+If `--name`, `--engine`, and `--harness` are all provided, skip the interactive prompt entirely and proceed to Phase 2 using the provided values.
Otherwise, use `question` to gather missing details:
@@ -25,27 +26,44 @@ Otherwise, use `question` to gather missing details:
- **What is your game's one-line description?** (e.g., "A 2D platformer about a cat in space")
### Tab 2: Engine & Genre
-- **Which engine are you using?** (godot / unity / unreal)
+- **Which engine are you using?** (godot / unity / unreal / sfml3 / raylib / bevy)
- **What genre best describes your game?** (e.g., platformer, RPG, puzzle, FPS, strategy)
### Tab 3: Team
- **Team size** (solo / small 2-5 / medium 6-15 / large 16+)
-### Modular Installation
+### Tab 4: Agent Harness
+- **Prompt**: "Which agent harness will you use for this project?"
+- **Options**: `OpenCode` / `Pi` / `oh-my-pi (omp)` / `Claude Code` / `Codex`
-After setting project identity, install the desired framework modules:
+The template ships configuration for all five. The answer selects the module install target, the MCP config file, and the harness config this skill validates. It does not delete the others unless the user asks in Phase 7.
+
+| Harness | Content target | MCP file | Notes |
+|---------|----------------|----------|-------|
+| OpenCode | `.opencode/{agents,skills,commands,rules}` | `opencode.json` (`mcp.servers`) | Windows checkouts may need `node tools/fix-opencode-symlinks.mjs` |
+| Pi | `.agents/{agents,skills,commands,rules}` | `.pi/mcp.json` (`mcpServers`) | Project MCP loads only after the project is trusted |
+| omp | `.omp/agents` + `.agents/{skills,commands,rules}` | `.omp/mcp.json` (`mcpServers`) | Agents are generated into `.omp/agents/`; they inherit the session tool set |
+| Claude Code | `.claude/{agents,skills,commands,rules}` | `.mcp.json` (`mcpServers`) | Agents gain `name:` frontmatter; project MCP needs approval |
+| Codex | `.codex/agents` + `.agents/skills` | `.codex/config.toml` (`[mcp_servers.*]`) | Agents are converted to TOML; commands and path-rules do not map |
+
+## Phase 2: Install Modules
+
+Install the desired framework modules for the selected harness. Pass `--harness` on every command. `add` is idempotent: modules already present in the harness install state are skipped.
```bash
-# OpenCode target (default): .opencode/
-node .opencode/modules/install.mjs add core
-node .opencode/modules/install.mjs add engine-godot
-node .opencode/modules/install.mjs add art design ui qa release prototyping
+# Required
+node .opencode/modules/install.mjs add core --harness
-# Pi target: add --pi to install into .agents/
-node .opencode/modules/install.mjs add core --pi
+# Engine specialists (choose the module matching --engine)
+node .opencode/modules/install.mjs add engine-godot --harness # or engine-unity / engine-unreal / engine-sfml3 / engine-raylib / engine-bevy
+
+# Common starter set
+node .opencode/modules/install.mjs add art design ui qa release prototyping --harness
```
-Available modules (run `node .opencode/modules/install.mjs list` to see all):
+Use `--force` only to refresh files after an upgrade; it overwrites existing module files and refreshes the install record.
+
+Available modules (run `node .opencode/modules/install.mjs list --harness ` to see all):
| Module | Description |
|--------|-------------|
@@ -72,11 +90,18 @@ Available modules (run `node .opencode/modules/install.mjs list` to see all):
| `engine-bevy` | Bevy specialists |
| `data` | Data file conventions |
-## Phase 2: Replace README.md
+If a module has an MCP fragment for the selected harness, the installer merges it into that harness's MCP file. If the server already exists, it is left untouched and reported as skipped.
+
+Agent installation differs by harness: omp definitions are generated into
+`.omp/agents/`, Claude Code definitions gain the required `name:` frontmatter in
+`.claude/agents/`, and Codex definitions are converted to
+`.codex/agents/*.toml`. OpenCode and Pi use `.agents/agents/` content directly.
+
+## Phase 3: Replace README.md
Write a fresh README.md to the project root. Use a template structure like:
-```markdown
+````markdown
# [Game Name]
> [One-line description]
@@ -86,7 +111,7 @@ Built with [Engine] using [OpenCode Game Studios](https://github.com/striderZA/O
## Quick Start
```bash
-opencode
+[harness command]
```
Type `/start` for onboarding, or browse all skills with `/`.
@@ -105,26 +130,55 @@ Type `/start` for onboarding, or browse all skills with `/`.
## License
[Choose a license]
-```
+````
-Replace `[Game Name]`, `[One-line description]`, and `[Engine]` with the user's answers from Phase 1.
+Replace `[Game Name]`, `[One-line description]`, `[Engine]`, and `[harness command]` with the user's answers from Phase 1. Harness commands: `opencode`, `pi`, `omp`, `claude`, `codex`.
-## Phase 3: Update AGENTS.md
+## Phase 4: Update AGENTS.md
Read AGENTS.md and update:
- Set the engine to the user's choice by changing the `## Technology Stack` section
+- Add `- **Agent Harness**: [OpenCode 2.x | Pi 1.x | oh-my-pi 18.16.0+ | Claude Code | Codex]` to `## Technology Stack`
- Remove or update any project-specific settings
- If AGENTS.md is missing or malformed, warn and skip this phase
-## Phase 4: Update opencode.json
+## Phase 5: Configure the Selected Harness
+
+Read and validate only the config for the selected harness. If a file is missing or malformed, warn and skip that item.
+
+### OpenCode
+- Read `opencode.json`; remove internal-only plugin paths and keep `./.opencode/plugins/ccgs-hooks.ts` only if that file exists.
+- On Windows, check whether `.opencode/{agents,skills,commands,rules}` are directories. A git checkout without symlink support writes them as one-line text stubs. If any is a file, run:
+ ```bash
+ node tools/fix-opencode-symlinks.mjs
+ ```
+- Re-check that all four paths are directories before reporting success.
+
+### Pi
+- Read `.pi/settings.json`; ensure `"enableSkillCommands": true`.
+- MCP entries live in `.pi/mcp.json`. Tell the user that Pi reads this file only after the project is trusted.
-Read opencode.json and clean it up:
-- Remove any internal-only plugin paths
-- Set project name appropriately
-- Keep the ccgs-hooks.ts plugin reference only if the file actually exists: `if [ -f .opencode/plugins/ccgs-hooks.ts ]; then ...`
-- If opencode.json is missing or malformed, warn and skip this phase
+### oh-my-pi (omp)
+- Read `.omp/config.yml`; ensure the OCGS extensions are listed.
+- MCP entries live in `.omp/mcp.json`.
+- `add --harness omp` generates `.omp/agents/*.md` from `.agents/agents/`. Each definition carries `name` and `description` and inherits the session tool set.
+- Reload the agent hub with `Ctrl`+`R` in `/agents` after generation.
-## Phase 5: Remove Internal Files
+### Claude Code
+- Claude Code reads `AGENTS.md` natively from v2.1.277. For older versions, and as a harmless fallback, create `CLAUDE.md` at the project root containing only:
+ ```markdown
+ @AGENTS.md
+ ```
+- Do not create `CLAUDE.md` if one already exists.
+- MCP entries live in `.mcp.json`. Tell the user the first interactive session asks for approval.
+
+### Codex
+- Codex reads `AGENTS.md` natively; no instruction file is needed.
+- Custom agents live in `.codex/agents/*.toml`; `add --harness codex` generates them from `.agents/agents/`.
+- MCP entries live in `.codex/config.toml` under `[mcp_servers.]`. Codex loads project `.codex/` config only after the project is trusted.
+- Codex has no project-level slash commands and no path-scoped rule equivalent; skills and `AGENTS.md` carry those workflows.
+
+## Phase 6: Remove Internal Files
For each file/directory below, check existence first before deleting. If the directory already has user-created content, warn and skip rather than destroying it:
@@ -134,7 +188,37 @@ For each file/directory below, check existence first before deleting. If the dir
- Clear `production/` contents only if empty of user files: similar guard
- On any error (file locked, permission denied), warn and continue to next item
-## Phase 6: Optional Git Reset
+## Phase 7: Optional Harness Pruning
+
+Ask with `question`:
+
+- **Prompt**: "This project is set up for [harness]. Do you want to keep the other harness configs so you can switch later, or remove them?"
+- **Options**:
+ - `Keep all harness configs` — recommended; nothing is deleted.
+ - `Remove the other harness configs` — show the exact list first, then delete only after confirmation.
+ - `Show me the list first` — print the paths and ask again.
+
+Never delete these shared paths, whichever harness was selected:
+
+- `AGENTS.md`
+- `.agents/` (canonical content and module sources)
+- `.opencode/modules/` and `.opencode/plugins/` (module CLI, framework plugins, tests)
+- `.pi/extensions/` (shared by Pi and omp)
+- `tools/`, `tests/`, `docs/`, `src/`, `assets/`, `design/`, `production/`
+
+Prune list per selected harness:
+
+| Selected | Safe to remove |
+|----------|----------------|
+| OpenCode | `.pi/settings.json`, `.omp/config.yml`, `.omp/mcp.json`, `.omp/agents/`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| Pi | `.opencode/{agents,skills,commands,rules}`, `.omp/`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| omp | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| Claude Code | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.omp/`, `.codex/`, `.cursor/rules/` |
+| Codex | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.omp/`, `.claude/{agents,skills,commands,rules,modules}/`, `.cursor/rules/` |
+
+Show the resolved paths before deleting. Do not delete files that are not on this list. Remove empty parent directories only after their contents are gone.
+
+## Phase 8: Optional Git Reset
If the user selected `--reset-git` or agrees when prompted:
- Offer to reset git history to a single commit
@@ -144,7 +228,7 @@ If the user selected `--reset-git` or agrees when prompted:
- Delete all old tags (optional) — warn: if tags were previously pushed to remote, deletion requires `git push origin --delete ` for each one
- Force push if needed (warn: this rewrites remote history for anyone who has cloned this repo)
-## Phase 7: Summary
+## Phase 9: Summary
Print a completion summary:
@@ -154,6 +238,7 @@ Print a completion summary:
Project: [Game Name]
Engine: [Engine]
Team: [Size]
+ Harness: [Harness]
What's next:
- Run /setup-engine [engine] to configure your engine docs
diff --git a/.opencode/modules/core/skills/init-template/SKILL.md b/.opencode/modules/core/skills/init-template/SKILL.md
index ff0dfcd..d716d2c 100644
--- a/.opencode/modules/core/skills/init-template/SKILL.md
+++ b/.opencode/modules/core/skills/init-template/SKILL.md
@@ -1,7 +1,7 @@
---
name: init-template
-description: "First-time repo setup for new projects. Transforms the cloned OCGS template into a clean, ready-to-use game project with your own identity."
-argument-hint: "[--reset-git] [--name \"My Game\"] [--engine godot|unity|unreal]"
+description: "First-time repo setup for new projects. Transforms the cloned OCGS template into a clean, ready-to-use game project with your own identity and harness."
+argument-hint: "[--reset-git] [--name \"My Game\"] [--engine godot|unity|unreal|sfml3|raylib|bevy] [--harness opencode|pi|omp|claude|codex]"
user-invocable: true
allowed-tools: Read, Glob, Grep, Write, Edit, Bash, question, Task
---
@@ -13,10 +13,11 @@ When this skill is invoked:
Check if CLI arguments were passed (from `argument-hint`):
- `--name "My Game"` → sets game name (skips the name question below)
-- `--engine godot|unity|unreal` → sets engine (skips engine question)
+- `--engine godot|unity|unreal|sfml3|raylib|bevy` → sets engine (skips engine question)
+- `--harness opencode|pi|omp|claude|codex` → sets harness (skips harness question)
- `--reset-git` → automatically offers git reset (skips the prompt)
-If `--name` and `--engine` are both provided, skip the interactive prompt entirely and proceed to Phase 2 using the provided values.
+If `--name`, `--engine`, and `--harness` are all provided, skip the interactive prompt entirely and proceed to Phase 2 using the provided values.
Otherwise, use `question` to gather missing details:
@@ -25,27 +26,44 @@ Otherwise, use `question` to gather missing details:
- **What is your game's one-line description?** (e.g., "A 2D platformer about a cat in space")
### Tab 2: Engine & Genre
-- **Which engine are you using?** (godot / unity / unreal)
+- **Which engine are you using?** (godot / unity / unreal / sfml3 / raylib / bevy)
- **What genre best describes your game?** (e.g., platformer, RPG, puzzle, FPS, strategy)
### Tab 3: Team
- **Team size** (solo / small 2-5 / medium 6-15 / large 16+)
-### Modular Installation
+### Tab 4: Agent Harness
+- **Prompt**: "Which agent harness will you use for this project?"
+- **Options**: `OpenCode` / `Pi` / `oh-my-pi (omp)` / `Claude Code` / `Codex`
-After setting project identity, install the desired framework modules:
+The template ships configuration for all five. The answer selects the module install target, the MCP config file, and the harness config this skill validates. It does not delete the others unless the user asks in Phase 7.
+
+| Harness | Content target | MCP file | Notes |
+|---------|----------------|----------|-------|
+| OpenCode | `.opencode/{agents,skills,commands,rules}` | `opencode.json` (`mcp.servers`) | Windows checkouts may need `node tools/fix-opencode-symlinks.mjs` |
+| Pi | `.agents/{agents,skills,commands,rules}` | `.pi/mcp.json` (`mcpServers`) | Project MCP loads only after the project is trusted |
+| omp | `.omp/agents` + `.agents/{skills,commands,rules}` | `.omp/mcp.json` (`mcpServers`) | Agents are generated into `.omp/agents/`; they inherit the session tool set |
+| Claude Code | `.claude/{agents,skills,commands,rules}` | `.mcp.json` (`mcpServers`) | Agents gain `name:` frontmatter; project MCP needs approval |
+| Codex | `.codex/agents` + `.agents/skills` | `.codex/config.toml` (`[mcp_servers.*]`) | Agents are converted to TOML; commands and path-rules do not map |
+
+## Phase 2: Install Modules
+
+Install the desired framework modules for the selected harness. Pass `--harness` on every command. `add` is idempotent: modules already present in the harness install state are skipped.
```bash
-# OpenCode target (default): .opencode/
-node .opencode/modules/install.mjs add core
-node .opencode/modules/install.mjs add engine-godot
-node .opencode/modules/install.mjs add art design ui qa release prototyping
+# Required
+node .opencode/modules/install.mjs add core --harness
-# Pi target: add --pi to install into .agents/
-node .opencode/modules/install.mjs add core --pi
+# Engine specialists (choose the module matching --engine)
+node .opencode/modules/install.mjs add engine-godot --harness # or engine-unity / engine-unreal / engine-sfml3 / engine-raylib / engine-bevy
+
+# Common starter set
+node .opencode/modules/install.mjs add art design ui qa release prototyping --harness
```
-Available modules (run `node .opencode/modules/install.mjs list` to see all):
+Use `--force` only to refresh files after an upgrade; it overwrites existing module files and refreshes the install record.
+
+Available modules (run `node .opencode/modules/install.mjs list --harness ` to see all):
| Module | Description |
|--------|-------------|
@@ -72,11 +90,18 @@ Available modules (run `node .opencode/modules/install.mjs list` to see all):
| `engine-bevy` | Bevy specialists |
| `data` | Data file conventions |
-## Phase 2: Replace README.md
+If a module has an MCP fragment for the selected harness, the installer merges it into that harness's MCP file. If the server already exists, it is left untouched and reported as skipped.
+
+Agent installation differs by harness: omp definitions are generated into
+`.omp/agents/`, Claude Code definitions gain the required `name:` frontmatter in
+`.claude/agents/`, and Codex definitions are converted to
+`.codex/agents/*.toml`. OpenCode and Pi use `.agents/agents/` content directly.
+
+## Phase 3: Replace README.md
Write a fresh README.md to the project root. Use a template structure like:
-```markdown
+````markdown
# [Game Name]
> [One-line description]
@@ -86,7 +111,7 @@ Built with [Engine] using [OpenCode Game Studios](https://github.com/striderZA/O
## Quick Start
```bash
-opencode
+[harness command]
```
Type `/start` for onboarding, or browse all skills with `/`.
@@ -105,26 +130,55 @@ Type `/start` for onboarding, or browse all skills with `/`.
## License
[Choose a license]
-```
+````
-Replace `[Game Name]`, `[One-line description]`, and `[Engine]` with the user's answers from Phase 1.
+Replace `[Game Name]`, `[One-line description]`, `[Engine]`, and `[harness command]` with the user's answers from Phase 1. Harness commands: `opencode`, `pi`, `omp`, `claude`, `codex`.
-## Phase 3: Update AGENTS.md
+## Phase 4: Update AGENTS.md
Read AGENTS.md and update:
- Set the engine to the user's choice by changing the `## Technology Stack` section
+- Add `- **Agent Harness**: [OpenCode 2.x | Pi 1.x | oh-my-pi 18.16.0+ | Claude Code | Codex]` to `## Technology Stack`
- Remove or update any project-specific settings
- If AGENTS.md is missing or malformed, warn and skip this phase
-## Phase 4: Update opencode.json
+## Phase 5: Configure the Selected Harness
+
+Read and validate only the config for the selected harness. If a file is missing or malformed, warn and skip that item.
+
+### OpenCode
+- Read `opencode.json`; remove internal-only plugin paths and keep `./.opencode/plugins/ccgs-hooks.ts` only if that file exists.
+- On Windows, check whether `.opencode/{agents,skills,commands,rules}` are directories. A git checkout without symlink support writes them as one-line text stubs. If any is a file, run:
+ ```bash
+ node tools/fix-opencode-symlinks.mjs
+ ```
+- Re-check that all four paths are directories before reporting success.
+
+### Pi
+- Read `.pi/settings.json`; ensure `"enableSkillCommands": true`.
+- MCP entries live in `.pi/mcp.json`. Tell the user that Pi reads this file only after the project is trusted.
-Read opencode.json and clean it up:
-- Remove any internal-only plugin paths
-- Set project name appropriately
-- Keep the ccgs-hooks.ts plugin reference only if the file actually exists: `if [ -f .opencode/plugins/ccgs-hooks.ts ]; then ...`
-- If opencode.json is missing or malformed, warn and skip this phase
+### oh-my-pi (omp)
+- Read `.omp/config.yml`; ensure the OCGS extensions are listed.
+- MCP entries live in `.omp/mcp.json`.
+- `add --harness omp` generates `.omp/agents/*.md` from `.agents/agents/`. Each definition carries `name` and `description` and inherits the session tool set.
+- Reload the agent hub with `Ctrl`+`R` in `/agents` after generation.
-## Phase 5: Remove Internal Files
+### Claude Code
+- Claude Code reads `AGENTS.md` natively from v2.1.277. For older versions, and as a harmless fallback, create `CLAUDE.md` at the project root containing only:
+ ```markdown
+ @AGENTS.md
+ ```
+- Do not create `CLAUDE.md` if one already exists.
+- MCP entries live in `.mcp.json`. Tell the user the first interactive session asks for approval.
+
+### Codex
+- Codex reads `AGENTS.md` natively; no instruction file is needed.
+- Custom agents live in `.codex/agents/*.toml`; `add --harness codex` generates them from `.agents/agents/`.
+- MCP entries live in `.codex/config.toml` under `[mcp_servers.]`. Codex loads project `.codex/` config only after the project is trusted.
+- Codex has no project-level slash commands and no path-scoped rule equivalent; skills and `AGENTS.md` carry those workflows.
+
+## Phase 6: Remove Internal Files
For each file/directory below, check existence first before deleting. If the directory already has user-created content, warn and skip rather than destroying it:
@@ -134,7 +188,37 @@ For each file/directory below, check existence first before deleting. If the dir
- Clear `production/` contents only if empty of user files: similar guard
- On any error (file locked, permission denied), warn and continue to next item
-## Phase 6: Optional Git Reset
+## Phase 7: Optional Harness Pruning
+
+Ask with `question`:
+
+- **Prompt**: "This project is set up for [harness]. Do you want to keep the other harness configs so you can switch later, or remove them?"
+- **Options**:
+ - `Keep all harness configs` — recommended; nothing is deleted.
+ - `Remove the other harness configs` — show the exact list first, then delete only after confirmation.
+ - `Show me the list first` — print the paths and ask again.
+
+Never delete these shared paths, whichever harness was selected:
+
+- `AGENTS.md`
+- `.agents/` (canonical content and module sources)
+- `.opencode/modules/` and `.opencode/plugins/` (module CLI, framework plugins, tests)
+- `.pi/extensions/` (shared by Pi and omp)
+- `tools/`, `tests/`, `docs/`, `src/`, `assets/`, `design/`, `production/`
+
+Prune list per selected harness:
+
+| Selected | Safe to remove |
+|----------|----------------|
+| OpenCode | `.pi/settings.json`, `.omp/config.yml`, `.omp/mcp.json`, `.omp/agents/`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| Pi | `.opencode/{agents,skills,commands,rules}`, `.omp/`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| omp | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.claude/{agents,skills,commands,rules,modules}/`, `.codex/`, `.cursor/rules/` |
+| Claude Code | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.omp/`, `.codex/`, `.cursor/rules/` |
+| Codex | `.opencode/{agents,skills,commands,rules}`, `.pi/settings.json`, `.omp/`, `.claude/{agents,skills,commands,rules,modules}/`, `.cursor/rules/` |
+
+Show the resolved paths before deleting. Do not delete files that are not on this list. Remove empty parent directories only after their contents are gone.
+
+## Phase 8: Optional Git Reset
If the user selected `--reset-git` or agrees when prompted:
- Offer to reset git history to a single commit
@@ -144,7 +228,7 @@ If the user selected `--reset-git` or agrees when prompted:
- Delete all old tags (optional) — warn: if tags were previously pushed to remote, deletion requires `git push origin --delete ` for each one
- Force push if needed (warn: this rewrites remote history for anyone who has cloned this repo)
-## Phase 7: Summary
+## Phase 9: Summary
Print a completion summary:
@@ -154,6 +238,7 @@ Print a completion summary:
Project: [Game Name]
Engine: [Engine]
Team: [Size]
+ Harness: [Harness]
What's next:
- Run /setup-engine [engine] to configure your engine docs
From d49dafcb4c6f94d61bb8e4f8393ce96fe07fa792 Mon Sep 17 00:00:00 2001
From: Jaco du Preez
Date: Wed, 4 Nov 2026 20:53:33 +0200
Subject: [PATCH 5/6] chore(git): anchor framework ignore to repo root
The unanchored framework/ pattern also ignored docs/framework/, which is
tracked. Anchor it so documentation changes stage normally.
Refs: docs/framework/quick-start.md
---
.gitignore | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index ea65d67..bd671f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,7 +14,7 @@ production/session-state/
# Superpowers SDK design docs (not part of OCGS)
.superpowers/
docs/superpowers/
-framework/
+/framework/
# Git worktrees
.worktrees/
From baeaabe834ad373b51b312ca39467d762ffd10fd Mon Sep 17 00:00:00 2001
From: Jaco du Preez
Date: Wed, 4 Nov 2026 20:53:35 +0200
Subject: [PATCH 6/6] docs: document multi-harness project setup
Document the five harness targets, module install flags, generated omp
and Codex agents, MCP files, and the updated directory layout.
Refs: docs/framework/quick-start.md
---
AGENTS.md | 34 ++++++++++---
README.md | 44 ++++++++++++++---
UPGRADING.md | 27 ++++++++--
docs/WORKFLOW-GUIDE.md | 2 +-
docs/framework/directory-structure.md | 34 +++++++++----
docs/framework/quick-start.md | 71 ++++++++++++++++++++-------
docs/framework/setup-requirements.md | 22 +++++++--
docs/omp-compatibility.md | 17 ++++++-
docs/pi-workflow.md | 10 ++++
9 files changed, 207 insertions(+), 54 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 3a9ccf2..cfca7d8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,6 +9,7 @@ Each agent owns a specific domain, enforcing separation of concerns and quality.
- **Language**: [CHOOSE: GDScript / C# / C++ / Blueprint / C / C++17]
- **Build System**: [SPECIFY after choosing engine]
- **Asset Pipeline**: [SPECIFY after choosing engine]
+- **Agent Harness**: [CHOOSE: OpenCode / Pi / oh-my-pi / Claude Code / Codex]
> **Note**: Engine-specialist agents exist for Godot, Unity, Unreal, SFML 3,
> Raylib, and Bevy. Use the set matching your engine.
@@ -20,21 +21,24 @@ Each agent owns a specific domain, enforcing separation of concerns and quality.
├── AGENTS.md # Project configuration
├── opencode.json # OpenCode V2 config (plugins, MCP, permissions)
├── .pi/ # Pi config (settings, 9 extensions)
-├── .omp/ # oh-my-pi config and MCP
+├── .omp/ # oh-my-pi config, generated agents, MCP
+├── .claude/ # Claude Code target (agents, skills, commands, rules)
+├── .mcp.json # Claude Code project MCP servers
+├── .codex/ # Codex target (agents, MCP config)
├── .githooks/ # Tracked git hooks (commit-msg, pre-commit)
├── .agents/ # Canonical content (harness-agnostic)
│ ├── agents/ # 52 agent definitions
│ ├── skills/ # 78 skill workflows
│ ├── commands/ # 78 slash commands
│ ├── rules/ # 11 path-scoped coding standards
-│ └── modules/ # 22 Pi-target module mirrors
+│ └── modules/ # 22 module sources (non-OpenCode targets)
├── .opencode/ # OpenCode-specific
│ ├── plugins/ # TypeScript plugins (ccgs-hooks, drift-detector, changelog-generator)
│ ├── agents/ → ../.agents/agents/ (symlink)
│ ├── skills/ → ../.agents/skills/ (symlink)
│ ├── commands/ → ../.agents/commands/ (symlink)
│ ├── rules/ → ../.agents/rules/ (symlink)
-│ └── modules/ # Module CLI + 22 OpenCode-target module mirrors
+│ └── modules/ # Module CLI + 22 OpenCode-target module sources
├── docs/
│ ├── architecture/ # Architecture Decision Records (ADRs)
│ ├── engine-reference/ # Curated engine API snapshots (version-pinned)
@@ -81,9 +85,9 @@ The framework is partitioned into installable theme modules.
**Available modules:** art, design, architecture, stories, programming, ui, audio, narrative, level-design, qa, release, prototyping, live-ops, localization, data, engine-godot, engine-unity, engine-unreal, engine-sfml3, engine-raylib, engine-bevy.
-**Install:** `node .opencode/modules/install.mjs add `
-**Remove:** `node .opencode/modules/install.mjs remove `
-**List:** `node .opencode/modules/install.mjs list`
+**Install:** `node .opencode/modules/install.mjs add [--harness ]`
+**Remove:** `node .opencode/modules/install.mjs remove [--harness ]`
+**List:** `node .opencode/modules/install.mjs list [--harness ]`
## Coordination Rules
@@ -196,11 +200,27 @@ See [docs/omp-compatibility.md](docs/omp-compatibility.md) for omp setup, extens
| Command | OpenCode | Pi | oh-my-pi (omp) |
|---------|----------|----|----------------|
-| Delegation | `Task` tool (built-in) | `Task` tool (ocgs-delegation extension) | omp's built-in task tool; OCGS roster not loaded |
+| Delegation | `Task` tool (built-in) | `Task` tool (ocgs-delegation extension) | omp's built-in task tool; OCGS agents generated into `.omp/agents/` |
| Peer review | — | `/consult ` (ocgs-delegation extension) | — (ocgs-delegation not wired) |
| Decision capture | question tool | question tool (ocgs-question with TUI) | ask (omp built-in); answers logged to `agent-decisions.jsonl` by ocgs-question |
| Skill invocation | `/command-name` | `/command-name` (via prompt templates) | `/skill:` (`skills.enableSkillCommands`) |
+### Harness targets
+
+`/init-template` asks which harness the project uses, and the module CLI accepts
+`--harness` on `add`, `remove`, `list`, and `info`:
+
+| Harness | `--harness` | Content target | MCP file |
+|---------|-------------|----------------|----------|
+| OpenCode | `opencode` (default) | `.opencode/{agents,skills,commands,rules}` | `opencode.json` → `mcp.servers` |
+| Pi | `pi` (or `--pi`) | `.agents/{agents,skills,commands,rules}` | `.pi/mcp.json` → `mcpServers` |
+| oh-my-pi | `omp` (or `--omp`) | `.omp/agents` + `.agents/{skills,commands,rules}` | `.omp/mcp.json` → `mcpServers` |
+| Claude Code | `claude` (or `--claude`) | `.claude/{agents,skills,commands,rules}` | `.mcp.json` → `mcpServers` |
+| Codex | `codex` (or `--codex`) | `.codex/agents` + `.agents/skills` | `.codex/config.toml` → `[mcp_servers.*]` |
+
+Module sources live in `.opencode/modules/` (OpenCode target) and
+`.agents/modules/` (all other targets). Both trees are tracked; keep them in sync.
+
## Available Commands
Type `/` in OpenCode to see all available commands. All 78 commands route to
diff --git a/README.md b/README.md
index 5e6d080..f84f43a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-> ⚡ Evolved from [Claude Code Game Studios (CCGS)](https://github.com/Donchitos/Claude-Code-Game-Studios) — now a standalone framework supporting [OpenCode](https://opencode.ai), [Pi](https://github.com/earendil-works/pi-coding-agent) and [oh-my-pi](https://omp.sh/) coding agents.
+> ⚡ Evolved from [Claude Code Game Studios (CCGS)](https://github.com/Donchitos/Claude-Code-Game-Studios) — now a standalone framework supporting [OpenCode](https://opencode.ai), [Pi](https://github.com/earendil-works/pi-coding-agent), [oh-my-pi](https://omp.sh/), [Claude Code](https://code.claude.com/docs/), and [Codex](https://developers.openai.com/codex/) coding agents.
[](LICENSE)
[](.agents/agents/)
@@ -70,7 +70,7 @@ and [oh-my-pi](https://omp.sh/).
| Component | CCGS (Claude Code) | OpenCode | Pi | omp | Status |
|-----------|-------------------|----------|-----|-----|--------|
-| 🤖 **Agents** | 49 agents (`.claude/agents/`) | 52 agents (`.agents/agents/`) | 52 agents (`.agents/agents/`) | Not loaded — omp's task tool reads `.omp/agents/` | ✅ |
+| 🤖 **Agents** | 49 agents (`.claude/agents/`) | 52 agents (`.agents/agents/`) | 52 agents (`.agents/agents/`) | Generated into `.omp/agents/` by the installer | ✅ |
| ⌨️ **Skills** | 72 skills (`.claude/skills/`) | 78 skills (`.agents/skills/`) | 78 skills (`.agents/skills/`) | 78 skills (`/skill:`) | ✅ +6 |
| ⌨️ **Commands** | — | 78 commands (`.agents/commands/`) | → prompt templates | → slash commands | ✅ New |
| 🔗 **Plugins** | 12 bash hooks (`.claude/hooks/`) | 3 TS plugins (`.opencode/plugins/`) | 9 Pi extensions (`.pi/extensions/`) | 7 shared via config | ✅ **177 plugin + 54 hook tests** |
@@ -101,9 +101,21 @@ definitions are present, but `Task` delegation has a known defect — see
```bash
omp
```
-omp reads `.agents/` skills, commands, and rules natively and loads the shared Pi extensions through `.omp/config.yml`. It does not load `.agents/agents/`; its task tool uses `.omp/agents/`.
+omp reads `.agents/` skills, commands, and rules natively and loads the shared Pi extensions through `.omp/config.yml`. Its task tool reads project agents from `.omp/agents/`; run `/init-template --harness omp` (or `add core --harness omp`) to generate them from `.agents/agents/`.
See [docs/omp-compatibility.md](docs/omp-compatibility.md) for setup details and known gaps.
+### Claude Code
+```bash
+claude
+```
+Claude Code reads `AGENTS.md` natively and `.claude/rules/` with `paths` frontmatter. Run `/init-template --harness claude` (or `add core --harness claude`) to install agents, skills, and commands into `.claude/`; agent files gain the required `name:` frontmatter. Project MCP servers live in `.mcp.json` and need approval in an interactive session.
+
+### Codex
+```bash
+codex
+```
+Codex reads `.agents/skills` and `AGENTS.md` natively. Run `/init-template --harness codex` (or `add core --harness codex`) to generate `.codex/agents/*.toml`. Codex has no project-level slash commands and no path-scoped rule equivalent; its MCP entries live in `.codex/config.toml` and load only in trusted projects.
+
### 🎮 Demo Game
See [**OCGS-Pong**](https://github.com/striderZA/OCGS-Pong) — a complete Pong game built entirely with this toolchain. Demonstrates the full workflow from concept through implementation using OCGS agents and skills.
@@ -140,10 +152,15 @@ The framework is partitioned into **22 pluggable theme modules**. Only install w
| `data` | Data file conventions and validation |
```bash
-# Install modules
+# Install modules (OpenCode target is the default)
node .opencode/modules/install.mjs add core
node .opencode/modules/install.mjs add engine-godot art design qa
+# Install for a specific harness
+node .opencode/modules/install.mjs add core --harness omp
+node .opencode/modules/install.mjs add core --harness claude
+node .opencode/modules/install.mjs add core --harness codex
+
# List all available and installed modules
node .opencode/modules/install.mjs list
@@ -155,10 +172,18 @@ node .opencode/modules/install.mjs remove art
```
Module sources are mirrored: `.opencode/modules/` feeds the OpenCode target (the
-default) and `.agents/modules/` feeds the Pi target (`--pi`). Both trees are
+default) and `.agents/modules/` feeds every other target. Both trees are
tracked; keep them in sync when changing a module. User-modified files are
detected during removal and left in place.
+| Harness | `--harness` | Content target | MCP file |
+|---------|-------------|----------------|----------|
+| OpenCode | `opencode` (default) | `.opencode/{agents,skills,commands,rules}` | `opencode.json` |
+| Pi | `pi` / `--pi` | `.agents/{agents,skills,commands,rules}` | `.pi/mcp.json` |
+| oh-my-pi (omp) | `omp` / `--omp` | `.omp/agents` + `.agents/{skills,commands,rules}` | `.omp/mcp.json` |
+| Claude Code | `claude` / `--claude` | `.claude/{agents,skills,commands,rules}` | `.mcp.json` |
+| Codex | `codex` / `--codex` | `.codex/agents` + `.agents/skills` | `.codex/config.toml` |
+
---
## 🔌 Recommended Plugins
@@ -191,7 +216,7 @@ on startup. See [docs/pi-extensions.md](docs/pi-extensions.md) for details.
| CCGS (Claude Code) | OpenCode | Pi | omp |
|--------------------|----------|-----|-----|
| `.claude/skills/*.md` → | `.agents/skills/*.md` | `.agents/skills/*.md` | `.agents/skills/*.md` |
-| `.claude/agents/*.md` → | `.agents/agents/*.md` | `.agents/agents/*.md` | Not loaded — task tool reads `.omp/agents/` |
+| `.claude/agents/*.md` → | `.agents/agents/*.md` | `.agents/agents/*.md` | Generated into `.omp/agents/` |
| `.claude/hooks/*.sh` → | `.opencode/plugins/ccgs-hooks.ts` | `.pi/extensions/` | `.pi/extensions/` (via `.omp/config.yml`) |
| `.claude/rules/*.md` → | `.agents/rules/*.md` | `.agents/rules/*.md` | `.agents/rules/*.md` |
| `CLAUDE.md` → | `AGENTS.md` | `AGENTS.md` | `AGENTS.md` |
@@ -207,13 +232,16 @@ on startup. See [docs/pi-extensions.md](docs/pi-extensions.md) for details.
├── opencode.json ⚙️ OpenCode config (permissions, plugins)
├── .pi/ ⚙️ Pi config (extensions, settings)
│ └── extensions/ 🧩 Pi extensions (ocgs-core, delegation, question, etc.)
-├── .omp/ ⚙️ oh-my-pi config (extensions, MCP)
+├── .omp/ ⚙️ oh-my-pi config (extensions, generated agents, MCP)
+├── .claude/ ⚙️ Claude Code target (agents, skills, commands, rules)
+├── .mcp.json ⚙️ Claude Code project MCP servers
+├── .codex/ ⚙️ Codex target (agents, MCP config)
├── .agents/ 📦 Canonical content (harness-agnostic)
│ ├── agents/ 🤖 52 agent definitions
│ ├── skills/ 🛠️ 78 skill workflows
│ ├── commands/ ⌨️ 78 slash commands
│ ├── rules/ 📏 11 coding standards
-│ └── modules/ 🧩 22 Pi-target module mirrors
+│ └── modules/ 🧩 22 module sources (non-OpenCode targets)
│ ├── ... 22 modules total
├── .opencode/ ⚙️ OpenCode-specific (plugins, config)
│ ├── commands/ → .agents/commands/ (symlink)
diff --git a/UPGRADING.md b/UPGRADING.md
index c053e9a..b8a4490 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -93,17 +93,24 @@ Keep your previous `.opencode/` content only until you have verified the migrati
### 2. Modular install
The framework is partitioned into 22 theme modules. The installer reads
-`.opencode/modules/` for the OpenCode target and `.agents/modules/` for the Pi
-target:
+`.opencode/modules/` for the OpenCode target and `.agents/modules/` for every
+other target:
```shell
node .opencode/modules/install.mjs add core engine-godot art design qa
node .opencode/modules/install.mjs list
-# Pi target
-node .opencode/modules/install.mjs add core --pi
+# Other targets
+node .opencode/modules/install.mjs add core --harness pi
+node .opencode/modules/install.mjs add core --harness omp
+node .opencode/modules/install.mjs add core --harness claude
+node .opencode/modules/install.mjs add core --harness codex
```
+`--harness` selects the content target, the install manifest, and the MCP file.
+`--pi` remains as an alias. `/init-template` asks for the harness and passes it
+through.
+
### 3. OpenCode V2 configuration
`opencode.json` uses the V2 schema: `plugins` (plural), `mcp.servers`,
@@ -121,6 +128,18 @@ agent files in `.agents/agents/`.
- **OpenCode 2.x** — V2 config, plugins under `.opencode/plugins/`
- **Pi 1.x** — extensions under `.pi/extensions/`
- **omp 18.16.0+** — `.omp/config.yml` loads the shared Pi extensions
+- **Claude Code 2.1.277+** — `.claude/` content, `.mcp.json`, native `AGENTS.md`
+- **Codex** — `.agents/skills` and `AGENTS.md` native; `.codex/agents/*.toml`
+
+Module installs are harness-aware:
+
+| Harness | `--harness` | Content target | MCP file |
+|---------|-------------|----------------|----------|
+| OpenCode | `opencode` (default) | `.opencode/{agents,skills,commands,rules}` | `opencode.json` |
+| Pi | `pi` / `--pi` | `.agents/{agents,skills,commands,rules}` | `.pi/mcp.json` |
+| oh-my-pi | `omp` / `--omp` | `.omp/agents` + `.agents/{skills,commands,rules}` | `.omp/mcp.json` |
+| Claude Code | `claude` / `--claude` | `.claude/{agents,skills,commands,rules}` | `.mcp.json` |
+| Codex | `codex` / `--codex` | `.codex/agents` + `.agents/skills` | `.codex/config.toml` |
### 6. Engine reference
diff --git a/docs/WORKFLOW-GUIDE.md b/docs/WORKFLOW-GUIDE.md
index ab9e3fd..bf60008 100644
--- a/docs/WORKFLOW-GUIDE.md
+++ b/docs/WORKFLOW-GUIDE.md
@@ -39,7 +39,7 @@
Before you start, make sure you have:
-- **One agent harness** installed and working: OpenCode 2.x, Pi 1.x, or oh-my-pi 18.16.0+
+- **One agent harness** installed and working: OpenCode 2.x, Pi 1.x, oh-my-pi 18.16.0+, Claude Code 2.1.277+, or Codex
- **Git** with Git Bash (Windows) or standard terminal (Mac/Linux)
- **Node.js 18+** for the module CLI and hooks
- No `jq` or Python dependency — the hooks are TypeScript
diff --git a/docs/framework/directory-structure.md b/docs/framework/directory-structure.md
index b624a83..122d1a9 100644
--- a/docs/framework/directory-structure.md
+++ b/docs/framework/directory-structure.md
@@ -10,7 +10,7 @@
│ ├── skills/ # 78 skill workflows
│ ├── commands/ # 78 slash commands
│ ├── rules/ # 11 path-scoped coding standards
-│ └── modules/ # 22 Pi-target module mirrors
+│ └── modules/ # 22 module sources for non-OpenCode targets
│ ├── installed.json # Manifest of installed modules
│ ├── core/ # Core module (always installed)
│ ├── art/ # Art module (aseprite MCP, art bible)
@@ -33,15 +33,29 @@
│ │ ├── drift-detector.ts # Template drift detection
│ │ ├── changelog-generator.ts
│ │ └── tests/ # 19 test suites (177 tests)
-│ └── modules/ # Module CLI + 22 OpenCode-target mirrors
-│ ├── install.mjs # CLI: add/remove/list modules (--pi for Pi target)
+│ └── modules/ # Module CLI + 22 OpenCode-target module sources
+│ ├── install.mjs # CLI: add/remove/list (--harness )
│ └── installed.json # Module manifest
├── .pi/ # Pi-specific extensions & settings
-│ ├── extensions/ # 9 Pi extensions (ocgs-core, delegation, question, etc.)
+│ ├── extensions/ # 9 Pi extensions (shared with omp)
│ └── settings.json # Pi configuration
├── .omp/ # oh-my-pi config
+│ ├── agents/ # Generated agents for the task tool
│ ├── config.yml # Shared extension wiring
+│ ├── modules/installed.json # omp install manifest
│ └── mcp.json # Project MCP servers (disabled by default)
+├── .claude/ # Claude Code target (created by --harness claude)
+│ ├── agents/ # Subagents (name: frontmatter injected)
+│ ├── skills/ # Skills
+│ ├── commands/ # Slash commands
+│ ├── rules/ # Path-scoped rules (paths: frontmatter)
+│ └── modules/installed.json # Claude install manifest
+├── .mcp.json # Claude Code project MCP servers
+├── .codex/ # Codex target (created by --harness codex)
+│ ├── agents/ # TOML agents generated from .agents/agents/
+│ ├── config.toml # MCP servers under [mcp_servers.*]
+│ └── modules/installed.json # Codex install manifest
+├── CLAUDE.md # Optional @AGENTS.md fallback for Claude Code < 2.1.277
├── src/ # Game source code (core, gameplay, ai, networking, ui, tools)
├── assets/ # Game assets (art, audio, vfx, shaders, data)
├── design/ # Game design documents (gdd, narrative, levels, balance)
@@ -88,14 +102,14 @@
1. **`.agents/` is canonical**: Agent definitions, skills, commands, and rules
live here. This directory is harness-agnostic.
-2. **`.opencode/`, `.pi/`, and `.omp/` are harness-specific**: They contain
- symlinks to `.agents/` content plus harness-specific plugins, extensions, and
- configuration.
+2. **`.opencode/`, `.pi/`, `.omp/`, `.claude/`, and `.codex/` are harness-specific**:
+ They contain symlinks or installed copies of `.agents/` content plus
+ harness-specific plugins, extensions, and configuration.
3. **Modules are installable**: Theme modules are mirrored under
- `.opencode/modules/` (OpenCode target) and `.agents/modules/` (Pi target).
- Install with `node .opencode/modules/install.mjs add `, or add `--pi`
- to install into `.agents/`. Keep the two mirrors in sync when editing a module.
+ `.opencode/modules/` (OpenCode target) and `.agents/modules/` (every other
+ target). Install with `node .opencode/modules/install.mjs add
+ --harness `. Keep the two source mirrors in sync when editing a module.
4. **22 modules available**: core, art, design, architecture, stories, programming,
ui, audio, narrative, level-design, qa, release, prototyping, live-ops,
diff --git a/docs/framework/quick-start.md b/docs/framework/quick-start.md
index 1858510..f0012cd 100644
--- a/docs/framework/quick-start.md
+++ b/docs/framework/quick-start.md
@@ -3,8 +3,9 @@
## What Is This?
This is a complete agent architecture for game development, supporting
-[OpenCode](https://opencode.ai), [Pi](https://github.com/earendil-works/pi-coding-agent), and
-[oh-my-pi](https://omp.sh/) (`omp`). It
+[OpenCode](https://opencode.ai), [Pi](https://github.com/earendil-works/pi-coding-agent),
+[oh-my-pi](https://omp.sh/) (`omp`), [Claude Code](https://code.claude.com/docs/), and
+[Codex](https://developers.openai.com/codex/). It
organizes 52 specialized AI agents into a studio hierarchy that mirrors
real game development teams, with defined responsibilities, delegation
rules, and coordination protocols. It includes engine-specialist agents
@@ -314,21 +315,41 @@ definitions are present, but `Task` delegation has a known defect (see
omp
```
omp reads `.agents/` skills, commands, and rules natively and loads the shared
-Pi extensions through `.omp/config.yml`. See `docs/omp-compatibility.md` for
-setup and known gaps.
+Pi extensions through `.omp/config.yml`. Run `add core --harness omp` (or
+`/init-template --harness omp`) to generate `.omp/agents/` from `.agents/agents/`.
+See `docs/omp-compatibility.md` for setup and known gaps.
+
+#### Claude Code
+```bash
+claude
+```
+Claude Code reads `AGENTS.md` natively and `.claude/rules/` with `paths`
+frontmatter. Install agents, skills, and commands into `.claude/` with
+`--harness claude`; the installer adds the required `name:` frontmatter to
+agents. MCP servers live in `.mcp.json`.
+
+#### Codex
+```bash
+codex
+```
+Codex reads `.agents/skills` and `AGENTS.md` natively. Install with
+`--harness codex` to generate `.codex/agents/*.toml`. Codex has no project-level
+slash commands, and its `.codex/config.toml` MCP entries load only in trusted
+projects.
#### Install Modules
```bash
-node .opencode/modules/install.mjs add core # Required: directors, /start, /help
-node .opencode/modules/install.mjs add engine-godot # Engine specialists (or engine-unity/engine-unreal)
-node .opencode/modules/install.mjs add art # Aseprite MCP, art bible, asset specs
-node .opencode/modules/install.mjs add design # Game mechanics, systems, combat
-node .opencode/modules/install.mjs add qa # QA pipeline, test setup, profiling
-node .opencode/modules/install.mjs list # See all available + installed
-
-# Pi target: add --pi to install into .agents/ instead of .opencode/
-node .opencode/modules/install.mjs add core --pi
+HARNESS=opencode # opencode | pi | omp | claude | codex
+
+node .opencode/modules/install.mjs add core --harness "$HARNESS" # Required: directors, /start, /help
+node .opencode/modules/install.mjs add engine-godot --harness "$HARNESS" # Engine specialists
+node .opencode/modules/install.mjs add art --harness "$HARNESS" # Aseprite MCP, art bible, asset specs
+node .opencode/modules/install.mjs add design --harness "$HARNESS" # Game mechanics, systems, combat
+node .opencode/modules/install.mjs add qa --harness "$HARNESS" # QA pipeline, test setup, profiling
+node .opencode/modules/install.mjs list --harness "$HARNESS" # See all available + installed
+
+# Shorthands: --pi, --omp, --claude, --codex
```
Available modules: core, art, design, architecture, stories, programming, ui, audio, narrative, level-design, qa, release, prototyping, live-ops, localization, data, engine-godot, engine-unity, engine-unreal, engine-sfml3, engine-raylib, engine-bevy.
@@ -343,18 +364,30 @@ opencode.json -- OpenCode V2 config (plugins, MCP, permissi
skills/ -- 78 skill workflows
commands/ -- 78 slash commands
rules/ -- 11 path-scoped rule files
- modules/ -- 22 Pi-target module mirrors
+ modules/ -- Module sources for non-OpenCode targets
.opencode/ -- OpenCode-specific
+ agents|skills|commands|rules/ -- Symlinks to .agents/ (Windows: run tools/fix-opencode-symlinks.mjs)
plugins/ -- TypeScript plugins (ccgs-hooks, drift, changelog)
- modules/ -- Module CLI + 22 OpenCode-target module mirrors
- install.mjs -- Module CLI: add/remove/list [--pi]
- installed.json -- Installed module manifest
+ modules/ -- Module CLI + OpenCode module sources
+ install.mjs -- Module CLI: add/remove/list [--harness ]
+ installed.json -- OpenCode install manifest
.pi/ -- Pi-specific
settings.json -- Pi settings
- extensions/ -- 9 Pi extensions (auto-discovered)
+ extensions/ -- 9 Pi extensions (shared with omp)
+ mcp.json -- Project MCP servers (created by module install)
.omp/ -- oh-my-pi-specific
+ agents/ -- Generated agent definitions for the task tool
config.yml -- Extension wiring
- mcp.json -- Project MCP servers (disabled by default)
+ mcp.json -- Project MCP servers
+.claude/ -- Claude Code target (created by --harness claude)
+ agents/ skills/ commands/ rules/
+ modules/installed.json -- Claude install manifest
+.mcp.json -- Claude Code project MCP servers
+.codex/ -- Codex target (created by --harness codex)
+ agents/ -- TOML agents generated from .agents/agents/
+ config.toml -- MCP servers under [mcp_servers.*]
+ modules/installed.json -- Codex install manifest
+CLAUDE.md -- Optional @AGENTS.md fallback for Claude Code < 2.1.277
docs/ -- Framework and project documentation
architecture/ -- ADRs (created during architecture phase)
engine-reference/ -- Version-pinned engine API snapshots
diff --git a/docs/framework/setup-requirements.md b/docs/framework/setup-requirements.md
index 449ff17..42b6aa4 100644
--- a/docs/framework/setup-requirements.md
+++ b/docs/framework/setup-requirements.md
@@ -5,7 +5,7 @@ Validation hooks run inside your harness and require Node.js. If Node.js or an
optional engine tool is missing, that validation is unavailable; the rest of the
framework still works.
-> **New projects:** After cloning this template, run `/init-template` before anything else. It customizes the template with your game name, engine choice, and team preferences, and cleans out example files.
+> **New projects:** After cloning this template, run `/init-template` before anything else. It customizes the template with your game name, engine choice, agent harness, and team preferences, and cleans out example files.
## Required
@@ -13,7 +13,7 @@ framework still works.
| ---- | ---- | ---- |
| **Git** | Version control, branch management | [git-scm.com](https://git-scm.com/) |
| **Node.js 18+** | Module CLI + hooks | Runtime for `install.mjs` and the CCGS TypeScript hooks plugin | [nodejs.org](https://nodejs.org/) |
-| **One agent harness** | AI agent CLI | Choose **one**: OpenCode 2.x (tested 2.2.5), Pi 1.x, or oh-my-pi 18.16.0+ |
+| **One agent harness** | AI agent CLI | Choose **one**: OpenCode 2.x (tested 2.2.5), Pi 1.x, oh-my-pi 18.16.0+, Claude Code 2.1.277+ (for native `AGENTS.md`), or Codex |
### Install the harness you use
@@ -22,8 +22,24 @@ framework still works.
| **OpenCode 2.x** | `npm install -g @opencode/cli` | `opencode --version` (expect 2.x) |
| **Pi 1.x** | `npm install -g @earendil-works/pi-coding-agent` | `pi --version` |
| **oh-my-pi 18.16.0+** | See [omp.sh](https://omp.sh/) | `omp --version` |
+| **Claude Code** | See [code.claude.com](https://code.claude.com/docs/en/setup) | `claude --version` (expect 2.1.277+ for `AGENTS.md`) |
+| **Codex** | See [developers.openai.com/codex](https://developers.openai.com/codex/) | `codex --version` |
-The framework does not require all three. Install one harness and run `opencode`, `pi`, or `omp` from the repository root.
+The framework does not require all five. Install one harness and run `opencode`, `pi`, `omp`, `claude`, or `codex` from the repository root.
+
+### Module install target
+
+`/init-template` asks which harness the project uses, and the module CLI takes `--harness`:
+
+| Harness | `--harness` | Content target | MCP file |
+| ---- | ---- | ---- | ---- |
+| OpenCode | `opencode` (default) | `.opencode/{agents,skills,commands,rules}` | `opencode.json` → `mcp.servers` |
+| Pi | `pi` (or `--pi`) | `.agents/{agents,skills,commands,rules}` | `.pi/mcp.json` → `mcpServers` |
+| oh-my-pi | `omp` (or `--omp`) | `.omp/agents` + `.agents/{skills,commands,rules}` | `.omp/mcp.json` → `mcpServers` |
+| Claude Code | `claude` (or `--claude`) | `.claude/{agents,skills,commands,rules}` | `.mcp.json` → `mcpServers` |
+| Codex | `codex` (or `--codex`) | `.codex/agents` + `.agents/skills` | `.codex/config.toml` → `[mcp_servers.*]` |
+
+Shorthands (`--pi`, `--omp`, `--claude`, `--codex`) remain available. The installer writes MCP fragments only for the selected harness; entries that already exist are left untouched.
## Recommended
diff --git a/docs/omp-compatibility.md b/docs/omp-compatibility.md
index 6b8d26d..f7088e1 100644
--- a/docs/omp-compatibility.md
+++ b/docs/omp-compatibility.md
@@ -8,6 +8,7 @@ omp loads the same `.pi/extensions/*` modules that Pi does. Nothing is copied in
1. Install oh-my-pi. Tested with 18.16.0 (see [Tested Versions](#tested-versions)).
2. From the repository root (the directory with `AGENTS.md` and `.omp/`), run `omp`.
+3. Generate the project agent roster: `node .opencode/modules/install.mjs add core --harness omp` (or run `/init-template --harness omp`). This writes `.omp/agents/*.md`; press `Ctrl`+`R` in `/agents` to reload the hub.
Project config is read from `.omp/config.yml` in the working directory. Launch from the repository root. Launching from `src/` loads no OCGS extensions (see [Acceptance Matrix](#acceptance-matrix), row 12).
@@ -20,7 +21,17 @@ omp's built-in `agents` provider reads `.agents/` directly, so these load withou
- `.agents/commands/*.md` as slash commands.
- `.agents/rules/*.md`, which load but are not applied on their own. See [Rules](#rules).
-`.agents/agents/` does not load. See [What Does Not Carry Over](#what-does-not-carry-over).
+`.agents/agents/` does not load; the installer generates `.omp/agents/` from it. See [Generated Agents](#generated-agents).
+
+## Generated Agents
+
+omp reads project agents from `.omp/agents/.md`. Install a module with `--harness omp` to generate one file per agent in `.agents/agents/`:
+
+```bash
+node .opencode/modules/install.mjs add core --harness omp
+```
+
+Each generated file carries `name` and `description`; the source body becomes the agent's instructions. `tools` is omitted, so the agent inherits the session's tool set. Add an explicit `tools` list to an individual `.omp/agents/.md` when you want a narrower boundary. omp reads the nearest `.omp/agents` directory, so a project definition wins over a user-level one. Press `Ctrl`+`R` in `/agents` after generating or editing.
## What `.omp/config.yml` Wires
@@ -66,13 +77,15 @@ omp reads MCP servers from `mcpServers` in `.omp/mcp.json` (project) and `~/.omp
To run `/autopilot-playtest`, set `enabled` to `true` and restart omp. Its tools are named `mcp__autopilot_`, for example `mcp__autopilot_start_run`. The `autopilot` command must be on PATH. In our test, the OpenCode V2 `mcp.servers` entry in `opencode.json` gave omp no Autopilot tools, so `.omp/mcp.json` is the entry that applies.
+Module installs write here when you pass `--harness omp`. For example, `node .opencode/modules/install.mjs add art --harness omp` merges the Aseprite fragment into `.omp/mcp.json`; an existing server with the same name is left untouched.
+
## What Does Not Carry Over
| Item | On Pi | On omp | Reason |
|---|---|---|---|
| `ocgs-core` barrel | Loaded via explicit `-e`; discovery requires `-a` (see [Pi Notes](#pi-notes)) | Not wired | omp's agents provider reads `.agents/` directly, so the barrel is unnecessary |
| `ocgs-delegation` (`Task`, `/consult`) | Loaded, but broken | Not wired | See the delegation defect below |
-| `.agents/agents/` roster (52 agents) | Read by `Task`, which is broken | Not loaded | omp's `task` tool reads `.omp/agents/` and its bundled agents |
+| `.agents/agents/` roster (52 agents) | Read by `Task`, which is broken | Generated into `.omp/agents/` by the installer | omp reads `.omp/agents/`, not `.agents/agents/` |
| `.pi/settings.json` | Read | Not read | Set `skills.enableSkillCommands` in `.omp/config.yml` instead |
### Delegation defect (both harnesses)
diff --git a/docs/pi-workflow.md b/docs/pi-workflow.md
index 121a995..e0e366b 100644
--- a/docs/pi-workflow.md
+++ b/docs/pi-workflow.md
@@ -108,6 +108,16 @@ If you've been using OCGS with OpenCode and want to try Pi:
To switch back, just run `opencode` instead. Both harnesses share the same `.agents/` content.
+Module installs are per harness. Content for Pi lands in `.agents/`; MCP fragments
+go to `.pi/mcp.json`:
+
+```bash
+node .opencode/modules/install.mjs add art --harness pi
+```
+
+Use `--harness omp` for oh-my-pi: same skills/commands/rules target, with
+`.omp/agents/` for generated agent definitions and `.omp/mcp.json` for MCP.
+
## Troubleshooting
| Symptom | Likely Cause | Fix |