diff --git a/aidd_docs/tasks/2026_10/2026_10_02_antigravity-flat-host/phase-4.md b/aidd_docs/tasks/2026_10/2026_10_02_antigravity-flat-host/phase-4.md index 62fa96668..687c54e91 100644 --- a/aidd_docs/tasks/2026_10/2026_10_02_antigravity-flat-host/phase-4.md +++ b/aidd_docs/tasks/2026_10/2026_10_02_antigravity-flat-host/phase-4.md @@ -1,5 +1,5 @@ --- -status: pending +status: done --- @@ -16,8 +16,9 @@ cli/ │ └── portable-agent.ts ✅ rebuild frontmatter: name, description, model: inherit, tools as YAML list; drop everything else ├── src/contexts/tools/domain/profiles/antigravity/ │ ├── antigravity-paths.ts ✏️ agents dir -│ ├── profile.ts ✏️ AgentsCapability, nested `/agent.md` -│ └── build.ts ✏️ agents artifact, link rewriting relative to the nested path +│ └── build.ts ✏️ agents artifact, nested `/agent.md`, link rewriting relative to the nested path +├── src/kernel/errors.ts ✏️ `InvalidAgentFrontmatterError` +├── package.json, scripts/check-bundle-size.mjs ✏️ budget 742 KB, measured 734.5 KB ├── tests/contexts/tools/domain/formats/ │ └── portable-agent.unit.test.ts ✅ model mapping, tools string → list, Claude-only keys dropped, empty description rejected ├── tests/contexts/tools/domain/profiles/antigravity/build.unit.test.ts ✏️ agent path and body @@ -66,7 +67,7 @@ journey ### `2)` Antigravity agents in setup and translate -1. `AgentsCapability` and flat `agents` artifact writing `.agents/agents/-/agent.md`, body links rewritten from that nested path. +1. Flat `agents` artifact writing `.agents/agents/-/agent.md`, body links rewritten from that nested path. No `AgentsCapability`: a flat tool declaring one also gets the raw fallback route (`content-translator.ts`'s `flatSectionFile`), which copies Claude frontmatter to `.agents/agents//.md`, where `agy` 1.2.17 drops it in silence (measured: `model: opus` and comma `tools` both missing from `/agents`). `setup` takes the built-tree route, which reads the artifact. 2. Unit test the path and body; regenerate golden, only `antigravity:flat` moves. ### `3)` Live check diff --git a/cli/package.json b/cli/package.json index e83ebb756..cb167d4b0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -36,7 +36,7 @@ "node": ">=22.12" }, "packageManager": "pnpm@12.3.4", - "bundleBudgetKB": 734, + "bundleBudgetKB": 742, "scripts": { "build": "tsup && node scripts/check-bundle-size.mjs", "build:check-size": "node scripts/check-bundle-size.mjs", diff --git a/cli/scripts/check-bundle-size.mjs b/cli/scripts/check-bundle-size.mjs index c99bfadca..69f2bdcd3 100644 --- a/cli/scripts/check-bundle-size.mjs +++ b/cli/scripts/check-bundle-size.mjs @@ -30,6 +30,8 @@ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")); // guards for #829 (+10.4 KB over the preceding 703.8 KB); 1.1% measured headroom. // 734 KB: 725.8 KB, Kilo Code's profile, generated bridge, and runtime smoke support (+9.6 KB // over next's 716.2 KB); 1.1% measured headroom. +// 742 KB: 734.5 KB, Antigravity's flat profile, project hooks merge and portable agents +// (+7.4 KB over next's 727.1 KB); 1.0% measured headroom. const budgetKB = pkg.bundleBudgetKB ?? 500; const budgetBytes = budgetKB * 1024; diff --git a/cli/src/contexts/tools/domain/formats/portable-agent.ts b/cli/src/contexts/tools/domain/formats/portable-agent.ts new file mode 100644 index 000000000..80e57bdda --- /dev/null +++ b/cli/src/contexts/tools/domain/formats/portable-agent.ts @@ -0,0 +1,27 @@ +import { InvalidAgentFrontmatterError } from "../../../../kernel/errors.js"; + +/** Rebuilt, never copied: a host silently drops an agent with an unknown `model` or list-less `tools`. */ +export function portableAgentFrontmatter( + source: Record, + name: string +): Record { + const description = typeof source.description === "string" ? source.description.trim() : ""; + if (description === "") throw new InvalidAgentFrontmatterError(name, "description is empty"); + const tools = toolList(source.tools); + return { + name, + description, + model: "inherit", + ...(tools === null ? {} : { tools }), + }; +} + +function toolList(tools: unknown): string[] | null { + if (Array.isArray(tools)) return tools.map(String); + if (typeof tools !== "string") return null; + const list = tools + .split(",") + .map((tool) => tool.trim()) + .filter((tool) => tool !== ""); + return list.length > 0 ? list : null; +} diff --git a/cli/src/contexts/tools/domain/profiles/antigravity/antigravity-paths.ts b/cli/src/contexts/tools/domain/profiles/antigravity/antigravity-paths.ts index c08c65d50..d475ee98c 100644 --- a/cli/src/contexts/tools/domain/profiles/antigravity/antigravity-paths.ts +++ b/cli/src/contexts/tools/domain/profiles/antigravity/antigravity-paths.ts @@ -5,3 +5,4 @@ export const ANTIGRAVITY_DIRECTORY = ".agents/"; export const ANTIGRAVITY_SKILLS_DIR = `${ANTIGRAVITY_DIRECTORY}skills/`; export const ANTIGRAVITY_HOOKS_DIR = `${ANTIGRAVITY_DIRECTORY}hooks/`; export const ANTIGRAVITY_HOOKS_FILE = `${ANTIGRAVITY_DIRECTORY}hooks.json`; +export const ANTIGRAVITY_AGENTS_DIR = `${ANTIGRAVITY_DIRECTORY}agents/`; diff --git a/cli/src/contexts/tools/domain/profiles/antigravity/build.ts b/cli/src/contexts/tools/domain/profiles/antigravity/build.ts index f064689a2..d8564b0e9 100644 --- a/cli/src/contexts/tools/domain/profiles/antigravity/build.ts +++ b/cli/src/contexts/tools/domain/profiles/antigravity/build.ts @@ -1,15 +1,48 @@ -/** Antigravity's project distribution is flat and carries skills and hooks. A skill sits one level - * under `.agents/skills/`, since `agy` expands none nested deeper. */ +/** Flat only. A skill sits one level under `.agents/skills/`: `agy` expands none nested deeper. */ +import { parseFrontmatter, serializeFrontmatter } from "../../../../../kernel/markdown.js"; import { genericFlatSkillPath } from "../../../../../kernel/materialization/flat-paths.js"; +import { rewriteRelativeLinks } from "../../../../../kernel/materialization/relative-link-rewrite.js"; import type { ToolBuildContract } from "../../build-contract.js"; +import { portableAgentFrontmatter } from "../../formats/portable-agent.js"; import { antigravityProjectHooksFormat } from "./antigravity-hooks.js"; -import { ANTIGRAVITY_HOOKS_FILE, ANTIGRAVITY_SKILLS_DIR } from "./antigravity-paths.js"; +import { + ANTIGRAVITY_AGENTS_DIR, + ANTIGRAVITY_HOOKS_FILE, + ANTIGRAVITY_SKILLS_DIR, +} from "./antigravity-paths.js"; function antigravityFlatSkillPath(plugin: string, rel: string): string { return genericFlatSkillPath(ANTIGRAVITY_SKILLS_DIR, plugin, rel.replace(/^skills\//, "")); } +function agentName(plugin: string, rel: string): string { + return `${plugin}-${rel.replace(/^agents\//, "").replace(/(\.agent)?\.md$/, "")}`; +} + +function antigravityFlatAgentPath(plugin: string, rel: string): string { + return `${ANTIGRAVITY_AGENTS_DIR}${agentName(plugin, rel)}/agent.md`; +} + +function resolveTarget(plugin: string, rel: string): string { + if (rel.startsWith("agents/")) return antigravityFlatAgentPath(plugin, rel); + if (rel.startsWith("skills/")) return antigravityFlatSkillPath(plugin, rel); + return rel; +} + +function transformAntigravityAgent(content: string, plugin: string, outName: string): string { + const { frontmatter, body } = parseFrontmatter(content); + const rel = `agents/${outName}`; + const rewrittenBody = rewriteRelativeLinks(body, { + currentFilePluginRelative: antigravityFlatAgentPath(plugin, rel), + resolveTargetPath: (target) => resolveTarget(plugin, target), + }); + return serializeFrontmatter( + portableAgentFrontmatter(frontmatter, agentName(plugin, rel)), + rewrittenBody + ); +} + export function buildAntigravityFlatContract(): ToolBuildContract { return { manifestFileRelative: null, @@ -22,7 +55,12 @@ export function buildAntigravityFlatContract(): ToolBuildContract { path: antigravityFlatSkillPath, rewriteSkillName: true, }, - agents: { supported: false }, + agents: { + supported: true, + source: { kind: "filteredTree", srcDir: "agents", inputExt: ".md" }, + path: antigravityFlatAgentPath, + transform: transformAntigravityAgent, + }, mcp: { supported: false }, hooks: { supported: true, diff --git a/cli/src/kernel/errors.ts b/cli/src/kernel/errors.ts index b3200cd8d..37839cfb8 100644 --- a/cli/src/kernel/errors.ts +++ b/cli/src/kernel/errors.ts @@ -195,6 +195,13 @@ export class UnresolvableUserScopeError extends Error { } } +export class InvalidAgentFrontmatterError extends Error { + constructor(agentName: string, detail: string) { + super(`Agent '${agentName}' has invalid frontmatter: ${detail}.`); + this.name = "InvalidAgentFrontmatterError"; + } +} + export class InvalidPluginManifestError extends Error { constructor(detail?: string) { super(detail ? `Invalid plugin manifest: ${detail}` : "Invalid plugin manifest."); diff --git a/cli/tests/contexts/tools/domain/formats/portable-agent.unit.test.ts b/cli/tests/contexts/tools/domain/formats/portable-agent.unit.test.ts new file mode 100644 index 000000000..78dd54ebb --- /dev/null +++ b/cli/tests/contexts/tools/domain/formats/portable-agent.unit.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { portableAgentFrontmatter } from "../../../../../src/contexts/tools/domain/formats/portable-agent.js"; +import { InvalidAgentFrontmatterError } from "../../../../../src/kernel/errors.js"; + +describe("portableAgentFrontmatter", () => { + it("names the agent, keeps its description and inherits the session model", () => { + expect( + portableAgentFrontmatter( + { name: "executor", description: "Turns a task into code.", model: "opus" }, + "aidd-dev-executor" + ) + ).toEqual({ + name: "aidd-dev-executor", + description: "Turns a task into code.", + model: "inherit", + }); + }); + + it("turns a comma-separated tools string into a list", () => { + expect( + portableAgentFrontmatter({ description: "d", tools: "Read, Grep,Glob" }, "a").tools + ).toEqual(["Read", "Grep", "Glob"]); + }); + + it("keeps a tools list as a list", () => { + expect( + portableAgentFrontmatter({ description: "d", tools: ["Read", "Bash"] }, "a").tools + ).toEqual(["Read", "Bash"]); + }); + + it("emits no tools key when the source declares none", () => { + expect(portableAgentFrontmatter({ description: "d" }, "a")).not.toHaveProperty("tools"); + }); + + it("drops every key it does not rebuild", () => { + expect( + Object.keys( + portableAgentFrontmatter( + { description: "d", color: "red", permissionMode: "plan", skills: ["x"] }, + "a" + ) + ).sort() + ).toEqual(["description", "model", "name"]); + }); + + it.each([undefined, "", " "])( + "refuses a blank description (%j), naming the agent", + (description) => { + expect(() => portableAgentFrontmatter({ description }, "aidd-dev-executor")).toThrow( + InvalidAgentFrontmatterError + ); + expect(() => portableAgentFrontmatter({ description }, "aidd-dev-executor")).toThrow( + /aidd-dev-executor/ + ); + } + ); +}); diff --git a/cli/tests/contexts/tools/domain/profiles/antigravity/build.unit.test.ts b/cli/tests/contexts/tools/domain/profiles/antigravity/build.unit.test.ts index 4681169d8..a62f772da 100644 --- a/cli/tests/contexts/tools/domain/profiles/antigravity/build.unit.test.ts +++ b/cli/tests/contexts/tools/domain/profiles/antigravity/build.unit.test.ts @@ -47,7 +47,43 @@ describe("buildAntigravityFlatContract", () => { }); }); - it.each(["agents", "mcp", "rules", "commands"] as const)("declares %s unsupported", (kind) => { + it("writes an agent only in the nested layout, named after its plugin", () => { + const agents = supported(buildAntigravityFlatContract().artifacts.agents); + + expect(agents.path("aidd-dev", "agents/executor.md")).toBe( + ".agents/agents/aidd-dev-executor/agent.md" + ); + expect(agents.path("aidd-dev", "agents/executor.agent.md")).toBe( + ".agents/agents/aidd-dev-executor/agent.md" + ); + }); + + it("rebuilds the agent's frontmatter and links its body from the nested directory", () => { + const agents = supported(buildAntigravityFlatContract().artifacts.agents); + const source = [ + "---", + "name: executor", + "description: Turns a task into code.", + "model: opus", + "color: red", + "---", + // biome-ignore lint/suspicious/noTemplateCurlyInString: the Claude placeholder the rewrite resolves + "Follow @${CLAUDE_PLUGIN_ROOT}/skills/01-plan/SKILL.md", + ].join("\n"); + + expect(agents.transform?.(source, "aidd-dev", "executor.md")).toBe( + [ + "---", + "name: 'aidd-dev-executor'", + "description: 'Turns a task into code.'", + "model: 'inherit'", + "---", + "Follow [SKILL.md](../../skills/aidd-dev-01-plan/SKILL.md)", + ].join("\n") + ); + }); + + it.each(["mcp", "rules", "commands"] as const)("declares %s unsupported", (kind) => { expect(buildAntigravityFlatContract().artifacts[kind].supported).toBe(false); }); diff --git a/cli/tests/golden/snapshots/framework-build/golden.json b/cli/tests/golden/snapshots/framework-build/golden.json index 6ffd2f06f..5355dfe4f 100644 --- a/cli/tests/golden/snapshots/framework-build/golden.json +++ b/cli/tests/golden/snapshots/framework-build/golden.json @@ -2130,6 +2130,10 @@ ".agents/skills/aidd-async-dev-01-setup/actions/skills/03-generate-workflow.md": "11f7ec6c03284d0524179f71337691301a6362cf77bf3aa666fe41686b4df40b", ".agents/skills/aidd-async-dev-01-setup/actions/skills/04-write-config.md": "eb7ecb812e8bdaaeba2e56c71c77bf8c14fce0a2c44311ff7985444617635dd5", ".agents/skills/aidd-async-dev-01-setup/actions/skills/05-bootstrap-labels.md": "f53177ce1c58767f1bdfcfa3e72f7d4cc5e3d4fd782c35c3998815317be108b1", - ".agents/hooks/aidd-context/update_memory.js": "140d7db788452f5f4c32316d522f595a36e06638b19a42d32e42a1a7324b7149" + ".agents/hooks/aidd-context/update_memory.js": "140d7db788452f5f4c32316d522f595a36e06638b19a42d32e42a1a7324b7149", + ".agents/agents/aidd-dev-reviewer/agent.md": "80f7f93f8fe14b7b8983466639155dca76e825c4b2a6a9b9203bd48a41135990", + ".agents/agents/aidd-dev-planner/agent.md": "dc5756134837010d3fc117bbd9b47f868a98ca185504877b6cb41f4797098ff9", + ".agents/agents/aidd-dev-implementer/agent.md": "bb397194473e1256883f08281deeabc0e2a1e3ed3a1184b8f48e6a47551ded9e", + ".agents/agents/aidd-async-dev-async-orchestrator/agent.md": "2d1838d0bb76a5e0db435e0fde792ab8502f2454cc97e1fba2f998b2eb49319d" } }