* 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:
@@ -160,6 +160,18 @@ const AGENT_TRANSFORM_SRCS = [
|
||||
'src/model-catalog.cts',
|
||||
];
|
||||
|
||||
// #3738: antigravity's global skills pass through the antigravity converter
|
||||
// (src/runtime-artifact-conversion.cts, mirrored hand-authored in bin/install.js
|
||||
// per ADR-1508), whose rewrites — e.g. ~/.claude/skills/ → ~/.gemini/config/skills/
|
||||
// — can move emitted bytes with NO commands/gsd source changing. Declaring the
|
||||
// transform here attributes that ripple class permanently, the same way
|
||||
// AGENT_TRANSFORM_SRCS does for agents; scoped to runtime 'antigravity' so a
|
||||
// converter change never blankets the other skills runtimes' attribution.
|
||||
const ANTIGRAVITY_SKILL_TRANSFORM_SRCS = [
|
||||
'src/runtime-artifact-conversion.cts',
|
||||
'bin/install.js',
|
||||
];
|
||||
|
||||
/**
|
||||
* A `sources` entry ending in `/` is a PREFIX, not a file: it means "any repo path
|
||||
* under this directory legitimately explains this emitted path". Used where an
|
||||
@@ -481,6 +493,9 @@ const PROVENANCE_RULES = [
|
||||
roots: SKILLS_ROOTS,
|
||||
pattern: /^([^/]+)\/SKILL\.md$/,
|
||||
sources: (m) => [`${COMMANDS_SRC}/${stripSkillPrefix(m[1])}.md`],
|
||||
// #3738: see ANTIGRAVITY_SKILL_TRANSFORM_SRCS above — antigravity's skill
|
||||
// bytes are converter-produced, so a converter change explains the ripple.
|
||||
transforms: (_m, ctx) => (ctx.runtime === 'antigravity' ? ANTIGRAVITY_SKILL_TRANSFORM_SRCS : []),
|
||||
},
|
||||
{
|
||||
id: 'skills-nested-from-commands',
|
||||
|
||||
@@ -42,6 +42,7 @@ const {
|
||||
MINIMUM_MANIFEST_FAMILIES,
|
||||
runMinimalInstall,
|
||||
buildParityManifest,
|
||||
extraEmitRootsFor,
|
||||
PKG_VERSION,
|
||||
} = require('./install-shared.cjs');
|
||||
const { mergeAckSources, MAX_ACK_FRAGMENTS } = require('./emitted-diff.cjs');
|
||||
@@ -898,7 +899,12 @@ function currentManifests({ repoRoot } = {}) {
|
||||
for (const { name, runtime, scope } of MANIFEST_FAMILIES) {
|
||||
const { configDir, root } = runMinimalInstall({ runtime, scope, installScript });
|
||||
try {
|
||||
manifests[name] = buildParityManifest(configDir, root, { pkgVersion });
|
||||
manifests[name] = buildParityManifest(configDir, root, {
|
||||
pkgVersion,
|
||||
// #3738: cover home-override emit roots outside configDir (antigravity
|
||||
// → <HOME>/.gemini/config) so the differential keeps seeing them.
|
||||
extraEmitRoots: extraEmitRootsFor(runtime, scope, root),
|
||||
});
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
|
||||
@@ -299,6 +299,32 @@ function walk(dir) {
|
||||
return results;
|
||||
}
|
||||
|
||||
// #3738: runtimes whose GLOBAL artifact layout declares a kind `home` override
|
||||
// that resolves OUTSIDE configDir (antigravity → <HOME>/.gemini/config, the dir
|
||||
// AGY scans for machine-local discovery). The parity walk must cover those
|
||||
// roots too, or every emitted skill/agent silently leaves the manifest the
|
||||
// moment the override appears — exactly the #3547 blind-spot class this
|
||||
// harness exists to prevent (a real install shape the manifest cannot see).
|
||||
// Mirrors the capability registry's artifactLayout `home` fields the same way
|
||||
// RUNTIME_META mirrors configHome; codex's `.agents` override is deliberately
|
||||
// NOT listed here — its skills have never been manifest-covered, and widening
|
||||
// this table for codex is a coverage change unrelated to #3738.
|
||||
const EXTRA_GLOBAL_EMIT_ROOTS = {
|
||||
antigravity: [path.join('.gemini', 'config')],
|
||||
};
|
||||
|
||||
/**
|
||||
* Absolute extra emit-root directories for a runtime/scope pair — the
|
||||
* home-override install roots outside configDir — or [] when none.
|
||||
* @param {string} runtime
|
||||
* @param {string} scope
|
||||
* @param {string} root the sandboxed HOME/temp root the install ran under
|
||||
*/
|
||||
function extraEmitRootsFor(runtime, scope, root) {
|
||||
const suffixes = (scope === 'global' && EXTRA_GLOBAL_EMIT_ROOTS[runtime]) || [];
|
||||
return suffixes.map((suffix) => path.join(root, suffix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a deterministic hash-map of all non-volatile files under configDir.
|
||||
*
|
||||
@@ -391,7 +417,29 @@ function collectNormalizedEmittedFiles(configDir, root, opts, callerName) {
|
||||
'Pass the version of the tree that produced the emitted content at configDir.'
|
||||
);
|
||||
}
|
||||
const allFiles = walk(configDir);
|
||||
// #3738: home-override emit roots outside configDir (see
|
||||
// EXTRA_GLOBAL_EMIT_ROOTS). Each extra root's files are keyed rel to THAT
|
||||
// root — the emitted key space is install-location-relative ('skills/…',
|
||||
// 'agents/…'), so the same artifact keeps the same key whether the layout
|
||||
// resolves it under configDir or under the override root. configDir entries
|
||||
// win on collision (a layout would never write both, but a stale leftover
|
||||
// under configDir must not shadow the live emit root).
|
||||
const extraEmitRoots = Array.isArray(opts.extraEmitRoots) ? opts.extraEmitRoots : [];
|
||||
const allFiles = walk(configDir).map((full) => ({ full, relRoot: configDir }));
|
||||
for (const extraRoot of extraEmitRoots) {
|
||||
if (typeof extraRoot !== 'string' || extraRoot.length === 0) {
|
||||
throw new Error(`${callerName}: opts.extraEmitRoots entries must be non-empty absolute paths`);
|
||||
}
|
||||
if (path.resolve(extraRoot) === path.resolve(configDir)) continue;
|
||||
// An absent extra root is a legitimate shape, not an error: the baseline
|
||||
// side measures a BASE tree whose installer may predate the home override
|
||||
// (the artifacts then live under configDir, which IS walked). Only a
|
||||
// root that exists but cannot be read is a failure — walk() surfaces that.
|
||||
let stat;
|
||||
try { stat = fs.statSync(extraRoot); } catch { continue; }
|
||||
if (!stat.isDirectory()) continue;
|
||||
for (const full of walk(extraRoot)) allFiles.push({ full, relRoot: extraRoot });
|
||||
}
|
||||
const unsorted = {};
|
||||
|
||||
// The claude LOCAL install resolves its config dir via realpath, which on macOS
|
||||
@@ -405,11 +453,15 @@ function collectNormalizedEmittedFiles(configDir, root, opts, callerName) {
|
||||
let realRoot = root;
|
||||
try { realRoot = fs.realpathSync(root); } catch { /* root already gone / not resolvable */ }
|
||||
|
||||
for (const full of allFiles) {
|
||||
for (const { full, relRoot } of allFiles) {
|
||||
// Build POSIX-style relative path for cross-platform stability
|
||||
const rel = path.relative(configDir, full).split(path.sep).join('/');
|
||||
const rel = path.relative(relRoot, full).split(path.sep).join('/');
|
||||
|
||||
if (VOLATILE_FILES.has(rel)) continue;
|
||||
// Collision policy stated above: configDir owns the key first; a file in
|
||||
// an extra emit root with an already-claimed rel is the stale-leftover
|
||||
// case, not a second opinion.
|
||||
if (relRoot !== configDir && Object.prototype.hasOwnProperty.call(unsorted, rel)) continue;
|
||||
if (HOOK_CONFIG_FILES.has(path.basename(rel))) continue;
|
||||
if (HOOK_CONFIG_RELATIVE_PATHS.has(rel)) continue;
|
||||
if (EXCLUDED_PREFIXES.some((p) => rel.startsWith(p))) continue;
|
||||
@@ -500,10 +552,15 @@ function buildEmittedSizes(configDir, root, opts = {}) {
|
||||
* normalizes to, never which paths buildParityManifest walks or excludes — so there
|
||||
* is nothing for a caller to pass here, and forwarding one through would only let a
|
||||
* bad version value make a pure file-set query throw for no file-set-shaped reason
|
||||
* (#2891 review FINDING 6; verified no caller passes a third argument —
|
||||
* tests/golden-install-tree.test.cjs, scripts/gen-install-tree-fixtures.cjs). */
|
||||
function buildInstallTree(configDir, root) {
|
||||
return Object.keys(buildParityManifest(configDir, root)).sort();
|
||||
* (#2891 review FINDING 6; verified no caller passes a version argument —
|
||||
* tests/golden-install-tree.test.cjs, scripts/gen-install-tree-fixtures.cjs).
|
||||
* #3738: an OPTIONAL third argument — extraEmitRoots (array, see
|
||||
* extraEmitRootsFor) — is the one non-version thing a file-set query legitimately
|
||||
* needs: the home-override install roots outside configDir. Omitted/null keep the
|
||||
* legacy configDir-only walk, so buildInstallTree(cd, root, null) still equals
|
||||
* buildInstallTree(cd, root). */
|
||||
function buildInstallTree(configDir, root, extraEmitRoots) {
|
||||
return Object.keys(buildParityManifest(configDir, root, { extraEmitRoots })).sort();
|
||||
}
|
||||
|
||||
function simulateHookCopy(hooksSrc, hooksDest) {
|
||||
@@ -752,6 +809,7 @@ module.exports = {
|
||||
buildParityManifest,
|
||||
buildEmittedSizes,
|
||||
buildInstallTree,
|
||||
extraEmitRootsFor,
|
||||
simulateHookCopy,
|
||||
installerEnv,
|
||||
runMinimalInstall,
|
||||
|
||||
Reference in New Issue
Block a user