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
23 changes: 23 additions & 0 deletions .agentic/tasks/01KTYCSKQ7B9J720YPJM7T707D.json
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions packages/agentic/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions packages/agentic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
137 changes: 137 additions & 0 deletions packages/agentic/src/cli/commands/runtime.ts
Original file line number Diff line number Diff line change
@@ -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 <command> [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 <name> 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 <name>")

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)
}
34 changes: 34 additions & 0 deletions packages/agentic/src/cli/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import type {
ArtifactFinalizedOutput,
ArtifactInspectedOutput,
CapabilityDef,
RuntimeCommandOutput,
RuntimeListOutput,
} from "../types.js"


Expand Down Expand Up @@ -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")
}
57 changes: 57 additions & 0 deletions packages/agentic/src/cli/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
21 changes: 21 additions & 0 deletions packages/agentic/src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -136,6 +144,13 @@ const commands: Record<string, Command> = {
"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,
}

Expand Down Expand Up @@ -190,6 +205,12 @@ Commands:
capability show <name> Show capability declaration details
capability validate <name-or-file> Validate a capability by name or file path

runtime list List known runtime targets
runtime add <name> 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:
Expand Down
5 changes: 5 additions & 0 deletions packages/agentic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export type {
CapabilityPolicy,
CapabilityArtifacts,
CapabilityDef,
RuntimeCapability,
RuntimeCommandName,
RuntimeRef,
RuntimeListOutput,
RuntimeCommandOutput,
} from "./types.js"
export {
CAPABILITY_EFFECTS,
Expand Down
Loading