diff --git a/.changeset/791-antigravity-agents-dir.md b/.changeset/791-antigravity-agents-dir.md new file mode 100644 index 000000000..b6917e902 --- /dev/null +++ b/.changeset/791-antigravity-agents-dir.md @@ -0,0 +1,6 @@ +--- +type: Changed +pr: 1090 +--- + +**Antigravity workspace skills now install to the canonical `.agents/` directory** — fresh installs write workspace artifacts under `.agents/` (the Google-Codelabs-documented base) instead of `.agent/`; the legacy `.agent/` layout is still recognized so existing installs keep working. The global `~/.gemini/antigravity/` path is unchanged. (#1090) diff --git a/bin/install.js b/bin/install.js index 55b0fcaae..34349fec5 100755 --- a/bin/install.js +++ b/bin/install.js @@ -465,7 +465,7 @@ function getDirName(runtime) { if (runtime === 'gemini') return '.gemini'; if (runtime === 'kilo') return '.kilo'; if (runtime === 'codex') return '.codex'; - if (runtime === 'antigravity') return '.agent'; + if (runtime === 'antigravity') return '.agents'; if (runtime === 'cursor') return '.cursor'; if (runtime === 'windsurf') return '.windsurf'; if (runtime === 'augment') return '.augment'; @@ -500,7 +500,7 @@ function getConfigDirFromHome(runtime, isGlobal) { if (runtime === 'kilo') return "'.config', 'kilo'"; if (runtime === 'codex') return "'.codex'"; if (runtime === 'antigravity') { - if (!isGlobal) return "'.agent'"; + if (!isGlobal) return "'.agents'"; const antigravityDir = resolveAntigravityGlobalDir(); const rel = path.relative(os.homedir(), antigravityDir); const segments = rel.split(path.sep).filter(Boolean); @@ -2210,8 +2210,8 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) { /** * Apply Antigravity-specific content conversion — path replacement + command name conversion. * Path mappings depend on install mode: - * Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agent/ - * Local: ~/.claude/ → .agent/, ./.claude/ → ./.agent/ + * Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/ + * Local: ~/.claude/ → .agents/, ./.claude/ → ./.agents/ * Applied to ALL Antigravity content (skills, agents, engine files). * @param {string} content - Source content to convert * @param {boolean} [isGlobal=false] - Whether this is a global install @@ -2225,14 +2225,14 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) { c = c.replace(/\$HOME\/\.claude\b/g, '$HOME/.gemini/antigravity'); c = c.replace(/~\/\.claude\b/g, '~/.gemini/antigravity'); } else { - c = c.replace(/\$HOME\/\.claude\//g, '.agent/'); - c = c.replace(/~\/\.claude\//g, '.agent/'); + c = c.replace(/\$HOME\/\.claude\//g, '.agents/'); + c = c.replace(/~\/\.claude\//g, '.agents/'); // Bare form (no trailing slash) — must come after slash form to avoid double-replace - c = c.replace(/\$HOME\/\.claude\b/g, '.agent'); - c = c.replace(/~\/\.claude\b/g, '.agent'); + c = c.replace(/\$HOME\/\.claude\b/g, '.agents'); + c = c.replace(/~\/\.claude\b/g, '.agents'); } - c = c.replace(/\.\/\.claude\//g, './.agent/'); - c = c.replace(/\.claude\//g, '.agent/'); + c = c.replace(/\.\/\.claude\//g, './.agents/'); + c = c.replace(/\.claude\//g, '.agents/'); // Command name conversion (all gsd: references → gsd-) c = c.replace(/gsd:/g, 'gsd-'); // Runtime-neutral agent name replacement (#766) @@ -9437,6 +9437,10 @@ function install(isGlobal, runtime = 'claude', options = {}) { // Get the target directory based on runtime and install type. // Cline local installs write to the project root (like Claude Code) — .clinerules // lives at the root, not inside a .cline/ subdirectory. + // #791: antigravity local installs write to .agents/ (canonical). The legacy .agent/ + // directory is recognized by RUNTIME_DIRS (update-context) and _LEGACY_SCAN_SUBDIR_NAMES + // but NOT auto-removed here; legacy .agent/ gsd artifacts are recognized but not + // auto-removed on reinstall (dual-read fallback per issue #791 spec). const targetDir = isGlobal ? getGlobalConfigDir(runtime, explicitConfigDir) : isCline @@ -11900,7 +11904,8 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [ '.codex', '.copilot', '.github', // copilot local form - '.agent', // antigravity local form + '.agents', // antigravity local form (canonical, #791) + '.agent', // antigravity local form (legacy, backward-compat) '.cursor', '.windsurf', '.codeium/windsurf', diff --git a/docs/installer-migrations.md b/docs/installer-migrations.md index da1251338..7ba863c56 100644 --- a/docs/installer-migrations.md +++ b/docs/installer-migrations.md @@ -371,7 +371,7 @@ for the new shape before changing migration behavior. | Kimi CLI | Agent Skills in `skills/gsd-*/SKILL.md`; explicit custom agent YAML/prompt artifacts in `agents/gsd.yaml`, `agents/gsd.md`, and `agents/subagents/gsd-*`; `gsd-core/` payload files referenced by generated skills; manifest, pristine, local-patch, and migration journal files from the normal installer safety pipeline | Global `KIMI_CONFIG_DIR`, explicit `--config-dir`, or first-existing generic skills root: `~/.config/agents` when `~/.config/agents/skills` exists or no generic skills root exists yet, otherwise `~/.agents` when `~/.agents/skills` exists and `~/.config/agents/skills` does not; `KIMI_CONFIG_DIR` and `--config-dir` are GSD write-location overrides and arbitrary roots require Kimi-side `--skills-dir` or `extra_skill_dirs` configuration for skill discovery; local `--kimi --local` is guarded and writes no project-level artifacts | GSD owns only generated `skills/gsd-*`, `agents/gsd.*`, `agents/subagents/gsd-*`, installed `gsd-core/` payload files, and manifest/preservation/migration records. GSD does not own Kimi config files, hooks, settings, rules, statusline, update-banner registration, or non-GSD Kimi skills/agents. Reinstall/update must preserve locally modified generated Kimi artifacts through manifest-backed `gsd-local-patches/`; uninstall removes only GSD-owned Kimi artifacts and preserves non-GSD user content. | [Agent Skills](https://moonshotai.github.io/kimi-cli/en/customization/skills.html), [Agents and Subagents](https://moonshotai.github.io/kimi-cli/en/customization/agents.html), [Tools](https://moonshotai.github.io/kimi-code/en/reference/tools.html); docs checked 2026-06-07 | | Codex | Skills in `skills/gsd-*/SKILL.md`; agents as source markdown plus per-agent TOML in `agents/`; `[agents.gsd-*]` and hooks in `config.toml` | Global `CODEX_HOME` or `~/.codex`; local `./.codex` | GSD owns generated skills, generated agent TOML, `agents.gsd-*` config sections, `[features].hooks` when added by GSD (canonical; legacy alias `codex_hooks` is recognized and migrated forward, #3566), and GSD hook entries | [Codex config schema](https://developers.openai.com/codex/config-schema.json), [Codex developer docs](https://developers.openai.com/codex/); docs not versioned, checked 2026-05-15; installer compatibility sentinel: Codex 0.130.0 features.hooks key (legacy `codex_hooks` recognized) | | GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR`, `COPILOT_HOME`, or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 | -| Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agent` | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. | +| Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agents` (canonical, #791) or `./.agent` (legacy, recognized for backward-compat) | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. Fresh installs write to `.agents/` (the Google-Codelabs-documented form); existing `.agent/` installs continue to be detected and served. | | Cursor | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/`; lifecycle hooks via `hooks.json` (sessionStart + postToolUse, #777) | Global `CURSOR_CONFIG_DIR` or `~/.cursor`; local `./.cursor` | GSD owns generated skills/agents, GSD rule files or references, and GSD-managed `hooks.json` entries (sentinel `gsd-managed:true`); no statusline ownership | [Cursor rules](https://docs.cursor.com/context/rules); [Cursor hooks](https://docs.cursor.com/context/hooks); docs not versioned, checked 2026-06-07 | | Windsurf | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `WINDSURF_CONFIG_DIR` or `~/.codeium/windsurf`; local `./.windsurf` | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Windsurf public rule docs were source-limited in search results as of 2026-05-11; installer targets the common workspace rules convention `./.windsurf/rules` and must be rechecked before migrations rewrite rules | | Augment Code | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/` | Global `AUGMENT_CONFIG_DIR` or `~/.augment`; local `./.augment` | GSD owns generated skills/agents only; no hook/statusline ownership | [Augment Agent Skills](https://docs.augmentcode.com/cli/skills), [Augment IDE skills](https://docs.augmentcode.com/using-augment/skills); IDE skills public beta in VS Code 0.789.0+, checked 2026-05-11 | diff --git a/gsd-core/workflows/update.md b/gsd-core/workflows/update.md index 16ffb9be0..9c88fa10d 100644 --- a/gsd-core/workflows/update.md +++ b/gsd-core/workflows/update.md @@ -13,7 +13,7 @@ Detect the installed GSD version, scope, runtime, and config dir. First, derive `PREFERRED_CONFIG_DIR` and `PREFERRED_RUNTIME` from the invoking prompt's `execution_context` path — this is the one input only the workflow knows: - If the path contains `/gsd-core/workflows/update.md`, strip that suffix and store the remainder as `PREFERRED_CONFIG_DIR`. -- Infer `PREFERRED_RUNTIME` from the path: `/.codex/` -> `codex`; `/.gemini/antigravity-ide/`, `/.gemini/antigravity-cli/`, `/.gemini/antigravity/`, `/.agent/` -> `antigravity` (`.agent` is the local Antigravity install dir; see bin/install.js `getDirName('antigravity')`, #503); `/.gemini/` -> `gemini`; `/.config/kilo/` or `/.kilo/` -> `kilo`; `/.config/opencode/` or `/.opencode/` -> `opencode`; otherwise `claude`. +- Infer `PREFERRED_RUNTIME` from the path: `/.codex/` -> `codex`; `/.gemini/antigravity-ide/`, `/.gemini/antigravity-cli/`, `/.gemini/antigravity/`, `/.agents/` or `/.agent/` -> `antigravity` (`.agents` is the canonical local Antigravity install dir (#791); `.agent` is the legacy form (#503); see bin/install.js `getDirName('antigravity')`); `/.gemini/` -> `gemini`; `/.config/kilo/` or `/.kilo/` -> `kilo`; `/.config/opencode/` or `/.opencode/` -> `opencode`; otherwise `claude`. Then resolve the install context via the deterministic projection (#498). **Do NOT re-derive scope, runtime, or version by hand** — `update-context` owns that cascade in tested code (`gsd-core/bin/lib/update-context.cjs`), the same way `check-latest-version` owns the package name (#2992): @@ -443,7 +443,7 @@ for dir in "${CACHE_DIRS[@]}"; do fi done -for dir in .claude .config/opencode .opencode .gemini/antigravity-ide .gemini/antigravity-cli .gemini/antigravity .agent .gemini .config/kilo .kilo .codex .cursor .codeium/windsurf .augment .trae .qwen .hermes .codebuddy .cline; do +for dir in .claude .config/opencode .opencode .gemini/antigravity-ide .gemini/antigravity-cli .gemini/antigravity .agents .agent .gemini .config/kilo .kilo .codex .cursor .codeium/windsurf .augment .trae .qwen .hermes .codebuddy .cline; do rm -f "./$dir/cache/gsd-update-check"*.json rm -f "$HOME/$dir/cache/gsd-update-check"*.json done diff --git a/src/update-context.cts b/src/update-context.cts index e39f6fecc..c11ab5895 100644 --- a/src/update-context.cts +++ b/src/update-context.cts @@ -24,7 +24,8 @@ export const RUNTIME_DIRS: RuntimeDirEntry[] = [ ['antigravity', '.gemini/antigravity-ide'], ['antigravity', '.gemini/antigravity-cli'], ['antigravity', '.gemini/antigravity'], - ['antigravity', '.agent'], // local Antigravity install dir (#503; bin/install.js getDirName('antigravity')) + ['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity')) + ['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs) ['gemini', '.gemini'], ['kilo', '.config/kilo'], ['kilo', '.kilo'], diff --git a/tests/bug-2418-antigravity-bare-path.test.cjs b/tests/bug-2418-antigravity-bare-path.test.cjs index 6a9c6252e..168486158 100644 --- a/tests/bug-2418-antigravity-bare-path.test.cjs +++ b/tests/bug-2418-antigravity-bare-path.test.cjs @@ -73,12 +73,12 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () = }); describe('local install', () => { - test('replaces ~/.claude (bare, no trailing slash) with .agent', () => { + test('replaces ~/.claude (bare, no trailing slash) with .agents', () => { const input = 'configDir = ~/.claude'; const result = convertClaudeToAntigravityContent(input, false); assert.ok( - result.includes('.agent'), - `Expected .agent in output, got: ${result}` + result.includes('.agents'), + `Expected .agents in output, got: ${result}` ); assert.ok( !result.includes('~/.claude'), @@ -86,12 +86,12 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () = ); }); - test('replaces $HOME/.claude (bare, no trailing slash) with .agent', () => { + test('replaces $HOME/.claude (bare, no trailing slash) with .agents', () => { const input = 'export DIR=$HOME/.claude'; const result = convertClaudeToAntigravityContent(input, false); assert.ok( - result.includes('.agent'), - `Expected .agent in output, got: ${result}` + result.includes('.agents'), + `Expected .agents in output, got: ${result}` ); assert.ok( !result.includes('$HOME/.claude'), @@ -102,8 +102,8 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () = test('does not double-replace ~/.claude/ paths', () => { const input = 'See ~/.claude/gsd-core/'; const result = convertClaudeToAntigravityContent(input, false); - // .agent/ should appear exactly once - const count = (result.match(/\.agent\//g) || []).length; + // .agents/ should appear exactly once + const count = (result.match(/\.agents\//g) || []).length; assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`); }); }); diff --git a/tests/bug-2557-gemini-local-hook-paths.test.cjs b/tests/bug-2557-gemini-local-hook-paths.test.cjs index d214672e9..13ac2101a 100644 --- a/tests/bug-2557-gemini-local-hook-paths.test.cjs +++ b/tests/bug-2557-gemini-local-hook-paths.test.cjs @@ -26,7 +26,7 @@ describe('bug #2557: Gemini/Antigravity local hooks use relative paths (not $CLA }); test('Antigravity local prefix is bare dirName', () => { - assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent'); + assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agents' }), '.agents'); }); test('non-Gemini local prefix remains $CLAUDE_PROJECT_DIR anchored', () => { diff --git a/tests/bug-3413-shell-command-projection.test.cjs b/tests/bug-3413-shell-command-projection.test.cjs index 8b0db8ba1..bbc6a2541 100644 --- a/tests/bug-3413-shell-command-projection.test.cjs +++ b/tests/bug-3413-shell-command-projection.test.cjs @@ -112,7 +112,7 @@ describe('bug #3439: shell projection module owns managed-hook policy and legacy test('projectLocalHookPrefix centralizes runtime-specific project-dir interpolation policy', () => { assert.equal(projectLocalHookPrefix({ runtime: 'gemini', dirName: '.gemini' }), '.gemini'); - assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agent' }), '.agent'); + assert.equal(projectLocalHookPrefix({ runtime: 'antigravity', dirName: '.agents' }), '.agents'); assert.equal( projectLocalHookPrefix({ runtime: 'claude', dirName: '.claude' }), '"$CLAUDE_PROJECT_DIR"/.claude', diff --git a/tests/bug-503-update-agent-antigravity-detection.test.cjs b/tests/bug-503-update-agent-antigravity-detection.test.cjs index 2be71257f..fd4a64587 100644 --- a/tests/bug-503-update-agent-antigravity-detection.test.cjs +++ b/tests/bug-503-update-agent-antigravity-detection.test.cjs @@ -49,8 +49,26 @@ function fakeFs(files) { }; } -describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () => { - test('projection resolves a LOCAL ./.agent install to the antigravity runtime', () => { +describe('/gsd:update detects local Antigravity (.agent / .agents) installs (#503 / #791)', () => { + test('projection resolves a LOCAL ./.agents install to the antigravity runtime (#791 canonical)', () => { + const HOME = '/home/u'; + const CWD = '/work/proj'; + const agentsDir = `${CWD}/.agents`; + const ffs = fakeFs({ + [`${agentsDir}/gsd-core/VERSION`]: '1.50.0\n', + [`${agentsDir}/gsd-core/workflows/update.md`]: 'x', + }); + const r = resolveUpdateContext({ home: HOME, cwd: CWD, env: {}, fs: ffs }); + assert.equal( + r.runtime, + 'antigravity', + `a local .agents install must map to the antigravity runtime, got "${r.runtime}"`, + ); + assert.equal(r.scope, 'LOCAL'); + assert.equal(r.installedVersion, '1.50.0'); + }); + + test('projection resolves a LOCAL ./.agent install to the antigravity runtime (#503 backward-compat)', () => { const HOME = '/home/u'; const CWD = '/work/proj'; const agentDir = `${CWD}/.agent`; @@ -62,26 +80,33 @@ describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () => assert.equal( r.runtime, 'antigravity', - `a local .agent install must map to the antigravity runtime, got "${r.runtime}"`, + `a legacy .agent install must still map to the antigravity runtime, got "${r.runtime}"`, ); assert.equal(r.scope, 'LOCAL'); assert.equal(r.installedVersion, '1.40.0'); }); - test('execution_context classifier maps a /.agent/ path to antigravity (update.md)', () => { + test('execution_context classifier maps /.agents/ and /.agent/ paths to antigravity (update.md)', () => { + const hasAgentsClassifierRule = + /\/\.agents\/[^\n]*->[^\n]*antigravity/.test(UPDATE_MD); + assert.ok( + hasAgentsClassifierRule, + 'update.md classifier must map a `/.agents/` path to the `antigravity` runtime', + ); const hasAgentClassifierRule = /\/\.agent\/[^\n]*->[^\n]*antigravity/.test(UPDATE_MD); assert.ok( hasAgentClassifierRule, - 'update.md classifier must map a `/.agent/` path to the `antigravity` runtime', + 'update.md classifier must still map a `/.agent/` path to the `antigravity` runtime (backward-compat)', ); }); - test('every runtime-dir `for dir in` loop in update.md includes .agent', () => { + test('every runtime-dir `for dir in` loop in update.md includes .agents and .agent', () => { // The LOCAL-scope discovery loop moved into the projection (#498); the // post-update cache-clear loop remains inline and still enumerates the // runtime config dirs as a literal `.claude ... .codex` list, so it must - // include .agent or a local Antigravity install keeps a stale indicator. + // include both .agents (canonical, #791) and .agent (legacy, #503) or + // stale indicators could linger. const runtimeDirLoops = UPDATE_MD .split('\n') .filter((l) => /for dir in .*\.claude.*\.codex/.test(l)); @@ -90,9 +115,13 @@ describe('/gsd:update detects local Antigravity (.agent) installs (#503)', () => `expected at least 1 runtime-dir loop in update.md, found ${runtimeDirLoops.length}`, ); for (const loop of runtimeDirLoops) { + assert.ok( + /(^|\s)\.agents(\s|$)/.test(loop), + `every runtime-dir loop must include .agents (canonical), got: ${loop.trim()}`, + ); assert.ok( /(^|\s)\.agent(\s|$)/.test(loop), - `every runtime-dir loop must include .agent, got: ${loop.trim()}`, + `every runtime-dir loop must include .agent (legacy backward-compat), got: ${loop.trim()}`, ); } }); diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index eb8287027..472e96d14 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -40,7 +40,7 @@ const EXPECTED_ALL_HOOKS = [ const RUNTIME_META = { claude: { localDir: '.claude', globalSuffix: '.claude' }, - antigravity: { localDir: '.agent', globalSuffix: path.join('.gemini', 'antigravity') }, + antigravity: { localDir: '.agents', globalSuffix: path.join('.gemini', 'antigravity') }, augment: { localDir: '.augment', globalSuffix: '.augment' }, cline: { localDir: '.cline', globalSuffix: '.cline' }, codebuddy: { localDir: '.codebuddy', globalSuffix: '.codebuddy' }, @@ -111,7 +111,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [] }) { try { const LOCAL_DIR_NAME = { claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo', - codex: '.codex', copilot: '.github', antigravity: '.agent', cursor: '.cursor', + codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor', windsurf: '.windsurf', augment: '.augment', trae: '.trae', qwen: '.qwen', codebuddy: '.codebuddy', cline: '.', }; diff --git a/tests/install.test.cjs b/tests/install.test.cjs index 0dfa93091..51dea95b1 100644 --- a/tests/install.test.cjs +++ b/tests/install.test.cjs @@ -261,7 +261,7 @@ describe('getConfigDirFromHome — spot-checks', () => { assert.strictEqual(getConfigDirFromHome('trae', true), "'.trae'"); }); - test('antigravity returns .agent (local) and legacy fallback global path when no 2.x dirs exist', () => { + test('antigravity returns .agents (local) and legacy fallback global path when no 2.x dirs exist', () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-antigravity-empty-')); const savedHome = process.env.HOME; const savedUserProfile = process.env.USERPROFILE; @@ -270,7 +270,7 @@ describe('getConfigDirFromHome — spot-checks', () => { process.env.HOME = home; process.env.USERPROFILE = home; try { - assert.strictEqual(getConfigDirFromHome('antigravity', false), "'.agent'"); + assert.strictEqual(getConfigDirFromHome('antigravity', false), "'.agents'"); assert.strictEqual(getConfigDirFromHome('antigravity', true), "'.gemini', 'antigravity'"); } finally { if (savedHome === undefined) delete process.env.HOME; @@ -836,6 +836,110 @@ describe('install — changeset CLI lands at scripts/changeset/cli.cjs (#935)', }); }); +// ─── Section N: Antigravity .agents canonical workspace dir (#791) ───────────── +// allow-test-rule: runtime-contract-is-the-product +// Reads deployed agent .md files whose text IS the product surface the +// Antigravity runtime loads at startup (path references, command names). + +describe('antigravity local install writes to .agents/ canonical dir (#791)', () => { + let tmpDir; + let previousCwd; + + beforeEach(() => { + tmpDir = createTempDir('gsd-antigravity-791-'); + previousCwd = process.cwd(); + process.chdir(tmpDir); + }); + + afterEach(() => { + process.chdir(previousCwd); + cleanup(tmpDir); + }); + + test('install writes workspace skills under .agents/skills/', () => { + const result = install(false, 'antigravity'); + const agentsDir = path.join(tmpDir, '.agents'); + assert.strictEqual(result.runtime, 'antigravity'); + assert.ok(fs.existsSync(agentsDir), '.agents/ must be created for local antigravity install'); + const skillsDir = path.join(agentsDir, 'skills'); + assert.ok(fs.existsSync(skillsDir), '.agents/skills/ must exist after install'); + const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); + assert.ok(skillEntries.length > 0, 'at least one gsd-* skill must be installed under .agents/skills/'); + const firstSkill = path.join(skillsDir, skillEntries[0].name, 'SKILL.md'); + assert.ok(fs.existsSync(firstSkill), `SKILL.md must exist at ${firstSkill}`); + }); + + test('installed agent files reference .agents/ not ~/.claude/ or bare .agent/', () => { + // NOTE: skill content is intentionally NOT asserted here. The installer calls + // convertClaudeCommandToAntigravitySkill(content, skillName, runtime, cmdNames) + // where the 3rd arg is the string "antigravity" (truthy), routing local skills + // through the global content branch — a pre-existing quirk tracked separately. + // Agent files are NOT affected: convertClaudeAgentToAntigravityAgent(content, isGlobal) + // receives the boolean isGlobal correctly, so local agents use the local (.agents/) branch. + install(false, 'antigravity'); + const agentsDest = path.join(tmpDir, '.agents', 'agents'); + assert.ok(fs.existsSync(agentsDest), '.agents/agents/ must exist after local install'); + const agentFiles = fs.readdirSync(agentsDest) + .filter(f => f.startsWith('gsd-') && f.endsWith('.md')); + assert.ok(agentFiles.length > 0, 'pre-condition: at least one gsd-* agent must be installed'); + for (const file of agentFiles) { + const content = fs.readFileSync(path.join(agentsDest, file), 'utf8'); + // Local agent content must not contain global home-dir paths (should be .agents/) + assert.ok( + !content.includes('~/.claude/') && !content.includes('$HOME/.claude/'), + `${file} must not contain ~/.claude/ or $HOME/.claude/ in a local install; content uses .agents/`, + ); + // Local agent content must not reference the legacy singular .agent/ path + const bareAgentRefs = content.match(/(? { + install(false, 'antigravity'); + const legacyDir = path.join(tmpDir, '.agent'); + assert.ok(!fs.existsSync(legacyDir), + '.agent/ must not be created by a fresh install (new installs use .agents/)'); + }); + + test('global antigravity install still writes to ~/.gemini/antigravity (unchanged)', () => { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ag-global-')); + const savedHome = process.env.HOME; + const savedUserProfile = process.env.USERPROFILE; + const savedAntigravityConfig = process.env.ANTIGRAVITY_CONFIG_DIR; + delete process.env.ANTIGRAVITY_CONFIG_DIR; + process.env.HOME = homeDir; + process.env.USERPROFILE = homeDir; + try { + const result = install(true, 'antigravity'); + assert.strictEqual(result.runtime, 'antigravity'); + assert.ok( + result.configDir.startsWith(homeDir), + `global antigravity install must go under HOME, got: ${result.configDir}`, + ); + assert.ok( + fs.existsSync(path.join(result.configDir, 'skills')), + 'global antigravity install must create skills/ under ~/.gemini/antigravity', + ); + assert.ok( + !fs.existsSync(path.join(homeDir, '.agents')), + '.agents/ must NOT be created by a global install (global path is ~/.gemini/antigravity)', + ); + } finally { + if (savedHome === undefined) delete process.env.HOME; + else process.env.HOME = savedHome; + if (savedUserProfile === undefined) delete process.env.USERPROFILE; + else process.env.USERPROFILE = savedUserProfile; + if (savedAntigravityConfig === undefined) delete process.env.ANTIGRAVITY_CONFIG_DIR; + else process.env.ANTIGRAVITY_CONFIG_DIR = savedAntigravityConfig; + cleanup(homeDir); + } + }); +}); // ─── Section 6: Windsurf / devin-desktop alias (#792) ─────────────────────── describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792)', () => {