@ag-eco/agentplate-cli 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ag-eco/agentplate-cli",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "description": "Multi-agent orchestration for AI coding agents — spawn workers in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution. Pluggable runtime adapters for Claude Code, Pi, and more.",
5
5
  "author": "Jaymin West",
6
6
  "license": "MIT",
@@ -682,6 +682,34 @@ describe("startCoordinator", () => {
682
682
  expect(cmd).not.toContain("--model opus");
683
683
  });
684
684
 
685
+ test("--runtime overrides the spawned runtime adapter (pi)", async () => {
686
+ const { deps, calls } = makeDeps();
687
+ const originalSleep = Bun.sleep;
688
+ Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
689
+
690
+ try {
691
+ await captureStdout(() =>
692
+ coordinatorCommand(["start", "--no-attach", "--json", "--runtime", "pi"], deps),
693
+ );
694
+ } finally {
695
+ Bun.sleep = originalSleep;
696
+ }
697
+
698
+ expect(calls.createSession).toHaveLength(1);
699
+ const cmd = calls.createSession[0]?.command ?? "";
700
+ // Pi's buildSpawnCommand emits `pi --model <provider/model>`; the default
701
+ // claude runtime would not. Proves --runtime took precedence over config.
702
+ expect(cmd).toContain("pi --model");
703
+ expect(cmd).toContain("opus");
704
+ });
705
+
706
+ test("--runtime rejects an unknown adapter with ValidationError", async () => {
707
+ const { deps } = makeDeps();
708
+ await expect(
709
+ coordinatorCommand(["start", "--no-attach", "--runtime", "bogus"], deps),
710
+ ).rejects.toThrow(ValidationError);
711
+ });
712
+
685
713
  test("--json outputs JSON with expected fields", async () => {
686
714
  const { deps } = makeDeps();
687
715
  const originalSleep = Bun.sleep;
@@ -24,7 +24,7 @@ import { jsonOutput } from "../json.ts";
24
24
  import { printHint, printSuccess, printWarning } from "../logging/color.ts";
25
25
  import { createMailClient } from "../mail/client.ts";
26
26
  import { createMailStore } from "../mail/store.ts";
27
- import { getRuntime } from "../runtimes/registry.ts";
27
+ import { getRuntime, getRuntimeNames } from "../runtimes/registry.ts";
28
28
  import { openSessionStore } from "../sessions/compat.ts";
29
29
  import { createRunStore, createSessionStore } from "../sessions/store.ts";
30
30
  import { resolveBackend, trackerCliName } from "../tracker/factory.ts";
@@ -332,6 +332,13 @@ export interface CoordinatorSessionOptions {
332
332
  watchdog: boolean;
333
333
  monitor: boolean;
334
334
  profile?: string;
335
+ /**
336
+ * Runtime adapter override (e.g. "claude", "codex", "pi", "opencode",
337
+ * "gemini"). Takes precedence over config's per-capability and default
338
+ * runtime. When omitted, resolution falls back to
339
+ * runtime.capabilities[capability] → runtime.default → "claude".
340
+ */
341
+ runtime?: string;
335
342
  /** Override coordinator name (default: "coordinator"). */
336
343
  coordinatorName?: string;
337
344
  /** Generic persistent agent name override. Preferred over coordinatorName for new callers. */
@@ -386,6 +393,7 @@ export async function startCoordinatorSession(
386
393
  watchdog: watchdogFlag,
387
394
  monitor: monitorFlag,
388
395
  profile: profileFlag,
396
+ runtime: runtimeOverride,
389
397
  coordinatorName: coordinatorNameOpt,
390
398
  agentName: agentNameOpt,
391
399
  capability: capabilityOpt,
@@ -402,6 +410,16 @@ export async function startCoordinatorSession(
402
410
  const displayName = displayNameOpt ?? COORDINATOR_SPEC.displayName;
403
411
  const beaconBuilder = beaconBuilderOpt ?? buildCoordinatorBeacon;
404
412
 
413
+ // Fail fast on an unknown --runtime before doing any setup work, listing the
414
+ // valid adapters. getRuntime() would also reject it, but later and with a
415
+ // plain Error; this gives an immediate, typed, actionable message.
416
+ if (runtimeOverride && !getRuntimeNames().includes(runtimeOverride)) {
417
+ throw new ValidationError(
418
+ `Unknown runtime "${runtimeOverride}". Available: ${getRuntimeNames().join(", ")}`,
419
+ { field: "runtime", value: runtimeOverride },
420
+ );
421
+ }
422
+
405
423
  if (isRunningAsRoot()) {
406
424
  throw new AgentError(
407
425
  "Cannot spawn agents as root (UID 0). The claude CLI rejects --permission-mode bypassPermissions when run as root, causing the tmux session to die immediately. Run agentplate as a non-root user.",
@@ -478,7 +496,7 @@ export async function startCoordinatorSession(
478
496
  );
479
497
  const manifest = await manifestLoader.load();
480
498
  const resolvedModel = resolveModel(config, manifest, capability, "opus");
481
- const runtime = getRuntime(undefined, config, capability);
499
+ const runtime = getRuntime(runtimeOverride, config, capability);
482
500
 
483
501
  // Deploy hooks to the project root so the coordinator gets event logging,
484
502
  // mail check --inject, and activity tracking via the standard hook pipeline.
@@ -875,6 +893,7 @@ async function startPersistentAgent(
875
893
  watchdog: boolean;
876
894
  monitor: boolean;
877
895
  profile?: string;
896
+ runtime?: string;
878
897
  acceptExistingWatchdog?: boolean;
879
898
  },
880
899
  deps: CoordinatorDeps = {},
@@ -1645,7 +1664,7 @@ export function createPersistentAgentCommand(
1645
1664
 
1646
1665
  cmd
1647
1666
  .command("start")
1648
- .description(`Start the ${spec.commandName} (spawns Claude Code at project root)`)
1667
+ .description(`Start the ${spec.commandName} (spawns the configured runtime at project root)`)
1649
1668
  .option("--attach", "Always attach to tmux session after start")
1650
1669
  .option("--no-attach", "Never attach to tmux session after start")
1651
1670
  .option("--watchdog", `Auto-start watchdog daemon with ${spec.commandName}`)
@@ -1655,6 +1674,10 @@ export function createPersistentAgentCommand(
1655
1674
  )
1656
1675
  .option("--monitor", `Auto-start Tier 2 monitor agent with ${spec.commandName}`)
1657
1676
  .option("--profile <name>", "Trellis profile to apply to spawned agents")
1677
+ .option(
1678
+ "--runtime <name>",
1679
+ "Runtime adapter: claude | codex | pi | opencode | gemini (default: config or claude)",
1680
+ )
1658
1681
  .option("--json", "Output as JSON")
1659
1682
  .action(
1660
1683
  async (opts: {
@@ -1664,6 +1687,7 @@ export function createPersistentAgentCommand(
1664
1687
  monitor?: boolean;
1665
1688
  json?: boolean;
1666
1689
  profile?: string;
1690
+ runtime?: string;
1667
1691
  }) => {
1668
1692
  // opts.attach = true if --attach, false if --no-attach, undefined if neither
1669
1693
  const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
@@ -1676,6 +1700,7 @@ export function createPersistentAgentCommand(
1676
1700
  acceptExistingWatchdog: opts.acceptExistingWatchdog ?? false,
1677
1701
  monitor: opts.monitor ?? false,
1678
1702
  profile: opts.profile,
1703
+ runtime: opts.runtime,
1679
1704
  },
1680
1705
  deps,
1681
1706
  );
@@ -96,7 +96,11 @@ const SIBLING_TOOLS: SiblingTool[] = [
96
96
  * tool's entrypoint resolved inside this package. PATH-independent.
97
97
  */
98
98
  function toolArgv(tool: SiblingTool, ...cmd: string[]): string[] {
99
- const entryPath = new URL(tool.entry, import.meta.url).pathname;
99
+ // Resolve via import.meta.dir (a decoded filesystem path) rather than
100
+ // `new URL(...).pathname`, which percent-encodes — a path like
101
+ // `/Users/Jane Doe/...` would become `/Users/Jane%20Doe/...` and fail to
102
+ // spawn, surfacing as a spurious "loam/sprout/trellis unavailable" error.
103
+ const entryPath = join(import.meta.dir, tool.entry);
100
104
  return [process.execPath, entryPath, ...cmd];
101
105
  }
102
106
 
@@ -139,13 +139,15 @@ export function resolveUiDistPath(
139
139
  ): string {
140
140
  const projectDist = join(projectRoot, "ui", "dist");
141
141
  if (_exists(projectDist)) return projectDist;
142
- return new URL("../../ui/dist", import.meta.url).pathname;
142
+ // import.meta.dir is a decoded filesystem path; `new URL(...).pathname` would
143
+ // percent-encode spaces/special chars in the install path and 404 the SPA.
144
+ return join(import.meta.dir, "..", "..", "ui", "dist");
143
145
  }
144
146
 
145
147
  /** Read the package version once at module load to avoid circular imports with index.ts. */
146
148
  const _pkgVersion = (): string => {
147
149
  try {
148
- const raw = readFileSync(new URL("../../package.json", import.meta.url).pathname, "utf-8");
150
+ const raw = readFileSync(join(import.meta.dir, "..", "..", "package.json"), "utf-8");
149
151
  return (JSON.parse(raw) as { version: string }).version;
150
152
  } catch {
151
153
  return "unknown";
@@ -7,7 +7,7 @@ import { CursorRuntime } from "./cursor.ts";
7
7
  import { GeminiRuntime } from "./gemini.ts";
8
8
  import { OpenCodeRuntime } from "./opencode.ts";
9
9
  import { PiRuntime } from "./pi.ts";
10
- import { getRuntime } from "./registry.ts";
10
+ import { getRuntime, getRuntimeNames } from "./registry.ts";
11
11
 
12
12
  describe("getRuntime", () => {
13
13
  it("returns a ClaudeRuntime by default (no args)", () => {
@@ -194,3 +194,18 @@ describe("getRuntime", () => {
194
194
  });
195
195
  });
196
196
  });
197
+
198
+ describe("getRuntimeNames", () => {
199
+ it("lists every registered adapter, including the coordinator-supported set", () => {
200
+ const names = getRuntimeNames();
201
+ for (const expected of ["claude", "codex", "pi", "opencode", "gemini"]) {
202
+ expect(names).toContain(expected);
203
+ }
204
+ });
205
+
206
+ it("returns names that all resolve via getRuntime", () => {
207
+ for (const name of getRuntimeNames()) {
208
+ expect(getRuntime(name).id).toBe(name);
209
+ }
210
+ });
211
+ });
@@ -55,6 +55,18 @@ export function getAllRuntimes(): AgentRuntime[] {
55
55
  ];
56
56
  }
57
57
 
58
+ /**
59
+ * Names of all registered runtime adapters, in registration order.
60
+ *
61
+ * Used to validate a user-supplied `--runtime <name>` before resolution so the
62
+ * error can list the valid choices (see `ap coordinator start --runtime`).
63
+ *
64
+ * @returns Array of runtime names (e.g. "claude", "codex", "pi").
65
+ */
66
+ export function getRuntimeNames(): string[] {
67
+ return [...runtimes.keys()];
68
+ }
69
+
58
70
  /**
59
71
  * Resolve a runtime adapter by name.
60
72
  *
package/src/version.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Single source of truth for the package version, shared by every bundled bin
3
3
  * (ap/agentplate, lm/loam, sr, tl). Updated by scripts/version-bump.ts.
4
4
  */
5
- export const VERSION = "0.13.3";
5
+ export const VERSION = "0.14.0";