Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ To install skills for selected harnesses, list them with commas:
```bash
npx @3metajun/mstack --harness codex,claude
npx @3metajun/mstack --harness opencode,pi
npx @3metajun/mstack --harness antigravity,grok
```

To install selected skills, add `--skill`:
Expand Down Expand Up @@ -81,7 +82,7 @@ in `~/.config/mstack/models.json`; the
installer registers no hooks, so the hook comes only with the plugin.

The installer remains the route for bare `/meta-mode` names, per-skill
selection, and the shared Codex, OpenCode, and pi copy:
selection, and the shared Codex, OpenCode, pi, and Grok copy:
`npx @3metajun/mstack --harness claude`.

### Claude Desktop chat, Cowork, and claude.ai
Expand Down Expand Up @@ -190,13 +191,23 @@ The default directories are defined in
| Claude Code | `~/.claude/skills/` | `HARNESS_SKILLS_CLAUDE_DIR` |
| OpenCode | `~/.agents/skills/` | `HARNESS_SKILLS_OPENCODE_DIR` |
| pi | `~/.agents/skills/` | `HARNESS_SKILLS_PI_DIR` |
| Antigravity (`agy`) | `~/.gemini/antigravity-cli/skills/` | `HARNESS_SKILLS_ANTIGRAVITY_DIR` |
| Grok | `~/.agents/skills/` | `HARNESS_SKILLS_GROK_DIR` |

Codex, OpenCode and pi share one physical copy per skill by default. Updating
Codex, OpenCode, pi and Grok share one physical copy per skill by default. Updating
through any of these Harnesses updates that shared copy. Claude gets its own
adapter output without a frontmatter `name`: Claude uses the directory name,
while OpenCode skips that copy. No global Harness settings are changed.

OpenCode and pi agent artifacts retain their native configuration roots.
Antigravity does not read `~/.agents/skills/`, so it keeps its own canonical copy
under `~/.gemini/antigravity-cli/skills/` and relies on the frontmatter `name`
the canonical skills carry. Cursor already reads `~/.agents/skills/` and needs no
separate target; see the [Harness adapter reference](./docs/harness-adapters.md).

OpenCode and pi agent artifacts retain their native configuration roots. Grok
agents go to `~/.grok/agents/`. Antigravity artifacts install beside its skills
root (`~/.gemini/antigravity-cli/`); whether `agy` discovers agent files there is
not verified, so treat that target as storage until you confirm it.
`CLAUDE_CONFIG_DIR`, `XDG_CONFIG_HOME` and `PI_CODING_AGENT_DIR` still resolve
native roots and legacy migration locations; the latter two no longer move
the default shared skills. Use `HARNESS_SKILLS_*_DIR` for explicit skill paths.
Expand Down Expand Up @@ -312,6 +323,10 @@ npm run run-role -- --harness opencode --role explorer \

Add `--execute` to run the command. SSH environments pass the prompt through
strict POSIX quoting or a PowerShell encoded command, depending on `shell`.
Harnesses whose CLI takes the prompt as the value of a flag (`agy --print`,
`grok --single`) declare that flag as `runtime.promptArgs` in
[`profiles/harnesses.json`](./profiles/harnesses.json), so the model and read-only
arguments never land between the flag and the prompt.

Model roles live in `~/.config/mstack/models.json`. Each role accepts a model
string. The `reviewer` role also accepts a non-empty list of unique model strings.
Expand Down Expand Up @@ -358,10 +373,14 @@ default model unless you supply `--model`, `--parent-model`, or a `--file` with
model choices. An inherited role in that file needs `--parent-model`. The live
prompt checks a reply marker; it does not prove that the Harness loaded the skill
or completed its workflow. These live calls always use `--read-only`.
Codex, Claude Code, and pi use CLI-enforced tool restrictions.
Codex, Claude Code, pi, and Grok use CLI-enforced tool restrictions; Grok keeps
only `read_file`, `list_dir`, and `grep`.
OpenCode selects its built-in `plan` agent, which denies direct edits but still
allows shell commands in OpenCode 1.18; use a disposable checkout when its
prompt-only write boundary is insufficient. A live check also needs that
prompt-only write boundary is insufficient. Antigravity's `--mode plan` has the
same limit: it is an agent mode, not a tool allowlist. In Antigravity 1.2.17 it
left the workspace untouched but still wrote its own plan file under
`~/.gemini/antigravity-cli/`. A live check also needs that
Harness's credentials and configured model access.

