From f1110de364605f50b7d2fc8cee7efa7716cb6ac2 Mon Sep 17 00:00:00 2001 From: Travis Nesland Date: Fri, 12 Jun 2026 13:18:33 -0400 Subject: [PATCH] feat(cli): add runtime command namespace --- .../tasks/01KTYCSKQ7B9J720YPJM7T707D.json | 23 +++ packages/agentic/AGENTS.md | 1 + packages/agentic/README.md | 5 + packages/agentic/src/cli/commands/runtime.ts | 137 ++++++++++++++++++ packages/agentic/src/cli/format.ts | 34 +++++ packages/agentic/src/cli/main.test.ts | 57 ++++++++ packages/agentic/src/cli/main.ts | 21 +++ packages/agentic/src/index.ts | 5 + packages/agentic/src/types.ts | 43 ++++++ 9 files changed, 326 insertions(+) create mode 100644 .agentic/tasks/01KTYCSKQ7B9J720YPJM7T707D.json create mode 100644 packages/agentic/src/cli/commands/runtime.ts diff --git a/.agentic/tasks/01KTYCSKQ7B9J720YPJM7T707D.json b/.agentic/tasks/01KTYCSKQ7B9J720YPJM7T707D.json new file mode 100644 index 0000000..a9ce45a --- /dev/null +++ b/.agentic/tasks/01KTYCSKQ7B9J720YPJM7T707D.json @@ -0,0 +1,23 @@ +{ + "description": "GitHub #106: CLI: add runtime command namespace", + "status": "done", + "tags": [ + "spores", + "cli", + "runtime", + "github-106" + ], + "id": "01KTYCSKQ7B9J720YPJM7T707D", + "annotations": [ + { + "text": "status: ready → in_progress", + "timestamp": "2026-06-12T17:06:48.577Z" + }, + { + "text": "status: in_progress → done", + "timestamp": "2026-06-12T17:17:44.246Z" + } + ], + "created_at": "2026-06-12T17:06:39.463Z", + "updated_at": "2026-06-12T17:17:44.246Z" +} \ No newline at end of file diff --git a/packages/agentic/AGENTS.md b/packages/agentic/AGENTS.md index ddbd79a..7252161 100644 --- a/packages/agentic/AGENTS.md +++ b/packages/agentic/AGENTS.md @@ -93,6 +93,7 @@ Current command surface: - `agentic workflow list/show/run/status` - `agentic persona list/view/activate` - `agentic artifact create/read/write/edit/inspect/list/finalize` +- `agentic runtime list/add/init/run/status` — runtime package namespace only; package install/discovery/delegation belongs to the runtime lane ### Skills on disk diff --git a/packages/agentic/README.md b/packages/agentic/README.md index 46c3c33..c3cd5f7 100644 --- a/packages/agentic/README.md +++ b/packages/agentic/README.md @@ -49,6 +49,9 @@ agentic task next # Activate a persona (pipe into your LLM as system prompt) agentic persona list agentic persona activate spores-maintainer | llm --system - + +# Preview the runtime package front door +agentic runtime list ``` ## Primitives @@ -61,6 +64,8 @@ agentic persona activate spores-maintainer | llm --system - | **Tasks** | ULID-keyed task queue with status transitions and annotations | | **Persona** | Activate a "hat" at the start of a turn: memory tags, skills, task filter, workflow, and a rendered body with live situational facts | +The `agentic runtime ...` namespace is the CLI front door for optional runtime packages. Core Agentic recognizes official targets such as `local`, but package install, discovery, and command delegation live outside core and are being backfilled through runtime packages. + All five primitives read from `.agentic/` in your project root, with optional global overrides from `~/.agentic/`. Legacy `.spores/` and `~/.spores/` paths are honoured during the compatibility window when `.agentic/` is absent. ## What Agentic is not diff --git a/packages/agentic/src/cli/commands/runtime.ts b/packages/agentic/src/cli/commands/runtime.ts new file mode 100644 index 0000000..3ad8178 --- /dev/null +++ b/packages/agentic/src/cli/commands/runtime.ts @@ -0,0 +1,137 @@ +import type { + RuntimeCommandName, + RuntimeCommandOutput, + RuntimeListOutput, + RuntimeRef, +} from "../../types.js" +import type { Command } from "../context.js" +import { + formatRuntimeAction, + formatRuntimeHelp, + formatRuntimeList, +} from "../format.js" +import { output } from "../output.js" + +const PACKAGE_DISCOVERY_NOTE = + "Runtime packages are optional. This slice adds the CLI namespace only; package install, config writes, discovery, and delegation are intentionally deferred." + +const OFFICIAL_RUNTIMES: RuntimeRef[] = [ + { + name: "local", + package_name: "@tnezdev/agentic-runtime-local", + description: "Run Agentic workspaces on the local machine.", + status: "known", + capabilities: ["init", "run", "status"], + install_command: "bun add -d @tnezdev/agentic-runtime-local", + }, +] + +const RUNTIME_HELP = `Usage: agentic runtime [args] + +Runtime packages are optional packages that make Agentic workspaces runnable. +Core Agentic provides this CLI front door; runtime packages own harness and +platform integration. + +Commands: + runtime list List known runtime targets + runtime add Show package guidance for a runtime target + runtime init [name] Initialize a configured runtime target + runtime run [target] Run a target with the default runtime + runtime status [name] Show runtime availability guidance` + +function knownRuntimeNames(): string { + return OFFICIAL_RUNTIMES.map((runtime) => runtime.name).join(", ") +} + +function getRuntime(name: string): RuntimeRef { + const runtime = OFFICIAL_RUNTIMES.find((candidate) => candidate.name === name) + if (runtime === undefined) { + throw new Error( + `Unknown runtime target "${name}". Known official targets: ${knownRuntimeNames()}.`, + ) + } + return runtime +} + +function runtimeAction( + command: RuntimeCommandName, + runtime: RuntimeRef, + status: RuntimeCommandOutput["status"], + message: string, + target?: string | undefined, +): RuntimeCommandOutput { + return { + command, + runtime, + ...(target !== undefined ? { target } : {}), + status, + message, + next_steps: [ + `Runtime package: ${runtime.package_name}`, + `Install command once the package exists: ${runtime.install_command}`, + "Next runtime slice: package discovery, config writes, and command delegation.", + ], + } +} + +function unavailableError( + command: RuntimeCommandName, + runtime: RuntimeRef, + target?: string | undefined, +): Error { + const action = runtimeAction( + command, + runtime, + "needs_package", + `Cannot ${command} with runtime "${runtime.name}" yet because runtime package discovery/delegation is not implemented in core.`, + target, + ) + return new Error([action.message, ...action.next_steps].join(" ")) +} + +export const runtimeHelpCommand: Command = async (ctx) => { + output(ctx, RUNTIME_HELP, formatRuntimeHelp) +} + +export const runtimeListCommand: Command = async (ctx) => { + const result: RuntimeListOutput = { + runtimes: OFFICIAL_RUNTIMES, + note: PACKAGE_DISCOVERY_NOTE, + } + output(ctx, result, formatRuntimeList) +} + +export const runtimeAddCommand: Command = async (ctx, args) => { + const name = args[0] + if (name === undefined) throw new Error("Usage: agentic runtime add ") + + const runtime = getRuntime(name) + const result = runtimeAction( + "add", + runtime, + "recognized", + `Runtime target "${runtime.name}" resolves to ${runtime.package_name}. This command does not mutate config until package discovery lands.`, + ) + output(ctx, result, formatRuntimeAction) +} + +export const runtimeInitCommand: Command = async (_ctx, args) => { + const runtime = getRuntime(args[0] ?? "local") + throw unavailableError("init", runtime) +} + +export const runtimeRunCommand: Command = async (_ctx, args) => { + const runtime = getRuntime("local") + throw unavailableError("run", runtime, args[0]) +} + +export const runtimeStatusCommand: Command = async (ctx, args) => { + const runtime = getRuntime(args[0] ?? "local") + const result = runtimeAction( + "status", + runtime, + "needs_package", + `Runtime target "${runtime.name}" is known, but runtime package discovery/delegation is not implemented yet.`, + ) + output(ctx, result, formatRuntimeAction) +} diff --git a/packages/agentic/src/cli/format.ts b/packages/agentic/src/cli/format.ts index 01efbd1..0afc2c1 100644 --- a/packages/agentic/src/cli/format.ts +++ b/packages/agentic/src/cli/format.ts @@ -36,6 +36,8 @@ import type { ArtifactFinalizedOutput, ArtifactInspectedOutput, CapabilityDef, + RuntimeCommandOutput, + RuntimeListOutput, } from "../types.js" @@ -611,3 +613,35 @@ export function formatCapabilityValidate(result: CapabilityValidateResult): stri const errorLines = result.errors.map((e) => ` ${e.field}: ${e.message}`) return [`${result.subject}: invalid`, ...errorLines].join("\n") } + +// --------------------------------------------------------------------------- +// Runtime formatters +// --------------------------------------------------------------------------- + +export function formatRuntimeHelp(help: string): string { + return help +} + +export function formatRuntimeList(result: RuntimeListOutput): string { + const rows = result.runtimes.map((runtime) => [ + runtime.name, + runtime.package_name, + runtime.status, + runtime.capabilities.join(", "), + ]) + const body = + rows.length === 0 + ? "No runtime targets known." + : table(["NAME", "PACKAGE", "STATUS", "CAPABILITIES"], rows) + return [body, "", result.note].join("\n") +} + +export function formatRuntimeAction(result: RuntimeCommandOutput): string { + return [ + `${result.runtime.name}: ${result.status}`, + result.message, + ...(result.target !== undefined ? [`target: ${result.target}`] : []), + "next steps:", + ...result.next_steps.map((step) => ` - ${step}`), + ].join("\n") +} diff --git a/packages/agentic/src/cli/main.test.ts b/packages/agentic/src/cli/main.test.ts index 5683878..93e46af 100644 --- a/packages/agentic/src/cli/main.test.ts +++ b/packages/agentic/src/cli/main.test.ts @@ -46,6 +46,63 @@ describe("CLI", () => { const { stdout, exitCode } = await run("--help") expect(exitCode).toBe(0) expect(stdout).toContain("Usage: agentic") + expect(stdout).toContain("runtime list") + }) + + it("routes runtime help", async () => { + const { stdout, exitCode } = await run(...base, "runtime") + expect(exitCode).toBe(0) + expect(stdout).toContain("Usage: agentic runtime") + }) + + it("routes runtime list", async () => { + const result = (await runJson(...base, "runtime", "list")) as { + runtimes: Array<{ name: string; package_name: string }> + note: string + } + expect(result.runtimes[0]!.name).toBe("local") + expect(result.runtimes[0]!.package_name).toBe("@tnezdev/agentic-runtime-local") + expect(result.note).toContain("CLI namespace only") + }) + + it("runtime add gives package guidance for local", async () => { + const result = (await runJson(...base, "runtime", "add", "local")) as { + command: string + status: string + runtime: { name: string; package_name: string } + next_steps: string[] + } + expect(result.command).toBe("add") + expect(result.status).toBe("recognized") + expect(result.runtime.package_name).toBe("@tnezdev/agentic-runtime-local") + expect(result.next_steps.join("\n")).toContain("bun add -d @tnezdev/agentic-runtime-local") + }) + + it("runtime add rejects unknown runtime names", async () => { + const { stdout, exitCode } = await run( + "--json", + ...base, + "runtime", + "add", + "spaceship", + ) + expect(exitCode).toBe(1) + const result = JSON.parse(stdout) as { error: string } + expect(result.error).toContain('Unknown runtime target "spaceship"') + }) + + it("runtime run fails with actionable package guidance before delegation exists", async () => { + const { stdout, exitCode } = await run( + "--json", + ...base, + "runtime", + "run", + "inbox-review", + ) + expect(exitCode).toBe(1) + const result = JSON.parse(stdout) as { error: string } + expect(result.error).toContain("@tnezdev/agentic-runtime-local") + expect(result.error).toContain("bun add -d @tnezdev/agentic-runtime-local") }) it("exits 1 on unknown command", async () => { diff --git a/packages/agentic/src/cli/main.ts b/packages/agentic/src/cli/main.ts index 1ba8c4f..cc3cd6b 100644 --- a/packages/agentic/src/cli/main.ts +++ b/packages/agentic/src/cli/main.ts @@ -59,6 +59,14 @@ import { capabilityShowCommand, capabilityValidateCommand, } from "./commands/capability.js" +import { + runtimeAddCommand, + runtimeHelpCommand, + runtimeInitCommand, + runtimeListCommand, + runtimeRunCommand, + runtimeStatusCommand, +} from "./commands/runtime.js" type Parsed = { positional: string[] @@ -136,6 +144,13 @@ const commands: Record = { "capability list": capabilityListCommand, "capability show": capabilityShowCommand, "capability validate": capabilityValidateCommand, + runtime: runtimeHelpCommand, + "runtime help": runtimeHelpCommand, + "runtime list": runtimeListCommand, + "runtime add": runtimeAddCommand, + "runtime init": runtimeInitCommand, + "runtime run": runtimeRunCommand, + "runtime status": runtimeStatusCommand, wake: wakeCommand, } @@ -190,6 +205,12 @@ Commands: capability show Show capability declaration details capability validate Validate a capability by name or file path + runtime list List known runtime targets + runtime add Show package guidance for a runtime target + runtime init [name] Initialize a configured runtime target + runtime run [target] Run a target with the default runtime + runtime status [name] Show runtime availability guidance + wake Session bootstrap — identity, environment, personas Flags: diff --git a/packages/agentic/src/index.ts b/packages/agentic/src/index.ts index 431db47..4b253a6 100644 --- a/packages/agentic/src/index.ts +++ b/packages/agentic/src/index.ts @@ -63,6 +63,11 @@ export type { CapabilityPolicy, CapabilityArtifacts, CapabilityDef, + RuntimeCapability, + RuntimeCommandName, + RuntimeRef, + RuntimeListOutput, + RuntimeCommandOutput, } from "./types.js" export { CAPABILITY_EFFECTS, diff --git a/packages/agentic/src/types.ts b/packages/agentic/src/types.ts index 91a9b0b..b1dc730 100644 --- a/packages/agentic/src/types.ts +++ b/packages/agentic/src/types.ts @@ -364,6 +364,49 @@ export type DispatchHandlerHooks = { onUnregister?: () => Promise } +// --------------------------------------------------------------------------- +// Runtime package CLI types +// +// Core owns the runtime CLI front door and package seam. Runtime packages own +// harness/platform integration. These types describe the narrow placeholder +// surface used before package discovery/delegation lands. +// --------------------------------------------------------------------------- + +export type RuntimeCapability = + | "init" + | "run" + | "status" + | "dev" + | "deploy" + | "interactive" + | "json-events" + | "harness-sessions" + +export type RuntimeCommandName = "add" | "init" | "run" | "status" + +export type RuntimeRef = { + name: string + package_name: string + description: string + status: "known" + capabilities: RuntimeCapability[] + install_command: string +} + +export type RuntimeListOutput = { + runtimes: RuntimeRef[] + note: string +} + +export type RuntimeCommandOutput = { + command: RuntimeCommandName + runtime: RuntimeRef + target?: string | undefined + status: "recognized" | "needs_package" + message: string + next_steps: string[] +} + // --------------------------------------------------------------------------- // Lifecycle event types //