From ccc951fd089595cc6d650bcf789f528c346bc781 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Wed, 25 Mar 2026 14:51:48 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20skill=20injection=20=E2=80=94=20teach?= =?UTF-8?q?=20agents=20how=20to=20use=20spawn=20on=20recursive=20VMs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `--beta recursive` is active, a new "Spawn CLI" setup step injects agent-native instruction files teaching each agent how to use the `spawn` CLI to create child VMs. Skill files live in `skills/` at the repo root and use each agent's native format (YAML frontmatter for Claude/Codex/ OpenClaw, plain markdown for others, append mode for Hermes). - Add `skills/` directory with 8 agent-specific skill files - Add `spawn-skill.ts` module with path mapping, file reading, and injection - Register "spawn" as a conditional setup step gated by `--beta recursive` - Wire `injectSpawnSkill()` into orchestrate.ts postInstall flow - Add 52 tests covering path mapping, append mode, file existence, injection - Bump CLI version to 0.26.0 (minor: new feature) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cli/src/__tests__/spawn-skill.test.ts | 345 ++++++++++++++++++ packages/cli/src/shared/agents.ts | 26 +- packages/cli/src/shared/orchestrate.ts | 7 +- packages/cli/src/shared/spawn-skill.ts | 129 +++++++ skills/claude/SKILL.md | 38 ++ skills/codex/SKILL.md | 38 ++ skills/hermes/SOUL.md | 8 + skills/junie/AGENTS.md | 32 ++ skills/kilocode/spawn.md | 32 ++ skills/openclaw/SKILL.md | 38 ++ skills/opencode/AGENTS.md | 32 ++ skills/zeroclaw/AGENTS.md | 32 ++ 12 files changed, 750 insertions(+), 7 deletions(-) create mode 100644 packages/cli/src/__tests__/spawn-skill.test.ts create mode 100644 packages/cli/src/shared/spawn-skill.ts create mode 100644 skills/claude/SKILL.md create mode 100644 skills/codex/SKILL.md create mode 100644 skills/hermes/SOUL.md create mode 100644 skills/junie/AGENTS.md create mode 100644 skills/kilocode/spawn.md create mode 100644 skills/openclaw/SKILL.md create mode 100644 skills/opencode/AGENTS.md create mode 100644 skills/zeroclaw/AGENTS.md diff --git a/packages/cli/src/__tests__/spawn-skill.test.ts b/packages/cli/src/__tests__/spawn-skill.test.ts new file mode 100644 index 00000000..986163e3 --- /dev/null +++ b/packages/cli/src/__tests__/spawn-skill.test.ts @@ -0,0 +1,345 @@ +import { afterEach, describe, expect, it, mock } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { + getSpawnSkillPath, + getSpawnSkillSourceFile, + injectSpawnSkill, + isAppendMode, + readSkillContent, +} from "../shared/spawn-skill.js"; + +// ─── Path mapping tests ───────────────────────────────────────────────────── + +describe("getSpawnSkillPath", () => { + it("returns correct path for claude", () => { + expect(getSpawnSkillPath("claude")).toBe("~/.claude/skills/spawn/SKILL.md"); + }); + + it("returns correct path for codex", () => { + expect(getSpawnSkillPath("codex")).toBe("~/.agents/skills/spawn/SKILL.md"); + }); + + it("returns correct path for openclaw", () => { + expect(getSpawnSkillPath("openclaw")).toBe("~/.openclaw/skills/spawn/SKILL.md"); + }); + + it("returns correct path for zeroclaw", () => { + expect(getSpawnSkillPath("zeroclaw")).toBe("~/.zeroclaw/workspace/AGENTS.md"); + }); + + it("returns correct path for opencode", () => { + expect(getSpawnSkillPath("opencode")).toBe("~/.config/opencode/AGENTS.md"); + }); + + it("returns correct path for kilocode", () => { + expect(getSpawnSkillPath("kilocode")).toBe("~/.kilocode/rules/spawn.md"); + }); + + it("returns correct path for hermes", () => { + expect(getSpawnSkillPath("hermes")).toBe("~/.hermes/SOUL.md"); + }); + + it("returns correct path for junie", () => { + expect(getSpawnSkillPath("junie")).toBe("~/.junie/AGENTS.md"); + }); + + it("returns undefined for unknown agent", () => { + expect(getSpawnSkillPath("nonexistent")).toBeUndefined(); + }); +}); + +describe("getSpawnSkillSourceFile", () => { + it("returns correct source for claude", () => { + expect(getSpawnSkillSourceFile("claude")).toBe("claude/SKILL.md"); + }); + + it("returns correct source for codex", () => { + expect(getSpawnSkillSourceFile("codex")).toBe("codex/SKILL.md"); + }); + + it("returns correct source for openclaw", () => { + expect(getSpawnSkillSourceFile("openclaw")).toBe("openclaw/SKILL.md"); + }); + + it("returns correct source for zeroclaw", () => { + expect(getSpawnSkillSourceFile("zeroclaw")).toBe("zeroclaw/AGENTS.md"); + }); + + it("returns correct source for opencode", () => { + expect(getSpawnSkillSourceFile("opencode")).toBe("opencode/AGENTS.md"); + }); + + it("returns correct source for kilocode", () => { + expect(getSpawnSkillSourceFile("kilocode")).toBe("kilocode/spawn.md"); + }); + + it("returns correct source for hermes", () => { + expect(getSpawnSkillSourceFile("hermes")).toBe("hermes/SOUL.md"); + }); + + it("returns correct source for junie", () => { + expect(getSpawnSkillSourceFile("junie")).toBe("junie/AGENTS.md"); + }); + + it("returns undefined for unknown agent", () => { + expect(getSpawnSkillSourceFile("nonexistent")).toBeUndefined(); + }); +}); + +// ─── Append mode tests ────────────────────────────────────────────────────── + +describe("isAppendMode", () => { + it("returns true for hermes", () => { + expect(isAppendMode("hermes")).toBe(true); + }); + + it("returns false for claude", () => { + expect(isAppendMode("claude")).toBe(false); + }); + + it("returns false for codex", () => { + expect(isAppendMode("codex")).toBe(false); + }); + + it("returns false for openclaw", () => { + expect(isAppendMode("openclaw")).toBe(false); + }); + + it("returns false for zeroclaw", () => { + expect(isAppendMode("zeroclaw")).toBe(false); + }); + + it("returns false for opencode", () => { + expect(isAppendMode("opencode")).toBe(false); + }); + + it("returns false for kilocode", () => { + expect(isAppendMode("kilocode")).toBe(false); + }); + + it("returns false for junie", () => { + expect(isAppendMode("junie")).toBe(false); + }); +}); + +// ─── Skill file existence tests ───────────────────────────────────────────── + +describe("skill files exist in repo", () => { + // Find the skills/ directory relative to this test + const skillsDir = join(import.meta.dir, "../../../../skills"); + + const agents = [ + "claude", + "codex", + "openclaw", + "zeroclaw", + "opencode", + "kilocode", + "hermes", + "junie", + ]; + + for (const agent of agents) { + it(`skill file exists and is non-empty for ${agent}`, () => { + const sourceFile = getSpawnSkillSourceFile(agent); + expect(sourceFile).toBeDefined(); + const filePath = join(skillsDir, sourceFile!); + expect(existsSync(filePath)).toBe(true); + const content = readFileSync(filePath, "utf-8"); + expect(content.length).toBeGreaterThan(0); + }); + } + + for (const agent of [ + "claude", + "codex", + "openclaw", + ]) { + it(`${agent} skill file contains YAML frontmatter with name: spawn`, () => { + const sourceFile = getSpawnSkillSourceFile(agent); + const filePath = join(skillsDir, sourceFile!); + const content = readFileSync(filePath, "utf-8"); + expect(content).toStartWith("---\n"); + expect(content).toContain("name: spawn"); + }); + } + + for (const agent of [ + "zeroclaw", + "opencode", + "kilocode", + "junie", + ]) { + it(`${agent} skill file is plain markdown (no YAML frontmatter)`, () => { + const sourceFile = getSpawnSkillSourceFile(agent); + const filePath = join(skillsDir, sourceFile!); + const content = readFileSync(filePath, "utf-8"); + expect(content).toStartWith("# Spawn"); + }); + } +}); + +// ─── injectSpawnSkill tests ───────────────────────────────────────────────── + +describe("injectSpawnSkill", () => { + it("calls runner.runServer with correct base64 + path for claude", async () => { + let capturedCmd = ""; + const mockRunner = { + runServer: mock(async (cmd: string) => { + capturedCmd = cmd; + }), + uploadFile: mock(async () => {}), + }; + + await injectSpawnSkill(mockRunner, "claude"); + + expect(mockRunner.runServer).toHaveBeenCalledTimes(1); + expect(capturedCmd).toContain("~/.claude/skills/spawn/SKILL.md"); + expect(capturedCmd).toContain("mkdir -p ~/.claude/skills/spawn"); + expect(capturedCmd).toContain("base64 -d >"); + expect(capturedCmd).toContain("chmod 644"); + // Should use overwrite (>) not append (>>) + expect(capturedCmd).not.toContain(">>"); + }); + + it("uses append mode (>>) for hermes", async () => { + let capturedCmd = ""; + const mockRunner = { + runServer: mock(async (cmd: string) => { + capturedCmd = cmd; + }), + uploadFile: mock(async () => {}), + }; + + await injectSpawnSkill(mockRunner, "hermes"); + + expect(mockRunner.runServer).toHaveBeenCalledTimes(1); + expect(capturedCmd).toContain("~/.hermes/SOUL.md"); + expect(capturedCmd).toContain(">>"); + // Should NOT contain chmod for append mode + expect(capturedCmd).not.toContain("chmod"); + }); + + it("creates parent directories for all agents", async () => { + const agents = [ + "claude", + "codex", + "openclaw", + "zeroclaw", + "opencode", + "kilocode", + "hermes", + "junie", + ]; + for (const agent of agents) { + let capturedCmd = ""; + const mockRunner = { + runServer: mock(async (cmd: string) => { + capturedCmd = cmd; + }), + uploadFile: mock(async () => {}), + }; + + await injectSpawnSkill(mockRunner, agent); + expect(capturedCmd).toContain("mkdir -p"); + } + }); + + it("handles runner failure gracefully", async () => { + const mockRunner = { + runServer: mock(async () => { + throw new Error("SSH connection refused"); + }), + uploadFile: mock(async () => {}), + }; + + // Should not throw + await injectSpawnSkill(mockRunner, "claude"); + expect(mockRunner.runServer).toHaveBeenCalledTimes(1); + }); + + it("does nothing for unknown agent", async () => { + const mockRunner = { + runServer: mock(async () => {}), + uploadFile: mock(async () => {}), + }; + + await injectSpawnSkill(mockRunner, "nonexistent"); + expect(mockRunner.runServer).not.toHaveBeenCalled(); + }); + + it("base64-encodes real skill content", async () => { + let capturedCmd = ""; + const mockRunner = { + runServer: mock(async (cmd: string) => { + capturedCmd = cmd; + }), + uploadFile: mock(async () => {}), + }; + + await injectSpawnSkill(mockRunner, "codex"); + + // Extract the base64 string from the command + const b64Match = capturedCmd.match(/printf '%s' '([A-Za-z0-9+/=]+)'/); + expect(b64Match).not.toBeNull(); + // Decode and verify it contains spawn skill content + const decoded = Buffer.from(b64Match![1], "base64").toString("utf-8"); + expect(decoded).toContain("Spawn"); + expect(decoded).toContain("spawn"); + }); +}); + +// ─── readSkillContent tests ───────────────────────────────────────────────── + +describe("readSkillContent", () => { + it("returns content for known agent", () => { + const content = readSkillContent("claude"); + expect(content).not.toBeNull(); + expect(content).toContain("Spawn"); + }); + + it("returns null for unknown agent", () => { + expect(readSkillContent("nonexistent")).toBeNull(); + }); +}); + +// ─── "spawn" step visibility tests ────────────────────────────────────────── + +describe("spawn step gating", () => { + const savedBeta = process.env.SPAWN_BETA; + + afterEach(() => { + if (savedBeta === undefined) { + delete process.env.SPAWN_BETA; + } else { + process.env.SPAWN_BETA = savedBeta; + } + }); + + it("spawn step appears when SPAWN_BETA includes recursive", async () => { + process.env.SPAWN_BETA = "recursive"; + // Re-import to pick up the env var (the function reads env at call time) + const { getAgentOptionalSteps } = await import("../shared/agents.js"); + const steps = getAgentOptionalSteps("claude"); + const spawnStep = steps.find((s) => s.value === "spawn"); + expect(spawnStep).toBeDefined(); + expect(spawnStep!.defaultOn).toBe(true); + }); + + it("spawn step does not appear without --beta recursive", async () => { + delete process.env.SPAWN_BETA; + const { getAgentOptionalSteps } = await import("../shared/agents.js"); + const steps = getAgentOptionalSteps("claude"); + const spawnStep = steps.find((s) => s.value === "spawn"); + expect(spawnStep).toBeUndefined(); + }); + + it("spawn step appears alongside other beta features", async () => { + process.env.SPAWN_BETA = "tarball,recursive"; + const { getAgentOptionalSteps } = await import("../shared/agents.js"); + const steps = getAgentOptionalSteps("openclaw"); + const spawnStep = steps.find((s) => s.value === "spawn"); + expect(spawnStep).toBeDefined(); + }); +}); diff --git a/packages/cli/src/shared/agents.ts b/packages/cli/src/shared/agents.ts index 61f59dfe..0c228a0c 100644 --- a/packages/cli/src/shared/agents.ts +++ b/packages/cli/src/shared/agents.ts @@ -113,6 +113,14 @@ const AGENT_EXTRA_STEPS: Record = { ], }; +/** The "spawn" step — only shown when --beta recursive is active. */ +const SPAWN_STEP: OptionalStep = { + value: "spawn", + label: "Spawn CLI", + hint: "install spawn for recursive VM creation", + defaultOn: true, +}; + /** Steps shown for every agent. */ const COMMON_STEPS: OptionalStep[] = [ { @@ -140,13 +148,23 @@ const COMMON_STEPS: OptionalStep[] = [ /** Get the optional setup steps for a given agent (no CloudRunner required). */ export function getAgentOptionalSteps(agentName: string): OptionalStep[] { - const extra = AGENT_EXTRA_STEPS[agentName]; - return extra + const betaFeatures = (process.env.SPAWN_BETA ?? "").split(",").filter(Boolean); + const hasRecursive = betaFeatures.includes("recursive"); + + const steps = hasRecursive ? [ ...COMMON_STEPS, - ...extra, + SPAWN_STEP, ] - : COMMON_STEPS; + : [ + ...COMMON_STEPS, + ]; + + const extra = AGENT_EXTRA_STEPS[agentName]; + if (extra) { + steps.push(...extra); + } + return steps; } /** Validate step names against the known steps for an agent. diff --git a/packages/cli/src/shared/orchestrate.ts b/packages/cli/src/shared/orchestrate.ts index 3427fc5c..aaeaa9fe 100644 --- a/packages/cli/src/shared/orchestrate.ts +++ b/packages/cli/src/shared/orchestrate.ts @@ -17,6 +17,7 @@ import { getOrPromptApiKey } from "./oauth.js"; import { getSpawnCloudConfigPath, getSpawnPreferencesPath, getUserHome } from "./paths.js"; import { asyncTryCatch, asyncTryCatchIf, isOperationalError, tryCatch } from "./result.js"; import { isWindows } from "./shell.js"; +import { injectSpawnSkill } from "./spawn-skill.js"; import { sleep, startSshTunnel } from "./ssh.js"; import { ensureSshKeys, getSshKeyOpts } from "./ssh-keys.js"; import { @@ -549,11 +550,11 @@ async function postInstall( await setupAutoUpdate(cloud.runner, agentName, agent.updateCmd); } - // Recursive spawn setup — install spawn CLI and delegate credentials - const betaFeaturesPost = new Set((process.env.SPAWN_BETA ?? "").split(",").filter(Boolean)); - if (betaFeaturesPost.has("recursive") && cloud.cloudName !== "local") { + // Spawn CLI + skill injection (recursive spawn) + if (enabledSteps?.has("spawn") && cloud.cloudName !== "local") { await installSpawnCli(cloud.runner); await delegateCloudCredentials(cloud.runner, cloud.cloudName); + await injectSpawnSkill(cloud.runner, agentName); } // Pre-launch hooks (retry loop) diff --git a/packages/cli/src/shared/spawn-skill.ts b/packages/cli/src/shared/spawn-skill.ts new file mode 100644 index 00000000..13b7b6cf --- /dev/null +++ b/packages/cli/src/shared/spawn-skill.ts @@ -0,0 +1,129 @@ +// shared/spawn-skill.ts — Skill injection for recursive spawn +// Writes agent-native instruction files teaching each agent how to use `spawn`. + +import type { CloudRunner } from "./agent-setup.js"; + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { wrapSshCall } from "./agent-setup.js"; +import { asyncTryCatchIf, isOperationalError, tryCatch } from "./result.js"; +import { logInfo, logWarn } from "./ui.js"; + +/** Map agent name → remote path where the skill file should be written. */ +const SKILL_REMOTE_PATHS: Record = { + claude: "~/.claude/skills/spawn/SKILL.md", + codex: "~/.agents/skills/spawn/SKILL.md", + openclaw: "~/.openclaw/skills/spawn/SKILL.md", + zeroclaw: "~/.zeroclaw/workspace/AGENTS.md", + opencode: "~/.config/opencode/AGENTS.md", + kilocode: "~/.kilocode/rules/spawn.md", + hermes: "~/.hermes/SOUL.md", + junie: "~/.junie/AGENTS.md", +}; + +/** Map agent name → local file inside the skills/ directory. */ +const SKILL_SOURCE_FILES: Record = { + claude: "claude/SKILL.md", + codex: "codex/SKILL.md", + openclaw: "openclaw/SKILL.md", + zeroclaw: "zeroclaw/AGENTS.md", + opencode: "opencode/AGENTS.md", + kilocode: "kilocode/spawn.md", + hermes: "hermes/SOUL.md", + junie: "junie/AGENTS.md", +}; + +/** Agents that use append mode (>>) instead of overwrite (>). */ +const APPEND_AGENTS = new Set([ + "hermes", +]); + +/** Get the remote target path for a given agent's spawn skill file. */ +export function getSpawnSkillPath(agentName: string): string | undefined { + return SKILL_REMOTE_PATHS[agentName]; +} + +/** Get the local source file path (relative to skills/) for a given agent. */ +export function getSpawnSkillSourceFile(agentName: string): string | undefined { + return SKILL_SOURCE_FILES[agentName]; +} + +/** Whether the agent uses append mode (hermes appends to SOUL.md). */ +export function isAppendMode(agentName: string): boolean { + return APPEND_AGENTS.has(agentName); +} + +/** + * Resolve the absolute path to the skills/ directory. + * Works both in dev (source tree) and when bundled (cli.js next to skills/). + */ +function getSkillsDir(): string { + // In the source tree: packages/cli/src/shared/spawn-skill.ts + // skills/ is at the repo root: ../../../../skills/ + // When bundled as cli.js: packages/cli/cli.js → ../../skills/ + // Use import.meta.dir which gives the directory of the current file. + const candidates = [ + join(import.meta.dir, "../../../../skills"), + join(import.meta.dir, "../../../skills"), + join(import.meta.dir, "../../skills"), + ]; + for (const candidate of candidates) { + const r = tryCatch(() => readFileSync(join(candidate, "claude/SKILL.md"))); + if (r.ok) { + return candidate; + } + } + // Fallback: assume repo root relative to process.cwd() + return join(process.cwd(), "skills"); +} + +/** + * Read a skill file's content from the local skills/ directory. + * Returns null if the file doesn't exist or the agent has no skill file. + */ +export function readSkillContent(agentName: string): string | null { + const sourceFile = getSpawnSkillSourceFile(agentName); + if (!sourceFile) { + return null; + } + const r = tryCatch(() => readFileSync(join(getSkillsDir(), sourceFile), "utf-8")); + return r.ok ? r.data : null; +} + +/** + * Inject the spawn skill file onto a remote VM for the given agent. + * Reads content from skills/{agent}/, base64-encodes it, and writes + * to the agent's native instruction file path on the remote. + */ +export async function injectSpawnSkill(runner: CloudRunner, agentName: string): Promise { + const remotePath = getSpawnSkillPath(agentName); + const content = readSkillContent(agentName); + + if (!remotePath || !content) { + logWarn(`No spawn skill file for agent: ${agentName}`); + return; + } + + const b64 = Buffer.from(content).toString("base64"); + if (!/^[A-Za-z0-9+/=]+$/.test(b64)) { + throw new Error("Unexpected characters in base64 output"); + } + + const append = isAppendMode(agentName); + const operator = append ? ">>" : ">"; + // dirname of ~ paths like ~/.claude/skills/spawn/SKILL.md + // We need to extract the directory portion for mkdir -p + const remoteDir = remotePath.slice(0, remotePath.lastIndexOf("/")); + + const cmd = append + ? `mkdir -p ${remoteDir} && printf '%s' '${b64}' | base64 -d ${operator} ${remotePath}` + : `mkdir -p ${remoteDir} && printf '%s' '${b64}' | base64 -d ${operator} ${remotePath} && chmod 644 ${remotePath}`; + + const result = await asyncTryCatchIf(isOperationalError, () => wrapSshCall(runner.runServer(cmd))); + + if (result.ok) { + logInfo(`Spawn skill injected: ${remotePath}`); + } else { + logWarn("Spawn skill injection failed — agent will work without spawn instructions"); + } +} diff --git a/skills/claude/SKILL.md b/skills/claude/SKILL.md new file mode 100644 index 00000000..5918846d --- /dev/null +++ b/skills/claude/SKILL.md @@ -0,0 +1,38 @@ +--- +name: spawn +description: Create child cloud VMs with AI coding agents using the spawn CLI +allowed-tools: Bash +--- + +# Spawn — Create Child VMs + +You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents. + +## Usage + +```bash +spawn --headless --output json --prompt "task description" +``` + +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Clouds: hetzner, digitalocean, aws, gcp, sprite + +## Example + +```bash +spawn claude hetzner --headless --output json --prompt "Implement the auth module" +``` + +Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_id":"..."}` + +## Managing Children + +- `spawn list --json` — see running children +- `spawn delete` — tear down a child VM +- `spawn tree` — see the full spawn tree + +## Context + +- You are running inside a spawned VM (SPAWN_DEPTH is set) +- Cloud credentials are pre-configured — no auth prompts +- OpenRouter billing is shared with the parent diff --git a/skills/codex/SKILL.md b/skills/codex/SKILL.md new file mode 100644 index 00000000..5918846d --- /dev/null +++ b/skills/codex/SKILL.md @@ -0,0 +1,38 @@ +--- +name: spawn +description: Create child cloud VMs with AI coding agents using the spawn CLI +allowed-tools: Bash +--- + +# Spawn — Create Child VMs + +You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents. + +## Usage + +```bash +spawn --headless --output json --prompt "task description" +``` + +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Clouds: hetzner, digitalocean, aws, gcp, sprite + +## Example + +```bash +spawn claude hetzner --headless --output json --prompt "Implement the auth module" +``` + +Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_id":"..."}` + +## Managing Children + +- `spawn list --json` — see running children +- `spawn delete` — tear down a child VM +- `spawn tree` — see the full spawn tree + +## Context + +- You are running inside a spawned VM (SPAWN_DEPTH is set) +- Cloud credentials are pre-configured — no auth prompts +- OpenRouter billing is shared with the parent diff --git a/skills/hermes/SOUL.md b/skills/hermes/SOUL.md new file mode 100644 index 00000000..f4ed732a --- /dev/null +++ b/skills/hermes/SOUL.md @@ -0,0 +1,8 @@ + + +## Spawn Capability + +You have the `spawn` CLI for creating child cloud VMs with AI agents. +Use `spawn --headless --output json --prompt "task"` to delegate work. +Available agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie. +Cloud credentials are pre-configured. Run `spawn list --json` to see children. diff --git a/skills/junie/AGENTS.md b/skills/junie/AGENTS.md new file mode 100644 index 00000000..c0f13a22 --- /dev/null +++ b/skills/junie/AGENTS.md @@ -0,0 +1,32 @@ +# Spawn — Create Child VMs + +You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents. + +## Usage + +```bash +spawn --headless --output json --prompt "task description" +``` + +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Clouds: hetzner, digitalocean, aws, gcp, sprite + +## Example + +```bash +spawn claude hetzner --headless --output json --prompt "Implement the auth module" +``` + +Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_id":"..."}` + +## Managing Children + +- `spawn list --json` — see running children +- `spawn delete` — tear down a child VM +- `spawn tree` — see the full spawn tree + +## Context + +- You are running inside a spawned VM (SPAWN_DEPTH is set) +- Cloud credentials are pre-configured — no auth prompts +- OpenRouter billing is shared with the parent diff --git a/skills/kilocode/spawn.md b/skills/kilocode/spawn.md new file mode 100644 index 00000000..c0f13a22 --- /dev/null +++ b/skills/kilocode/spawn.md @@ -0,0 +1,32 @@ +# Spawn — Create Child VMs + +You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents. + +## Usage + +```bash +spawn --headless --output json --prompt "task description" +``` + +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Clouds: hetzner, digitalocean, aws, gcp, sprite + +## Example + +```bash +spawn claude hetzner --headless --output json --prompt "Implement the auth module" +``` + +Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_id":"..."}` + +## Managing Children + +- `spawn list --json` — see running children +- `spawn delete` — tear down a child VM +- `spawn tree` — see the full spawn tree + +## Context + +- You are running inside a spawned VM (SPAWN_DEPTH is set) +- Cloud credentials are pre-configured — no auth prompts +- OpenRouter billing is shared with the parent diff --git a/skills/openclaw/SKILL.md b/skills/openclaw/SKILL.md new file mode 100644 index 00000000..5918846d --- /dev/null +++ b/skills/openclaw/SKILL.md @@ -0,0 +1,38 @@ +--- +name: spawn +description: Create child cloud VMs with AI coding agents using the spawn CLI +allowed-tools: Bash +--- + +# Spawn — Create Child VMs + +You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents. + +## Usage + +```bash +spawn --headless --output json --prompt "task description" +``` + +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Clouds: hetzner, digitalocean, aws, gcp, sprite + +## Example + +```bash +spawn claude hetzner --headless --output json --prompt "Implement the auth module" +``` + +Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_id":"..."}` + +## Managing Children + +- `spawn list --json` — see running children +- `spawn delete` — tear down a child VM +- `spawn tree` — see the full spawn tree + +## Context + +- You are running inside a spawned VM (SPAWN_DEPTH is set) +- Cloud credentials are pre-configured — no auth prompts +- OpenRouter billing is shared with the parent diff --git a/skills/opencode/AGENTS.md b/skills/opencode/AGENTS.md new file mode 100644 index 00000000..c0f13a22 --- /dev/null +++ b/skills/opencode/AGENTS.md @@ -0,0 +1,32 @@ +# Spawn — Create Child VMs + +You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents. + +## Usage + +```bash +spawn --headless --output json --prompt "task description" +``` + +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Clouds: hetzner, digitalocean, aws, gcp, sprite + +## Example + +```bash +spawn claude hetzner --headless --output json --prompt "Implement the auth module" +``` + +Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_id":"..."}` + +## Managing Children + +- `spawn list --json` — see running children +- `spawn delete` — tear down a child VM +- `spawn tree` — see the full spawn tree + +## Context + +- You are running inside a spawned VM (SPAWN_DEPTH is set) +- Cloud credentials are pre-configured — no auth prompts +- OpenRouter billing is shared with the parent diff --git a/skills/zeroclaw/AGENTS.md b/skills/zeroclaw/AGENTS.md new file mode 100644 index 00000000..c0f13a22 --- /dev/null +++ b/skills/zeroclaw/AGENTS.md @@ -0,0 +1,32 @@ +# Spawn — Create Child VMs + +You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents. + +## Usage + +```bash +spawn --headless --output json --prompt "task description" +``` + +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Clouds: hetzner, digitalocean, aws, gcp, sprite + +## Example + +```bash +spawn claude hetzner --headless --output json --prompt "Implement the auth module" +``` + +Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_id":"..."}` + +## Managing Children + +- `spawn list --json` — see running children +- `spawn delete` — tear down a child VM +- `spawn tree` — see the full spawn tree + +## Context + +- You are running inside a spawned VM (SPAWN_DEPTH is set) +- Cloud credentials are pre-configured — no auth prompts +- OpenRouter billing is shared with the parent