Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,5 +1,5 @@
---
status: pending
status: done
---

<!-- Fill or omit these sections; never add, rename, or reorder one. -->
Expand All @@ -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 `<name>/agent.md`
│ └── build.ts ✏️ agents artifact, link rewriting relative to the nested path
│ └── build.ts ✏️ agents artifact, nested `<name>/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
Expand Down Expand Up @@ -66,7 +67,7 @@ journey

### `2)` Antigravity agents in setup and translate

1. `AgentsCapability` and flat `agents` artifact writing `.agents/agents/<plugin>-<agent>/agent.md`, body links rewritten from that nested path.
1. Flat `agents` artifact writing `.agents/agents/<plugin>-<agent>/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/<plugin>/<file>.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
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions cli/scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
27 changes: 27 additions & 0 deletions cli/src/contexts/tools/domain/formats/portable-agent.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>,
name: string
): Record<string, unknown> {
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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/`;
46 changes: 42 additions & 4 deletions cli/src/contexts/tools/domain/profiles/antigravity/build.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions cli/src/kernel/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
@@ -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/
);
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
6 changes: 5 additions & 1 deletion cli/tests/golden/snapshots/framework-build/golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading