Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ ios-signing/
/apps/desktop/.ade
/.ade/shipLane/
/.ade/logs/

# Root AI hand-off specs (orchestrator/agent hand-offs, not read at runtime)
/goal.md
/codexGoal.md
/*-next-agent-prompt.md
/.playwright-mcp
/.codex-derived-data
package-lock.json
Expand Down
2 changes: 2 additions & 0 deletions apps/ade-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ ade link branch owner/repo my-branch --pr 42
ade link pr owner/repo 42 --ade
ade link linear-issue ADE-123 --branch arul/ade-123-fix
ade linear install
ade skill list --text
ade skill show ade-browser --text
```

Use typed commands first. They validate common arguments and provide stable JSON fields or readable text summaries. Use `ade help <command> <subcommand>` for exact flags, `ade actions list --text` to discover the full service-backed action catalog, and `ade actions run <domain.action>` only when there is no typed command for the workflow yet.
Expand Down
10 changes: 5 additions & 5 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1873,14 +1873,14 @@ describe("adeRpcServer", () => {
// it ends with the user prompt and carries the inline guidance preamble.
const createCall = (fixture.runtime.ptyService.create as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as { args: string[] };
const finalArg = createCall.args[createCall.args.length - 1];
expect(finalArg).toContain("only normal reason to skip ADE CLI");
expect(finalArg).toContain("ADE proof drawer");
expect(finalArg).toContain("clean up old, stale, or finished processes");
expect(finalArg).toContain("control plane for ADE state");
expect(finalArg).toContain("proof & screenshots");
expect(finalArg).toContain("clean up processes you start");
expect(finalArg.endsWith("Implement API wiring")).toBe(true);
expect(response.structuredContent.startupCommand).toContain("claude");
expect(response.structuredContent.startupCommand).toContain("--model");
expect(response.structuredContent.startupCommand).toContain("--permission-mode");
expect(response.structuredContent.startupCommand).toContain("only normal reason to skip ADE CLI");
expect(response.structuredContent.startupCommand).toContain("control plane for ADE state");
expect(response.structuredContent.permissionMode).toBe("default");
expect(response.structuredContent.contextRef?.path).toBeNull();
});
Expand Down Expand Up @@ -2439,7 +2439,7 @@ describe("adeRpcServer", () => {
expect(response.structuredContent.permissionMode).toBe("plan");
expect(response.structuredContent.startupCommand).toContain("--sandbox");
expect(response.structuredContent.startupCommand).toContain("read-only");
expect(response.structuredContent.startupCommand).toContain("only normal reason to skip ADE CLI");
expect(response.structuredContent.startupCommand).toContain("control plane for ADE state");
const contextPath = response.structuredContent.contextRef?.path as string | null;
expect(contextPath).toBeTruthy();
expect(contextPath?.includes("/.ade/cache/orchestrator/agent-context/run-123/")).toBe(true);
Expand Down
22 changes: 22 additions & 0 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as nodePty from "node-pty";
import { createFileLogger, type Logger } from "../../desktop/src/main/services/logging/logger";
import { openKvDb, type AdeDb } from "../../desktop/src/main/services/state/kvDb";
import { detectDefaultBaseRef, toProjectInfo, upsertProjectRow } from "../../desktop/src/main/services/projects/projectService";
import { reseedAdeSkills } from "../../desktop/src/main/services/skills/skillReseedService";
import {
createAdeProjectService,
initializeOrRepairAdeProject,
Expand Down Expand Up @@ -378,7 +379,28 @@ function inferAgentSkillsRootForCliEntry(cliEntry: string | null): string | null
return null;
}

let adeSkillsReseededForCli = false;

/**
* Materialize ADE's bundled `ade-*` skills into the home-level skill dirs every
* runtime natively discovers, so agents ADE spawns pick them up via the runtime's
* own progressive disclosure. Cheap no-op once on-disk copies are current;
* best-effort so an unwritable home dir never blocks the CLI.
*/
export function reseedBundledAdeSkillsForCli(): void {
if (adeSkillsReseededForCli) return;
if (process.env.ADE_DISABLE_SKILL_RESEED === "1" || process.env.VITEST) return;
adeSkillsReseededForCli = true;
try {
const bundledRoot = inferAgentSkillsRootForCliEntry(resolveCurrentAdeCliEntry());
if (bundledRoot) reseedAdeSkills({ bundledRoot });
} catch {
/* best-effort: skill re-seeding must never break agent launch */
}
}

