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

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