The Claude live path was verified with Claude Code 2.1.267, the Kiro-Pro
Expand Down
1 change: 1 addition & 0 deletions adapters/antigravity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions adapters/grok.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
23 changes: 21 additions & 2 deletions docs/harness-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,41 @@ to the official documentation used for each entry.
| Claude Code | `~/.claude/skills/` | `.claude/skills/` | `name`, `description`, `license`, `compatibility`, `metadata`, and Claude invocation fields |
| OpenCode | `~/.agents/skills/`, `~/.config/opencode/skills/`, `~/.claude/skills/` | `.opencode/skills/` | `license`, `compatibility`, and `metadata` |
| pi | `~/.agents/skills/`, `~/.pi/agent/skills/` | `.pi/skills/` | `license`, `compatibility`, `metadata`, `allowed-tools`, and `disable-model-invocation` |
| Antigravity (`agy`) | `~/.gemini/config/skills/`, `~/.gemini/antigravity-cli/skills/` | `.gemini/skills/`, `.agents/skills/`, `.agent/skills/` | `name` (required) and `description`; other fields are not documented |
| Grok | `~/.grok/skills/`, `~/.agents/skills/`, `~/.claude/skills/`, `~/.cursor/skills/` | `.grok/skills/`, `.agents/skills/`, `.claude/skills/`, `.cursor/skills/` | `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`, `user-invocable`, and `disable-model-invocation` |
| Cursor | `~/.cursor/skills/`, `~/.agents/skills/` | `.cursor/skills/`, `.agents/skills/`, `.claude/skills/` | Agent Skills fields |

Sources:

