feat(#791): migrate antigravity workspace base dir .agent → .agents (#1090)

Fresh antigravity workspace installs write under the canonical .agents/ (plural) base; legacy .agent/ stays recognized (dual-read). Global ~/.gemini/antigravity/ path unchanged. Closes #791.
This commit is contained in:
Tom Boucher
2026-06-11 22:37:55 -04:00
committed by GitHub
parent 94872662e9
commit 77a671ec53
11 changed files with 182 additions and 37 deletions

View File

@@ -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)

View File

@@ -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',

View File

@@ -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 |

View File

@@ -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

View File

@@ -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'],

View File

@@ -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}`);
});
});

View File

@@ -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', () => {

View File

@@ -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',

View File

@@ -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()}`,
);
}
});

View File

@@ -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: '.',
};

View File

@@ -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(/(?<!\w)\.agent(?!s)\//g) || [];
assert.strictEqual(
bareAgentRefs.length, 0,
`${file} must not reference legacy .agent/ path; found: ${bareAgentRefs.join(', ')}`,
);
}
});
test('legacy .agent/ is NOT written on a fresh local install', () => {
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)', () => {