Skip to content
Merged
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
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/

# Ephemeral orchestrator/agent hand-off files created during setup — not meant for version control
/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
59 changes: 58 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 @@ -15369,6 +15398,20 @@ async function runCli(
output: plan.text.endsWith("\n") ? plan.text : `${plan.text}\n`,
exitCode: 0,
};
// Ensure ADE's bundled skills are seeded into the home-level dirs every runtime
// discovers, but only on the paths that actually launch an agent/runtime/skill —
// cheap commands like `ade help` and `ade --version` must not pay the scan/hash
// cost (cheap no-op when already current).
if (
plan.kind === "skill" ||
plan.kind === "ade-code" ||
plan.kind === "runtime" ||
plan.kind === "serve" ||
(plan.kind === "execute" &&
/^(agent spawn|chat create|shell start cli)\b/.test(plan.label))
) {
reseedBundledAdeSkillsForCli();
}
const originalConsole = {
log: console.log,
info: console.info,
Expand Down Expand Up @@ -15432,6 +15475,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
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
Loading