- [Codex skills](https://learn.chatgpt.com/docs/build-skills) and [Codex subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents)
- [Claude Code skills](https://code.claude.com/docs/en/skills)
- [OpenCode skills](https://opencode.ai/docs/skills)
- [pi skills](https://pi.dev/docs/latest/skills)
- Antigravity: the `agy` 1.2.16 CLI and T3 Code's skill discovery for it,
[`AntigravitySkills.ts`](https://github.com/pingdotgg/t3code/blob/main/apps/server/src/provider/Drivers/AntigravitySkills.ts)
and the [Antigravity provider guide](https://github.com/pingdotgg/t3code/blob/main/docs/user/providers-antigravity.md).
Antigravity publishes no skill-path reference, so the roots above come from
that implementation.
- Grok: the Grok Build user guide shipped with the CLI (`docs/user-guide/08-skills.md`
under the Grok home directory), checked against `grok inspect --json` on 1.0.46,
which listed skills from `~/.agents/skills/`.
- [Cursor skills](https://cursor.com/docs/context/skills)

The canonical tree keeps the Agent Skills fields that all supported Harnesses
can read. Claude Code accepts `metadata` but does not act on its contents, so
`adapters/claude.json` removes that map and surfaces the logger requirement in
the `compatibility` field instead. OpenCode and pi retain `metadata` because
their official references support it.

mstack installs one canonical copy in `~/.agents/skills/` for Codex, OpenCode
and pi. Claude output omits frontmatter `name`; Claude's documented fallback
mstack installs one canonical copy in `~/.agents/skills/` for Codex, OpenCode,
pi and Grok. Cursor reads that directory as well, so it needs no installer
target. Antigravity does not read `~/.agents/skills/` (the agent treats
`.agents/skills/` only as a project directory), so its harness has its own
canonical copy under `~/.gemini/antigravity-cli/skills/`. Antigravity requires
the `name` field and a file named `SKILL.md`, which the canonical skills already
satisfy, so `adapters/antigravity.json` and `adapters/grok.json` are empty. Grok
also scans `~/.claude/skills/`; skills with the same name collapse to one entry.
Claude output omits frontmatter `name`; Claude's documented fallback
uses the directory name, so `/meta-mode` keeps its name. OpenCode requires an
explicit name before registration and skips the Claude copy. This behavior
was checked with OpenCode 1.18.30 using a named Claude control skill and an
Expand Down
24 changes: 18 additions & 6 deletions profiles/environments.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@
"codex": "C:\\path\\to\\Fleet\\codex\\skills",
"claude": "C:\\path\\to\\Fleet\\claude\\skills",
"opencode": "C:\\path\\to\\Fleet\\opencode\\skills",
"pi": "C:\\path\\to\\Fleet\\pi\\agent\\skills"
"pi": "C:\\path\\to\\Fleet\\pi\\agent\\skills",
"antigravity": "C:\\path\\to\\Fleet\\antigravity\\skills",
"grok": "C:\\path\\to\\Fleet\\grok\\skills"
},
"artifacts": {
"agents": {
"codex": "C:\\path\\to\\Fleet\\codex\\agents",
"claude": "C:\\path\\to\\Fleet\\shared\\agents",
"opencode": "C:\\path\\to\\Fleet\\shared\\agents",
"pi": "C:\\path\\to\\Fleet\\shared\\agents"
"pi": "C:\\path\\to\\Fleet\\shared\\agents",
"antigravity": "C:\\path\\to\\Fleet\\antigravity\\agents",
"grok": "C:\\path\\to\\Fleet\\grok\\agents"
},
"meta-mode-tools": {
"codex": "C:\\path\\to\\Fleet\\shared\\tools\\meta-mode",
"claude": "C:\\path\\to\\Fleet\\shared\\tools\\meta-mode",
"opencode": "C:\\path\\to\\Fleet\\shared\\tools\\meta-mode",
"pi": "C:\\path\\to\\Fleet\\shared\\tools\\meta-mode"
"pi": "C:\\path\\to\\Fleet\\shared\\tools\\meta-mode",
"antigravity": "C:\\path\\to\\Fleet\\shared\\tools\\meta-mode",
"grok": "C:\\path\\to\\Fleet\\shared\\tools\\meta-mode"
}
}
},
Expand All @@ -31,20 +37,26 @@
"codex": "/home/dev/.agents/skills",
"claude": "/home/dev/.claude/skills",
"opencode": "/home/dev/.agents/skills",
"pi": "/home/dev/.agents/skills"
"pi": "/home/dev/.agents/skills",
"antigravity": "/home/dev/.gemini/antigravity-cli/skills",
"grok": "/home/dev/.agents/skills"
},
"artifacts": {
"agents": {
"codex": "/home/dev/.codex/agents",
"claude": "/home/dev/.claude/agents",
"opencode": "/home/dev/.config/opencode/agents",
"pi": "/home/dev/.pi/agent/agents"
"pi": "/home/dev/.pi/agent/agents",
"antigravity": "/home/dev/.gemini/antigravity-cli/agents",
"grok": "/home/dev/.grok/agents"
},
"meta-mode-tools": {
"codex": "/home/dev/.agents/tools/meta-mode",
"claude": "/home/dev/.claude/tools/meta-mode",
"opencode": "/home/dev/.agents/tools/meta-mode",
"pi": "/home/dev/.agents/tools/meta-mode"
"pi": "/home/dev/.agents/tools/meta-mode",
"antigravity": "/home/dev/.gemini/antigravity-cli/tools/meta-mode",
"grok": "/home/dev/.agents/tools/meta-mode"
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions profiles/harnesses.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,28 @@
"modelArgs": ["--model", "{model}"],
"readOnlyArgs": ["--tools", "read,grep,find,ls"]
}
},
"antigravity": {
"directoryVariable": "HARNESS_SKILLS_ANTIGRAVITY_DIR",
"fallback": [".gemini", "antigravity-cli", "skills"],
"runtime": {
"command": "agy",
"headlessArgs": ["--output-format", "text"],
"promptArgs": ["--print"],
"modelArgs": ["--model", "{model}"],
"readOnlyArgs": ["--mode", "plan"]
}
},
"grok": {
"directoryVariable": "HARNESS_SKILLS_GROK_DIR",
"sharedSkills": true,
"fallback": [".grok", "skills"],
"runtime": {
"command": "grok",
"headlessArgs": ["--output-format", "plain"],
"promptArgs": ["--single"],
"modelArgs": ["--model", "{model}"],
"readOnlyArgs": ["--tools", "read_file,list_dir,grep", "--disallowed-tools", "search_tool,use_tool"]
}
}
}
4 changes: 3 additions & 1 deletion profiles/models.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"codex": {},
"claude": {},
"opencode": {},
"pi": {}
"pi": {},
"antigravity": {},
"grok": {}
}
}
37 changes: 31 additions & 6 deletions scripts/harness-targets.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,39 @@ const home = resolve("fixture-home");
test("default skills share a root while native roots retain harness-specific locations", () => {
const roots = resolveHarnessRoots(registry, { env: {}, home });
const shared = join(home, ".agents", "skills");
assert.deepEqual(roots.skills, { codex: shared, claude: join(home, ".claude", "skills"), opencode: shared, pi: shared });
assert.deepEqual(roots.skills, {
codex: shared,
claude: join(home, ".claude", "skills"),
opencode: shared,
pi: shared,
antigravity: join(home, ".gemini", "antigravity-cli", "skills"),
grok: shared,
});
assert.deepEqual(roots.native, {
codex: shared,
claude: join(home, ".claude", "skills"),
opencode: join(home, ".config", "opencode", "skills"),
pi: join(home, ".pi", "agent", "skills"),
antigravity: join(home, ".gemini", "antigravity-cli", "skills"),
grok: join(home, ".grok", "skills"),
});
assert.deepEqual(roots.legacy, roots.native);
assert.equal(roots.externalClaudeRoot, join(home, ".claude", "skills"));
});

test("Grok and Antigravity directory variables override roots and only Grok shares by default", () => {
const roots = resolveHarnessRoots(registry, {
home,
env: { HARNESS_SKILLS_GROK_DIR: "~/grok-explicit", HARNESS_SKILLS_ANTIGRAVITY_DIR: "~/antigravity-explicit" },
});
assert.equal(roots.skills.grok, join(home, "grok-explicit"));
assert.equal(roots.native.grok, join(home, "grok-explicit"));
assert.equal(roots.legacy.grok, join(home, ".grok", "skills"));
assert.equal(roots.skills.antigravity, join(home, "antigravity-explicit"));
assert.equal(roots.legacy.antigravity, join(home, ".gemini", "antigravity-cli", "skills"));
assert.throws(() => resolveHarnessRoots(registry, { home, env: { HARNESS_SKILLS_GROK_DIR: "relative" } }), /HARNESS_SKILLS_GROK_DIR must be an absolute path/);
});

test("configuration roots affect native and legacy locations without moving shared skills", () => {
const roots = resolveHarnessRoots(registry, {
home,
Expand Down Expand Up @@ -66,8 +88,8 @@ for (const explicit of [false, true]) {
t.after(() => rmSync(root, { recursive: true, force: true }));
const bin = join(root, "bin");
mkdirSync(bin);
for (const harness of Object.keys(registry)) {
const executable = join(bin, `${harness}${process.platform === "win32" ? ".ps1" : ""}`);
for (const { runtime } of Object.values(registry)) {
const executable = join(bin, `${runtime.command}${process.platform === "win32" ? ".ps1" : ""}`);
writeFileSync(executable, process.platform === "win32"
? 'if ($args.Count -eq 1 -and $args[0] -eq "--version") { Write-Output "fixture-cli 1.0"; exit 0 }; exit 9\n'
: '#!/bin/sh\nif [ "$#" = 1 ] && [ "$1" = "--version" ]; then printf "fixture-cli 1.0\\n"; exit 0; fi\nexit 9\n');
Expand All @@ -79,17 +101,20 @@ for (const explicit of [false, true]) {
if (explicit) {
env.HARNESS_SKILLS_PI_DIR = join(root, "custom-pi-skills");
env.HARNESS_SKILLS_OPENCODE_DIR = join(root, "custom-opencode-skills");
env.HARNESS_SKILLS_GROK_DIR = join(root, "custom-grok-skills");
env.HARNESS_SKILLS_ANTIGRAVITY_DIR = join(root, "custom-antigravity-skills");
}
const installed = spawnSync(process.execPath, [join(repository, "scripts", "install.mjs"), "--harness", "all", "--skill", "bro"], { cwd: root, env, encoding: "utf8" });
assert.equal(installed.status, 0, installed.stderr);
const smoked = spawnSync(process.execPath, [join(repository, "scripts", "smoke-harnesses.mjs"), "--harness", "all", "--skill", "bro", "--require-installed", "--json"], { cwd: root, env, encoding: "utf8" });
assert.equal(smoked.status, 0, `${smoked.stderr}\n${smoked.stdout}`);
const entries = JSON.parse(smoked.stdout);
assert.equal(entries.length, 4);
assert.equal(entries.length, 6);
for (const entry of entries) {
const expected = entry.harness === "claude" ? join(root, ".claude", "skills")
: explicit && ["pi", "opencode"].includes(entry.harness) ? join(root, `custom-${entry.harness}-skills`)
: join(root, ".agents", "skills");
: entry.harness === "antigravity" && !explicit ? join(root, ".gemini", "antigravity-cli", "skills")
: explicit && ["pi", "opencode", "grok", "antigravity"].includes(entry.harness) ? join(root, `custom-${entry.harness}-skills`)
: join(root, ".agents", "skills");
assert.equal(entry.target, expected, entry.harness);
assert.equal(entry.installed, true, entry.harness);
assert.equal(entry.cli, "available", entry.harness);
Expand Down
5 changes: 3 additions & 2 deletions scripts/install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ function valueAfter(flag) {
}

if (args.includes("--help") || !args.includes("--harness")) {
console.log(`Usage: node scripts/install.mjs --harness <codex|claude|opencode|pi|all> [--environment <name>] [--skill <name[,name...]>] [--artifact <name[,name...]>] [--no-skills] [--dry-run] [--replace]
console.log(`Usage: node scripts/install.mjs --harness <codex|claude|opencode|pi|antigravity|grok|all> [--environment <name>] [--skill <name[,name...]>] [--artifact <name[,name...]>] [--no-skills] [--dry-run] [--replace]

Installs canonical skills and optional artifacts into user-level harness directories.
Artifacts: ${validArtifacts.join(", ")} (use --artifact all for installable artifacts).
Codex, OpenCode and pi share ~/.agents/skills by default. Claude uses directory-based names.
Codex, OpenCode, pi and Grok share ~/.agents/skills by default. Antigravity reads only its own
~/.gemini/antigravity-cli/skills. Claude uses directory-based names.
Use --migrate --replace to back up recognized legacy copies outside skill discovery roots.
Existing directories are preserved unless --replace is supplied.`);
process.exit(args.includes("--help") ? 0 : 1);
Expand Down
13 changes: 12 additions & 1 deletion scripts/install.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ function fixture() {
HARNESS_SKILLS_CLAUDE_DIR: join(root, "claude skills"),
HARNESS_SKILLS_OPENCODE_DIR: join(root, "opencode skills"),
HARNESS_SKILLS_PI_DIR: join(root, "pi skills"),
HARNESS_SKILLS_ANTIGRAVITY_DIR: join(root, "antigravity skills"),
HARNESS_SKILLS_GROK_DIR: join(root, "grok skills"),
},
};
}
Expand Down Expand Up @@ -115,6 +117,8 @@ test("dry-run reports a plan without creating target directories", () => {
assert.equal(existsSync(env.HARNESS_SKILLS_CODEX_DIR), false);
assert.equal(existsSync(env.HARNESS_SKILLS_CLAUDE_DIR), false);
assert.equal(existsSync(env.HARNESS_SKILLS_OPENCODE_DIR), false);
assert.equal(existsSync(env.HARNESS_SKILLS_ANTIGRAVITY_DIR), false);
assert.equal(existsSync(env.HARNESS_SKILLS_GROK_DIR), false);
} finally {
rmSync(root, { recursive: true, force: true });
}
Expand Down Expand Up @@ -170,15 +174,20 @@ test("installs harness-specific frontmatter and preserves conflicts", () => {
const claudePath = join(env.HARNESS_SKILLS_CLAUDE_DIR, "show-me-your-work", "SKILL.md");
const opencodePath = join(env.HARNESS_SKILLS_OPENCODE_DIR, "show-me-your-work", "SKILL.md");
const piPath = join(env.HARNESS_SKILLS_PI_DIR, "show-me-your-work", "SKILL.md");
const antigravityPath = join(env.HARNESS_SKILLS_ANTIGRAVITY_DIR, "show-me-your-work", "SKILL.md");
const grokPath = join(env.HARNESS_SKILLS_GROK_DIR, "show-me-your-work", "SKILL.md");
const codex = readFileSync(codexPath, "utf8");
const claude = readFileSync(claudePath, "utf8");
const opencode = readFileSync(opencodePath, "utf8");
const pi = readFileSync(piPath, "utf8");
const antigravity = readFileSync(antigravityPath, "utf8");
const grok = readFileSync(grokPath, "utf8");
assert.match(codex, /^metadata:/m);
assert.doesNotMatch(codex, /^compatibility:/m);
assert.match(claude, /^compatibility:/m);
assert.doesNotMatch(claude, /^metadata:/m);
for (const adapted of [opencode, pi]) {
for (const adapted of [opencode, pi, antigravity, grok]) {
assert.match(adapted, /^name: show-me-your-work$/m);
assert.match(adapted, /^metadata:/m);
assert.match(adapted, /^ requirements: Node\.js 18 or newer for scripts\/log\.mjs$/m);
assert.doesNotMatch(adapted, /^compatibility:/m);
Expand Down Expand Up @@ -315,6 +324,8 @@ test("installs only selected skills", () => {
env.HARNESS_SKILLS_CLAUDE_DIR,
env.HARNESS_SKILLS_OPENCODE_DIR,
env.HARNESS_SKILLS_PI_DIR,
env.HARNESS_SKILLS_ANTIGRAVITY_DIR,
env.HARNESS_SKILLS_GROK_DIR,
]) {
assert.deepEqual(readdirSync(target).sort(), [
"codebase-design",
Expand Down
Loading
Loading