diff --git a/README.md b/README.md index 711b669..cb20b11 100644 --- a/README.md +++ b/README.md @@ -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`: @@ -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 @@ -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. @@ -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. @@ -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 diff --git a/adapters/antigravity.json b/adapters/antigravity.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/adapters/antigravity.json @@ -0,0 +1 @@ +{} diff --git a/adapters/grok.json b/adapters/grok.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/adapters/grok.json @@ -0,0 +1 @@ +{} diff --git a/docs/harness-adapters.md b/docs/harness-adapters.md index 0eb96cd..95a49b2 100644 --- a/docs/harness-adapters.md +++ b/docs/harness-adapters.md @@ -11,6 +11,9 @@ 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: @@ -18,6 +21,15 @@ Sources: - [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 @@ -25,8 +37,15 @@ can read. Claude Code accepts `metadata` but does not act on its contents, so 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 diff --git a/profiles/environments.example.json b/profiles/environments.example.json index 59d16c2..c39e894 100644 --- a/profiles/environments.example.json +++ b/profiles/environments.example.json @@ -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" } } }, @@ -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" } } } diff --git a/profiles/harnesses.json b/profiles/harnesses.json index 967a473..2a6ec61 100644 --- a/profiles/harnesses.json +++ b/profiles/harnesses.json @@ -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"] + } } } diff --git a/profiles/models.example.json b/profiles/models.example.json index b17b802..363941c 100644 --- a/profiles/models.example.json +++ b/profiles/models.example.json @@ -12,6 +12,8 @@ "codex": {}, "claude": {}, "opencode": {}, - "pi": {} + "pi": {}, + "antigravity": {}, + "grok": {} } } diff --git a/scripts/harness-targets.test.mjs b/scripts/harness-targets.test.mjs index 6c4c6d7..28b4699 100644 --- a/scripts/harness-targets.test.mjs +++ b/scripts/harness-targets.test.mjs @@ -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, @@ -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'); @@ -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); diff --git a/scripts/install.mjs b/scripts/install.mjs index 7cbdb39..7d655b4 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -56,11 +56,12 @@ function valueAfter(flag) { } if (args.includes("--help") || !args.includes("--harness")) { - console.log(`Usage: node scripts/install.mjs --harness [--environment ] [--skill ] [--artifact ] [--no-skills] [--dry-run] [--replace] + console.log(`Usage: node scripts/install.mjs --harness [--environment ] [--skill ] [--artifact ] [--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); diff --git a/scripts/install.test.mjs b/scripts/install.test.mjs index c51b283..da15305 100644 --- a/scripts/install.test.mjs +++ b/scripts/install.test.mjs @@ -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"), }, }; } @@ -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 }); } @@ -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); @@ -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", diff --git a/scripts/model-config.test.mjs b/scripts/model-config.test.mjs index 83693aa..41a9848 100644 --- a/scripts/model-config.test.mjs +++ b/scripts/model-config.test.mjs @@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process"; import test from "node:test"; import { readModelConfig, validateModelConfig } from "./model-config-lib.mjs"; -const harnesses = ["codex", "claude", "opencode", "pi"]; +const harnesses = ["codex", "claude", "opencode", "pi", "antigravity", "grok"]; const runRole = resolve("scripts/run-role.mjs"); const checkModels = resolve("scripts/model-config.mjs"); @@ -85,6 +85,20 @@ test("list execution requires explicit selection and preserves model arguments f } }); +test("prompt-valued flags sit directly before the prompt so model and read-only flags cannot be swallowed", (t) => { + const { file, run } = fixture(t, { roles: { reviewer: "review-a" } }); + const plan = (harness, ...extra) => JSON.parse(run([ + "--harness", harness, "--role", "reviewer", "--prompt", "inspect", "--file", file, ...extra, + ]).stdout); + assert.deepEqual(plan("antigravity", "--read-only").args, + ["--output-format", "text", "--model", "review-a", "--print", "inspect", "--mode", "plan"]); + assert.deepEqual(plan("grok", "--read-only").args, [ + "--output-format", "plain", "--model", "review-a", "--single", "inspect", + "--tools", "read_file,list_dir,grep", "--disallowed-tools", "search_tool,use_tool", + ]); + assert.deepEqual(plan("grok", "--model", "auto").args, ["--output-format", "plain", "--single", "inspect"]); +}); + test("inherit-parent requires the parent model while auto selects the CLI default", (t) => { const { file, run } = fixture(t, { roles: { reviewer: "inherit-parent" } }); const args = ["--harness", "pi", "--role", "reviewer", "--prompt", "inspect", "--file", file]; diff --git a/scripts/run-role.mjs b/scripts/run-role.mjs index b768265..8783451 100644 --- a/scripts/run-role.mjs +++ b/scripts/run-role.mjs @@ -80,7 +80,8 @@ const models = selectedModels.map((model) => { return parentModel; }); const definition = harnessRegistry[harness].runtime; -if (!definition?.command || !Array.isArray(definition.headlessArgs) || !Array.isArray(definition.modelArgs)) { +if (!definition?.command || !Array.isArray(definition.headlessArgs) || !Array.isArray(definition.modelArgs) || + (definition.promptArgs !== undefined && !Array.isArray(definition.promptArgs))) { throw new Error(`Harness ${harness} has no runtime command definition`); } @@ -89,7 +90,8 @@ const cwd = environmentCwd(environment, harness, options["--cwd"]); const plans = models.map((model) => { const commandArgs = [...definition.headlessArgs]; if (model !== "auto") commandArgs.push(...definition.modelArgs.map((part) => part.replace("{model}", model))); - commandArgs.push(prompt); + // promptArgs holds a flag that takes the prompt as its value (agy --print, grok --single), so it must sit directly before the prompt. + commandArgs.push(...(definition.promptArgs ?? []), prompt); if (options["--read-only"]) commandArgs.push(...(definition.readOnlyArgs ?? [])); const invocation = environment.transport === "ssh" ? buildSshInvocation(environment, definition.command, commandArgs, cwd) diff --git a/scripts/skill-discovery.test.mjs b/scripts/skill-discovery.test.mjs index 894fa96..80dc169 100644 --- a/scripts/skill-discovery.test.mjs +++ b/scripts/skill-discovery.test.mjs @@ -46,15 +46,49 @@ test("all harnesses install two canonical skills and two directory-named Claude ".agents/skills/show-me-your-work/SKILL.md", ".claude/skills/meta-mode/SKILL.md", ".claude/skills/show-me-your-work/SKILL.md", + ".gemini/antigravity-cli/skills/meta-mode/SKILL.md", + ".gemini/antigravity-cli/skills/show-me-your-work/SKILL.md", ]); for (const skill of ["meta-mode", "show-me-your-work"]) { - assert.match(installedText(root, ".agents/skills", skill), new RegExp(`^name: ${skill}\\r?$`, "m")); + for (const directory of [".agents/skills", ".gemini/antigravity-cli/skills"]) { + assert.match(installedText(root, directory, skill), new RegExp(`^name: ${skill}\\r?$`, "m")); + } const claude = installedText(root, ".claude/skills", skill); assert.doesNotMatch(claude.split(/^---\s*$/m)[1], /^name:/m); assert.match(claude, /^description:/m); } assert.equal(existsSync(join(root, ".pi/agent/skills")), false); assert.equal(existsSync(join(root, ".config/opencode/skills")), false); + assert.equal(existsSync(join(root, ".grok/skills")), false); +}); + +test("Grok shares the default copy while Antigravity keeps its own native root", (t) => { + const { root, env } = fixture(t); + succeeded(run(["--harness", "grok", "--skill", "meta-mode"], env)); + assert.deepEqual(skillFiles(root), [".agents/skills/meta-mode/SKILL.md"]); + assert.equal(existsSync(join(root, ".grok/skills")), false); + succeeded(run(["--harness", "antigravity", "--skill", "meta-mode"], env)); + assert.deepEqual(skillFiles(root), [ + ".agents/skills/meta-mode/SKILL.md", + ".gemini/antigravity-cli/skills/meta-mode/SKILL.md", + ]); + const shared = installedText(root, ".agents/skills", "meta-mode"); + assert.equal(installedText(root, ".gemini/antigravity-cli/skills", "meta-mode"), shared); + const refreshed = run(["--harness", "codex,opencode,pi,grok", "--skill", "meta-mode", "--replace"], env); + succeeded(refreshed); + assert.match(refreshed.stdout, /shared meta-mode: .* \(codex, opencode, pi, grok\)/); + assert.deepEqual(skillFiles(join(root, ".agents/skills")), ["meta-mode/SKILL.md"]); +}); + +test("explicit Grok and Antigravity roots override their defaults", (t) => { + const { root, env } = fixture(t); + env.HARNESS_SKILLS_GROK_DIR = join(root, "custom-grok/skills"); + env.HARNESS_SKILLS_ANTIGRAVITY_DIR = join(root, "custom-antigravity/skills"); + succeeded(run(["--harness", "grok,antigravity", "--skill", "meta-mode"], env)); + assert.deepEqual(skillFiles(root), [ + "custom-antigravity/skills/meta-mode/SKILL.md", + "custom-grok/skills/meta-mode/SKILL.md", + ]); }); test("repeat replacement and a pi-only refresh keep one current shared copy", (t) => { @@ -67,8 +101,9 @@ test("repeat replacement and a pi-only refresh keep one current shared copy", (t writeFileSync(canonical, `${readFileSync(canonical, "utf8")}\nStale local marker\n`); succeeded(run(["--harness", "pi", "--skill", "meta-mode", "--replace"], env)); assert.equal(readFileSync(canonical, "utf8"), expected); - for (const directory of [".agents/skills", ".claude/skills", ".pi/agent/skills", ".config/opencode/skills"]) { - assert.deepEqual(skillFiles(join(root, directory)), directory.startsWith(".agents") || directory.startsWith(".claude") ? ["meta-mode/SKILL.md"] : []); + for (const directory of [".agents/skills", ".claude/skills", ".gemini/antigravity-cli/skills", ".pi/agent/skills", ".config/opencode/skills", ".grok/skills"]) { + const installed = [".agents", ".claude", ".gemini"].some((prefix) => directory.startsWith(prefix)); + assert.deepEqual(skillFiles(join(root, directory)), installed ? ["meta-mode/SKILL.md"] : []); } }); @@ -80,6 +115,8 @@ test("artifact-only installation preserves native harness roots", (t) => { ".claude/agents/meta-agent.md", ".config/opencode/agents/meta-agent.md", ".pi/agent/agents/meta-agent.md", + ".gemini/antigravity-cli/agents/meta-agent.md", + ".grok/agents/meta-agent.md", ]) assert.equal(existsSync(join(root, file)), true, file); assert.equal(existsSync(join(root, ".agents")), false); }); @@ -109,6 +146,7 @@ test("dry run reports shared destinations without creating files", (t) => { succeeded(result); assert.ok(result.stdout.includes(join(root, ".agents/skills/meta-mode")), result.stdout); assert.ok(result.stdout.includes(join(root, ".claude/skills/meta-mode")), result.stdout); + assert.ok(result.stdout.includes(join(root, ".gemini/antigravity-cli/skills/meta-mode")), result.stdout); assert.deepEqual(readdirSync(root), []); }); @@ -116,12 +154,12 @@ test("optional tools coalesce for all harnesses and resolve relative to a pi ski const { root, env } = fixture(t); const result = run(["--harness", "all", "--no-skills", "--artifact", "meta-mode-tools"], env); succeeded(result); - assert.match(result.stdout, /Installed 0 skill copies and 2 artifact copies/); + assert.match(result.stdout, /Installed 0 skill copies and 3 artifact copies/); const expected = readFileSync(resolve("tools/meta-mode/package.json"), "utf8"); - for (const directory of [".agents/tools/meta-mode", ".claude/tools/meta-mode"]) { + for (const directory of [".agents/tools/meta-mode", ".claude/tools/meta-mode", ".gemini/antigravity-cli/tools/meta-mode"]) { assert.equal(readFileSync(join(root, directory, "package.json"), "utf8"), expected); } - for (const directory of [".pi/agent/tools", ".config/opencode/tools"]) { + for (const directory of [".pi/agent/tools", ".config/opencode/tools", ".grok/tools"]) { assert.equal(existsSync(join(root, directory)), false, directory); } succeeded(run(["--harness", "pi", "--skill", "meta-mode", "--artifact", "meta-mode-tools", "--replace"], env));