Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fe20210
fix: load Antigravity plugin skills
fcakyon Nov 3, 2026
9403556
fix: address Antigravity plugin review
fcakyon Nov 4, 2026
7b31ce4
Merge branch 'main' into fatih/antigravity-plugins
fcakyon Nov 4, 2026
03c80e2
fix: keep valid Antigravity skills available
fcakyon Nov 4, 2026
cca73cc
Merge branch 'main' into fatih/antigravity-plugins
fcakyon Nov 4, 2026
1ca6f45
Merge branch 'main' into fatih/antigravity-plugins
fcakyon Nov 4, 2026
c4e03f9
fix: isolate invalid plugin preferences
fcakyon Nov 4, 2026
111c28d
Merge branch 'main' into fatih/antigravity-plugins
fcakyon Nov 4, 2026
b4df1cc
Merge branch 'main' into fatih/antigravity-plugins
fcakyon Nov 4, 2026
302bada
test: compare encoded plugin skill paths
fcakyon Nov 4, 2026
0cb8986
build: merge upstream main
fcakyon Nov 4, 2026
ee665f9
build: merge upstream main
fcakyon Nov 4, 2026
73bcc62
build: merge upstream main
fcakyon Nov 4, 2026
5c58b38
build: merge upstream main
fcakyon Nov 4, 2026
f60fdc9
build: merge upstream main
fcakyon Nov 5, 2026
dd1fd31
build: merge upstream main
fcakyon Nov 5, 2026
808f32f
build: merge upstream main
fcakyon Nov 5, 2026
7a93afa
build: merge upstream main
fcakyon Nov 5, 2026
7350af1
build: merge upstream main
fcakyon Nov 5, 2026
7821fb9
build: merge upstream main
fcakyon Nov 5, 2026
92cf21f
Merge remote-tracking branch 'github/main' into fatih/antigravity-plu…
maria-rcks Nov 7, 2026
bc9a828
Merge branch 'main' into fatih/antigravity-plugins
fcakyon Nov 7, 2026
00b8278
Merge remote-tracking branch 'origin/main' into fatih/antigravity-plu…
juliusmarminge Nov 14, 2026
eb5a729
fix(server): read Antigravity plugin config in full after main merge
juliusmarminge Nov 14, 2026
ac5e7da
fix(server): let Antigravity client reads reach prepared plugin skills
juliusmarminge Nov 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSession
import { makeAntigravityAcpRuntime } from "../../provider/acp/AntigravityAcpSupport.ts";
import { normalizeAntigravitySessionUpdate } from "../../provider/acp/AntigravityProtocol.ts";
import { ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE } from "../../provider/antigravityAuthSupport.ts";
import { prepareAntigravityPluginSkills } from "../../provider/Drivers/AntigravitySkills.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import {
Expand Down Expand Up @@ -292,6 +293,20 @@ describe("AntigravityAdapterV2 client file system", () => {
const requestLog = path.join(logs, "requests.ndjson");
const crypto = yield* SecureRandom.SecureRandom;
const instanceId = ProviderInstanceId.make("antigravity-containment-test");
const userHome = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-antigravity-home-",
});
const plugin = path.join(userHome, ".gemini", "config", "plugins", "alpha");
const pluginSkill = path.join(plugin, "skills", "review", "SKILL.md");
const pluginResource = path.join(path.dirname(pluginSkill), "checklist.md");
yield* fileSystem.makeDirectory(path.dirname(pluginSkill), { recursive: true });
yield* fileSystem.writeFileString(pluginSkill, "---\nname: review\n---\nUse checklist.md\n");
yield* fileSystem.writeFileString(pluginResource, "checklist");
yield* fileSystem.writeFileString(path.join(plugin, "plugin.json"), "{}");
const clientFileRoots = yield* prepareAntigravityPluginSkills({
userHome,
runtimeTempDirectory: yield* fileSystem.makeTempDirectoryScoped(),
});
const adapter = yield* makeAntigravityAdapterV2({
instanceId,
selfInvocation: yield* resolveSelfInvocation(),
Expand All @@ -307,13 +322,16 @@ describe("AntigravityAdapterV2 client file system", () => {
},
}).pipe(
Effect.provideService(SecureRandom.SecureRandom, crypto),
Effect.map((runtime): RuntimeService => ({
Effect.map((runtime) => ({
...runtime,
handleReadTextFile: (handler) =>
clientFileRoots,
handleReadTextFile: (handler: Parameters<RuntimeService["handleReadTextFile"]>[0]) =>
Effect.sync(() => {
readTextFile = handler;
}).pipe(Effect.andThen(runtime.handleReadTextFile(handler))),
handleWriteTextFile: (handler) =>
handleWriteTextFile: (
handler: Parameters<RuntimeService["handleWriteTextFile"]>[0],
) =>
Effect.sync(() => {
writeTextFile = handler;
}).pipe(Effect.andThen(runtime.handleWriteTextFile(handler))),
Expand Down Expand Up @@ -416,6 +434,19 @@ describe("AntigravityAdapterV2 client file system", () => {
context("fs/read_text_file"),
);
assert.equal(viaInsideLink.content, "inside");

const wrapper = path.join(clientFileRoots[0]!, ".agents", "skills", "0", "SKILL.md");
for (const [file, expected] of [
[wrapper, JSON.stringify(pluginSkill)],
[pluginSkill, "Use checklist.md"],
[pluginResource, "checklist"],
] as const) {
const read = yield* readTextFile(
{ sessionId: "mock-session-1", path: file },
context("fs/read_text_file"),
);
assert.include(read.content, expected);
}
}).pipe(Effect.provide(layerSession)),
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ export interface AntigravityAdapterV2Options {
readonly makeRuntime: (
input: Omit<AntigravityAcpRuntimeInput, "spawn" | "childProcessSpawner">,
) => Effect.Effect<
AcpSessionRuntime.AcpSessionRuntime["Service"],
AcpSessionRuntime.AcpSessionRuntime["Service"] & {
/** Session-local roots outside the workspace, such as prepared plugin skills. */
readonly clientFileRoots?: ReadonlyArray<string>;
},
EffectAcpErrors.AcpError | ProviderSetupError,
Scope.Scope
>;
Expand Down Expand Up @@ -161,6 +164,16 @@ export function makeAntigravityAcpAdapterFlavor(
handleReadTextFile: (handler) =>
runtime.handleReadTextFile((request, context) =>
handler(request, context).pipe(
Effect.catch((cause) =>
runtime.clientFileRoots?.length
? readAntigravityClientTextFile({
fileSystem: options.fileSystem,
path: options.path,
allowedRoots: runtime.clientFileRoots,
request,
}).pipe(Effect.mapError(() => cause))
: Effect.fail(cause),
),
Effect.onExit((exit) => {
const served = reads.attempted(request.path, Exit.isSuccess(exit));
if (served === undefined || projectUpdate === undefined) return Effect.void;
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/provider/Drivers/AntigravityDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
const nodePath = yield* HostProcess.ExecutablePath;
const baseEnv = yield* HostProcess.Environment;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-driver-" });
const userHome = path.join(root, "home");
const instanceId = ProviderInstanceId.make(path.basename(root));
const mockAgentPath = yield* path.fromFileUrl(
new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
Expand Down Expand Up @@ -211,6 +212,8 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
enabled: options.enabled ?? true,
config: { ...AntigravityDriver.defaultConfig(), ...options.config },
environment: [
{ name: "HOME", value: userHome },
{ name: "USERPROFILE", value: userHome },
{ name: "PATH", value: instancePath },
{ name: "T3_ACP_ANTIGRAVITY", value: "1" },
{ name: "T3_ACP_REQUEST_LOG_PATH", value: requestLog },
Expand Down Expand Up @@ -245,6 +248,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
});
return {
instance,
userHome,
refresh,
makeExecutable,
fs,
Expand Down Expand Up @@ -297,10 +301,13 @@ const layerTest = ProviderHostLive.layer.pipe(

it.layer(layerTest)("AntigravityDriver", (it) => {
it.effect.skipIf(windowsHost)(
"refreshes the selected executable and limits form capabilities to chat",
"refreshes the selected executable despite plugin preparation failure and limits form capabilities to chat",
() =>
Effect.gen(function* () {
const h = yield* makeHarness();
const plugin = h.path.join(h.userHome, ".gemini", "config", "plugins", "broken");
yield* h.fs.makeDirectory(plugin, { recursive: true });
yield* h.fs.writeFileString(h.path.join(plugin, "plugin.json"), "x".repeat(1_000_001));
yield* h.refresh();
const snapshot = yield* h.instance.snapshot.getSnapshot;
expect(snapshot.status).toBe("ready");
Expand Down
24 changes: 22 additions & 2 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ import {
} from "@t3tools/provider-core/server/driver";
import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity";
import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts";
import {
discoverAntigravitySkills,
prepareAntigravityPluginSkills,
resolveAntigravityUserHome,
} from "./AntigravitySkills.ts";

const DRIVER = ProviderDriverKind.make("antigravity");
const decodeSettings = Schema.decodeSync(AntigravitySettings);
Expand Down Expand Up @@ -200,7 +204,9 @@ export const AntigravityDriver: ProviderDriver<
const makeRuntime = Effect.fn("AntigravityDriver.makeRuntime")(function* (
input: Omit<AntigravityAcpRuntimeInput, "spawn" | "childProcessSpawner">,
): Effect.fn.Return<
AcpSessionRuntime.AcpSessionRuntime["Service"],
AcpSessionRuntime.AcpSessionRuntime["Service"] & {
readonly clientFileRoots: ReadonlyArray<string>;
},
AcpErrors.AcpError | ProviderSetupError,
Scope.Scope
> {
Expand Down Expand Up @@ -267,8 +273,21 @@ export const AntigravityDriver: ProviderDriver<
),
),
);
const pluginDirectories = yield* prepareAntigravityPluginSkills({
userHome,
runtimeTempDirectory,
}).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.catch((cause) =>
Effect.logWarning("Antigravity plugin skills are unavailable.", {
errorTag: cause._tag,
}).pipe(Effect.as([])),
),
);
const runtime = yield* makeAntigravityAcpRuntime({
...input,
additionalDirectories: [...(input.additionalDirectories ?? []), ...pluginDirectories],
runtimeVersion: executable.version,
authMethod: auth.authMethod,
childProcessSpawner: spawner,
Expand All @@ -283,6 +302,7 @@ export const AntigravityDriver: ProviderDriver<
}).pipe(Effect.provideService(SecureRandom.SecureRandom, random));
return {
...runtime,
clientFileRoots: pluginDirectories,
...(executable.harnessPath === null
? { setMode: (modeId: string) => runtime.setMode(antigravityCliSessionMode(modeId)) }
: {}),
Expand Down
111 changes: 110 additions & 1 deletion apps/server/src/provider/Drivers/AntigravitySkills.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { matchInlineSkills } from "@t3tools/shared/inlineSkills";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";

import { discoverAntigravitySkills } from "./AntigravitySkills.ts";
import { discoverAntigravitySkills, prepareAntigravityPluginSkills } from "./AntigravitySkills.ts";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";

const writeSkill = Effect.fn("writeSkill")(function* (directory: string, contents: string) {
Expand Down Expand Up @@ -93,6 +94,114 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {
}),
);

it.effect(
"reads enabled plugin skills without merging same-name skills from different plugins",
() =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const input = yield* makeWorkspace();
const config = path.join(input.userHome, ".gemini", "config");
const expected = [];
for (const name of ["alpha", "beta", "broken", "off", "disabled", "unregistered"]) {
const plugin = path.join(config, "plugins", name);
const skillPath = yield* writeSkill(
path.join(plugin, "skills", "review"),
"---\nname: review\ndescription: Review changes.\n---\n",
);
if (name !== "unregistered") {
yield* fileSystem.writeFileString(
path.join(plugin, "plugin.json"),
name === "off" || name === "broken"
? "{ invalid"
: JSON.stringify({
name: `--- Display ${name} ---`,
disabled: name === "beta" || name === "disabled",
}),
);
}
if (name === "alpha" || name === "beta") {
expected.push({
name: `plugin:${name}:review`,
displayName: `${name}:review`,
description: "Review changes.",
path: skillPath,
scope: "user",
enabled: true,
});
}
}
yield* fileSystem.writeFileString(
path.join(config, "config.json"),
JSON.stringify({ plugins: { beta: { enabled: true }, off: { enabled: false } } }),
);
for (const [directory, scope] of [
[path.join(input.cwd, ".agents", "skills"), "project"],
[path.join(input.userHome, ".gemini", "config", "skills"), "user"],
] as const) {
const name = `standalone-${scope}`;
const skillPath = yield* writeSkill(
path.join(directory, name),
`---\nname: ${name}\n---\n`,
);
expected.push({ name, path: skillPath, scope, enabled: true });
}
assert.deepEqual(yield* discoverAntigravitySkills(input), expected);
const directories = yield* prepareAntigravityPluginSkills({
userHome: input.userHome,
runtimeTempDirectory: yield* fileSystem.makeTempDirectoryScoped(),
});
const nativeSkills = yield* discoverAntigravitySkills({
cwd: directories[0]!,
userHome: path.join(input.userHome, "empty"),
});
assert.deepEqual(
nativeSkills.map((skill) => skill.name),
["plugin:alpha:review", "plugin:beta:review"],
);
for (const [index, skill] of nativeSkills.entries()) {
assert.include(
yield* fileSystem.readFileString(skill.path),
JSON.stringify(expected[index]!.path),
);
assert.equal(
directories[index + 1],
path.dirname(path.dirname(path.dirname(expected[index]!.path))),
);
assert.include(yield* fileSystem.readFileString(expected[index]!.path), "name: review\n");
}
for (const contents of ["{ invalid", '{"plugins":{"alpha":{"enabled":"yes"}}}']) {
yield* fileSystem.writeFileString(path.join(config, "config.json"), contents);
assert.deepEqual(yield* discoverAntigravitySkills(input), expected.slice(2));
}
}),
);

it.effect("keeps punctuation and prefix variants distinct and invocable", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const input = yield* makeWorkspace();
const directories = ["foo.bar", "foo_bar", "_alpha", "plugin_alpha", "alpha"];
for (const name of directories) {
const plugin = path.join(input.userHome, ".gemini", "config", "plugins", name);
yield* writeSkill(path.join(plugin, "skills", "review"), "---\nname: review\n---\n");
yield* fileSystem.writeFileString(path.join(plugin, "plugin.json"), "{}");
}
const skills = yield* discoverAntigravitySkills(input);
assert.lengthOf(skills, directories.length);
assert.equal(
new Set(skills.map((skill) => skill.name.toLowerCase())).size,
directories.length,
);
for (const skill of skills) {
const matches = [...matchInlineSkills("$" + skill.name, skills)];
assert.lengthOf(matches, 1);
assert.equal(matches[0]?.skill.path, skill.path);
}
}),
);

it.effect("uses native root order for duplicate names", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
Loading
Loading