fix(#3738): antigravity global skills/agents install to ~/.gemini/config (#3921)

* test(#3738): antigravity global skills/agents must resolve under ~/.gemini/config

Regression tests (RED first): --skills-root and gsd-tools query surfaces,
install-plan dest dirs, and converter skills-path rewrite.

* fix(#3738): antigravity global skills/agents install to ~/.gemini/config

Antigravity's machine-local discovery scans ~/.gemini/config/{skills,agents};
the configHome (~/.gemini/antigravity) is deprecated for artifacts. Declare the
ADR-1239 skills/agents 'home' override on the antigravity global layout — the
same mechanism codex uses (.agents) — and divert ~/.claude/skills/ references
in converted global content to ~/.gemini/config/skills/. configHome, settings,
probe/migration semantics, and the local .agents layout are unchanged.

* fix(#3738): retire deprecated configHome artifacts via installer migration 010

Next install converges an existing antigravity install: manifest-managed
skills/gsd-*/ and agents/gsd-*.md under the configHome (a location AGY does
not scan) are removed — modified files backed up first, unmanifested and
non-gsd entries preserved — and now-empty containers retired. Global scope
only; the local .agents surface is live. Docs + inventory updated.

* fix(#3738): converter sync in bin/install.js, harness emit-root coverage, migration baseline

- bin/install.js converter gains the same ~/.claude/skills → ~/.gemini/config/
  rewrite as src (ADR-1508 dual copy must stay in sync).
- Parity-manifest walk covers home-override emit roots (extraEmitRootsFor) so
  antigravity's emitted skills/agents stay differential-visible at their new
  install root; install-tree fixture regen confirms an unchanged key set.
- skills-from-commands rule declares the antigravity converter as a
  runtime-scoped transform; one ack fragment covers the identity-classed
  workflow whose antigravity copy embeds the old skills path.
- Migration 010 checksum baseline + home-override set doc updated; existing
  tests updated to the #3738 contract (global dest, golden parity via layout
  dest, integration expectations).

* fix(#3738): tolerate an absent extra emit root on baseline-side measurement

The base tree's installer predates the home override, so <HOME>/.gemini/config
does not exist there; walk() threw ENOENT and the in-job baseline build failed.
An absent extra root is the legitimate pre-override shape — skip it.

* fix(#3738): review findings — manifest agents root, bare skills-path rewrite, guard comment

- writeManifest resolves the agents-kind home override (_kindDestDirSafe), so
  the manifest records agents at their actual install root and drift detection
  keeps working (isolated review finding 1, major).
- Converter bare forms ~/.claude/skills and $HOME/.claude/skills (no trailing
  slash) divert to ~/.gemini/config/skills instead of falling through to the
  retired configHome path (finding 2).
- real-home-guard comment updated: antigravity's global agents kind is the
  first agents-kind home override (finding 3, doc-only).
- Regression tests for both behavioral findings.

* chore(#3738): changeset fragment (pr number backfilled after PR creation)

* chore(#3738): backfill changeset PR number (3921)

* fix(#3738): sandbox HOME in tests that install antigravity global artifacts

antigravity is the first home-override runtime in the golden-parity and
skills-wrapper suites (codex is not in their runtime lists), so those tests
never needed HOME sandboxing — the real-home guard now (correctly) refuses
their un-sandboxed global installs on CI, where HOME is the passwd home.

* fix(#3738): stop the K3 sequential-sandbox env leak; sandbox L2's home-override plans

K3's two back-to-back sandboxHome calls leave HOME pointing at the first
sandbox once the after-hooks restore (each call saves the env as it found
it, so the second saves the first's sandbox as 'original'). On the windows
matrix that leaked gsd-k3-qwen-* home into the L2 property, whose
antigravity/global run then (correctly) refused via the #3712 real-home
guard — antigravity is the runtime that made L2's plan escape into
os.homedir(). K3 now manages the env with a single restore; L2 sandboxes
HOME per run, mirroring L1.

* fix(#3738): L2 property's HOME sandbox must exist on disk

The #3712 guard's sandbox exemption fails closed when identify(effectiveHome)
is 'absent' — L2 never created its configDir, so on the windows matrix (tmpdir
under the real home) the antigravity/global run refused even with HOME
sandboxed. Create the per-run sandbox dir and clean it up.

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-27 02:24:03 -04:00
committed by GitHub
parent e20744eacb
commit 39673ae9ff
29 changed files with 936 additions and 55 deletions

View File

@@ -1001,6 +1001,10 @@ describe('skills wrapper threads install scope into converter isGlobal (regressi
const globalDir = createTempDir(`gsd-ial-g-${runtime}-`);
const localDir = createTempDir(`gsd-ial-l-${runtime}-`);
t.after(() => { cleanup(globalDir); cleanup(localDir); });
// #3738: antigravity's global skills kind resolves its `home` override
// from os.homedir(); sandbox HOME (with the #3712 marker) so the global
// install writes inside globalDir instead of the runner's real home.
sandboxHome(t, globalDir);
installRuntimeArtifacts(runtime, globalDir, 'global', RESOLVED_CORE);
installRuntimeArtifacts(runtime, localDir, 'local', RESOLVED_CORE);
@@ -1009,8 +1013,10 @@ describe('skills wrapper threads install scope into converter isGlobal (regressi
const lSkills = resolveRuntimeArtifactLayout(runtime, localDir, 'local').kinds.find(k => k.kind === 'skills');
assert.ok(gSkills && lSkills, `${runtime}: must resolve a skills kind for both scopes`);
const gCombined = readAllSkillMd(path.join(globalDir, gSkills.destSubpath));
const lCombined = readAllSkillMd(path.join(localDir, lSkills.destSubpath));
// #3738: the global skills tree may live under the kind `home` override
// (antigravity → ~/.gemini/config), so honor it like the installer does.
const gCombined = readAllSkillMd(path.join(gSkills.home ?? globalDir, gSkills.destSubpath));
const lCombined = readAllSkillMd(path.join(lSkills.home ?? localDir, lSkills.destSubpath));
// Precondition (non-vacuity guard): some core skill carries a ~/.claude
// reference, so the GLOBAL install surfaces the global home marker. If this
@@ -1108,6 +1114,58 @@ describe('convertClaudeToAntigravityContent bare path replacement (#2418)', () =
const count = (result.match(/~\/.gemini\/antigravity\//g) || []).length;
assert.strictEqual(count, 1, `Expected exactly 1 replacement, got ${count} in: ${result}`);
});
// #3738: global skills install under ~/.gemini/config/skills (the dir AGY
// scans), while gsd-core runtime references stay under configHome. A skills
// path must therefore rewrite to the config root, not ~/.gemini/antigravity.
test('replaces ~/.claude/skills/ with ~/.gemini/config/skills (#3738)', () => {
const input = 'Skill dirs live at `~/.claude/skills/gsd-*/`.';
const result = convertClaudeToAntigravityContent(input, true);
assert.ok(
result.includes('~/.gemini/config/skills/gsd-*/'),
`Expected ~/.gemini/config/skills rewrite, got: ${result}`
);
assert.ok(
!result.includes('~/.gemini/antigravity/skills'),
`Skills must not point at the deprecated dir, got: ${result}`
);
});
test('replaces $HOME/.claude/skills/ with $HOME/.gemini/config/skills (#3738)', () => {
const input = 'ls $HOME/.claude/skills/';
const result = convertClaudeToAntigravityContent(input, true);
assert.ok(
result.includes('$HOME/.gemini/config/skills/'),
`Expected $HOME/.gemini/config/skills rewrite, got: ${result}`
);
assert.ok(!result.includes('$HOME/.claude/'), `Expected full replacement, got: ${result}`);
});
test('replaces bare ~/.claude/skills (no trailing slash) with ~/.gemini/config/skills (#3738)', () => {
const input = 'ls ~/.claude/skills';
const result = convertClaudeToAntigravityContent(input, true);
assert.ok(
result.includes('~/.gemini/config/skills'),
`bare skills form must divert to the config root, got: ${result}`
);
assert.ok(
!result.includes('~/.gemini/antigravity/skills'),
`bare skills form must not fall through to the retired configHome path, got: ${result}`
);
});
test('keeps gsd-core references under ~/.gemini/antigravity when a skills ref is present (#3738)', () => {
const input = 'Read ~/.claude/gsd-core/workflows/x.md then list ~/.claude/skills/.';
const result = convertClaudeToAntigravityContent(input, true);
assert.ok(
result.includes('~/.gemini/antigravity/gsd-core/workflows/x.md'),
`gsd-core ref must stay under configHome, got: ${result}`
);
assert.ok(
result.includes('~/.gemini/config/skills/'),
`skills ref must move to the config root, got: ${result}`
);
});
});
describe('local install', () => {
@@ -7606,3 +7664,54 @@ describe('#3719: real global Claude install — agents/*.md @-refs must resolve
assert.deepStrictEqual(failures, [], `local install must not leak @$HOME/ or @~/.claude/:\n${failures.join('\n')}`);
});
});
// ── #3738: antigravity global artifacts install under ~/.gemini/config ────────
describe('#3738: antigravity global artifacts install under ~/.gemini/config', () => {
test('global skills and agents dest dirs resolve under <home>/.gemini/config, not configHome', (t) => {
const configDir = createTempDir('gsd-3738-antigravity-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const layout = resolveRuntimeArtifactLayout('antigravity', configDir, 'global');
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
const agentsKind = layout.kinds.find(k => k.kind === 'agents');
assert.ok(skillsKind, 'antigravity must have a skills kind');
assert.ok(agentsKind, 'antigravity must have an agents kind');
const expectedHome = path.join(configDir, '.gemini', 'config');
assert.strictEqual(skillsKind.home, expectedHome, 'skills home override must be ~/.gemini/config');
assert.strictEqual(agentsKind.home, expectedHome, 'agents home override must be ~/.gemini/config');
installRuntimeArtifacts('antigravity', configDir, 'global', RESOLVED_CORE);
assert.ok(
fs.existsSync(path.join(expectedHome, 'skills', 'gsd-help', 'SKILL.md')),
'a gsd-* skill must exist under ~/.gemini/config/skills'
);
const agentsDir = path.join(expectedHome, 'agents');
assert.ok(fs.existsSync(agentsDir), '~/.gemini/config/agents must exist');
assert.ok(
fs.readdirSync(agentsDir).some(n => n.startsWith('gsd-')),
'at least one gsd-* agent must exist under ~/.gemini/config/agents'
);
assert.ok(
!fs.existsSync(path.join(configDir, 'skills')),
'no skills dir may be created under the configHome (~/.gemini/antigravity)'
);
assert.ok(
!fs.existsSync(path.join(configDir, 'agents')),
'no agents dir may be created under the configHome (~/.gemini/antigravity)'
);
});
test('local (workspace) layout is unchanged: .agents/skills and .agents/agents', (t) => {
const configDir = createTempDir('gsd-3738-antigravity-local-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const layout = resolveRuntimeArtifactLayout('antigravity', configDir, 'local');
for (const kind of layout.kinds) {
assert.strictEqual(kind.home, undefined, `local ${kind.kind} must not carry a home override`);
}
});
});