function createHeadlessAdeCliAgentEnv(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
reseedBundledAdeSkillsForCli();
const next: NodeJS.ProcessEnv = { ...baseEnv };
const nextPath = augmentProcessPathWithShellAndKnownCliDirs({
env: next,
Expand Down
48 changes: 47 additions & 1 deletion apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
CliDeeplinkUsageError,
runDeeplinkCommand,
} from "./commands/deeplinks";
import {
CliSkillUsageError,
runSkillCommand,
} from "./commands/skill";
import { buildDeeplink } from "../../desktop/src/shared/deeplinks";
import {
AUTOMATIONS_COMING_SOON_MESSAGE,
Expand Down Expand Up @@ -55,6 +59,7 @@ import { MACOS_VM_PHASES } from "../../desktop/src/shared/types/macosVm";
import type { AdeServiceCommand } from "./serviceManager/common";
import { normalizeAdeRuntimeRole, resolveAdeDefaultRole } from "./runtimeRoles";
import type { AdeRuntime } from "./bootstrap";
import { reseedBundledAdeSkillsForCli } from "./bootstrap";
import { EncryptedFileCredentialStore } from "./services/credentials/credentialStore";

type JsonObject = Record<string, unknown>;
Expand Down Expand Up @@ -166,7 +171,8 @@ type CliPlan =
| { kind: "pty-host-worker" }
| { kind: "init"; targetPath: string | null }
| { kind: "cursor-cloud"; rest: string[] }
| { kind: "deeplink"; rest: string[] };
| { kind: "deeplink"; rest: string[] }
| { kind: "skill"; rest: string[] };

type CliConnection = {
mode: "desktop-socket" | "runtime-socket" | "headless";
Expand Down Expand Up @@ -427,6 +433,7 @@ const TOP_LEVEL_HELP = `${ADE_BANNER}
$ ade link lane | session | branch | pr | linear-issue
Build a shareable deeplink (copies to clipboard)
$ ade linear install Register ADE as Linear's "Open in coding tool" target
$ ade skill list | show <name> Browse ADE's bundled agent skills (no daemon)
$ ade runtime start | stop | status Manage the machine runtime daemon
$ ade serve Run the ADE runtime daemon in foreground
$ ade rpc --stdio Speak ADE JSON-RPC over stdin/stdout
Expand Down Expand Up @@ -958,6 +965,23 @@ const HELP_BY_COMMAND: Record<string, string> = {
Flags:
--ade Emit the custom "ade://" form. Defaults to the https mirror.
--no-clipboard Print the URL but do not copy it to the system clipboard.
`,
skill: `${ADE_BANNER}
ADE Skills

Browse ADE's bundled, version-locked agent skills directly from the bundled
resources. This is a local command that does NOT require the runtime daemon —
it is the tamper-proof backstop for agents that can't natively discover
ADE's skills.

$ ade skill list List bundled skills (JSON: name, description, path)
$ ade skill list --text One "name — description" line per skill
$ ade skill show <name> Print a skill's SKILL.md (JSON: name, description, content, path)
$ ade skill show <name> --text Print just the skill's markdown body

Flags:
--text Human-readable output.
--json Structured JSON output (default).
`,
runtime: `${ADE_BANNER}
ADE Runtime
Expand Down Expand Up @@ -10411,6 +10435,7 @@ function buildCliPlan(command: string[]): CliPlan {
project: "projects",
quota: "usage",
quotas: "usage",
skills: "skill",
};
const primaryHelpKey = aliases[primary] ?? primary;
if (hasHelpFlag(args)) {
Expand Down Expand Up @@ -10463,6 +10488,10 @@ function buildCliPlan(command: string[]): CliPlan {
// dispatcher can branch on it; reconstruct rest accordingly.
return { kind: "deeplink", rest: [primary, ...args] };
}
if (primary === "skill" || primary === "skills") {
// Local (non-RPC) bundled-agent-skill browser; no daemon required.
return { kind: "skill", rest: args };
}
if (primary === "linear") {
// `ade linear install` is the deeplink installer; every other `ade linear`
// subcommand (workflows, sync, quick-view, route, picker-data, ...) belongs
Expand Down Expand Up @@ -15432,6 +15461,20 @@ async function runCli(
throw error;
}
}
if (plan.kind === "skill") {
try {
// The global parser folds --text/--json into parsed.options.text;
// forward that choice to the local skill command (default = JSON).
const rest = [...plan.rest, parsed.options.text ? "--text" : "--json"];
const result = runSkillCommand(rest);
return { output: result.output, exitCode: result.exitCode };
} catch (error) {
if (error instanceof CliSkillUsageError) {
throw new CliUsageError(error.message);
}
throw error;
}
}
if (plan.kind === "runtime") {
const result = await runRuntimeCommand(plan.rest, parsed.options);
return {
Expand Down Expand Up @@ -15505,6 +15548,9 @@ async function main(): Promise<void> {
console.log = writeDiagnostic;
console.info = writeDiagnostic;
console.warn = writeDiagnostic;
// Ensure ADE's bundled skills are seeded into the home-level dirs every runtime
// discovers before any agent is spawned (cheap no-op when already current).
reseedBundledAdeSkillsForCli();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
try {
const result = await runCli(process.argv.slice(2));
await writeProcessOutput(process.stdout, result.output);
Expand Down
70 changes: 70 additions & 0 deletions apps/ade-cli/src/commands/skill.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";

import { CliSkillUsageError, runSkillCommand, runSkillList, runSkillShow } from "./skill";

describe("ade skill (bundled agent skills)", () => {
it("list --json returns entries for known bundled skills", () => {
const result = runSkillList(["--json"]);
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.output) as Array<{
name: string;
description: string;
path: string;
}>;
const names = parsed.map((entry) => entry.name);
expect(names).toContain("ade-browser");
expect(names).toContain("ade-cli-control-plane");
// Sorted + each entry carries a path to a SKILL.md.
expect([...names]).toEqual([...names].sort((a, b) => a.localeCompare(b)));
for (const entry of parsed) {
expect(entry.path).toMatch(/SKILL\.md$/);
}
});

it("list --text emits one name — description line per skill", () => {
const result = runSkillList(["--text"]);
expect(result.exitCode).toBe(0);
expect(result.output).toMatch(/ade-browser —/);
});

it("show <name> --json returns full content including frontmatter name", () => {
const result = runSkillShow(["ade-browser", "--json"]);
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.output) as {
name: string;
description: string;
content: string;
path: string;
};
expect(parsed.name).toBe("ade-browser");
expect(parsed.content).toContain("name: ade-browser");
expect(parsed.content).toContain("---");
});

it("show <name> --text prints the markdown body without frontmatter delimiters at the top", () => {
const result = runSkillShow(["ade-browser", "--text"]);
expect(result.exitCode).toBe(0);
expect(result.output.trimStart().startsWith("---")).toBe(false);
expect(result.output.length).toBeGreaterThan(0);
});

it("show errors clearly for an unknown skill and lists available names", () => {
expect(() => runSkillShow(["does-not-exist"])).toThrowError(/Unknown skill/);
try {
runSkillShow(["does-not-exist"]);
} catch (error) {
expect((error as Error).message).toContain("ade-browser");
}
});

it("top-level dispatch prints help with no args", () => {
const result = runSkillCommand([]);
expect(result.exitCode).toBe(0);
expect(result.output).toContain("ade skill list");
});

it("top-level dispatch rejects an unknown subcommand with a usage error", () => {
expect(() => runSkillCommand(["frobnicate"])).toThrowError(CliSkillUsageError);
expect(() => runSkillCommand(["frobnicate"])).toThrowError(/Unknown skill subcommand/);
});
});
Loading