@ag-eco/agentplate-cli 0.14.0 → 0.14.1
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.14.
|
|
3
|
+
"version": "0.14.1",
|
|
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",
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
coordinatorCommand,
|
|
29
29
|
createCoordinatorCommand,
|
|
30
30
|
resolveAttach,
|
|
31
|
+
resolveHeadless,
|
|
31
32
|
startCoordinatorSession,
|
|
32
33
|
} from "./coordinator.ts";
|
|
33
34
|
import {
|
|
@@ -710,6 +711,40 @@ describe("startCoordinator", () => {
|
|
|
710
711
|
).rejects.toThrow(ValidationError);
|
|
711
712
|
});
|
|
712
713
|
|
|
714
|
+
test("--headless uses the direct-spawn path instead of tmux", async () => {
|
|
715
|
+
const { deps, calls } = makeDeps();
|
|
716
|
+
const spawnCalls: string[][] = [];
|
|
717
|
+
deps._spawnHeadless = async (argv) => {
|
|
718
|
+
spawnCalls.push(argv);
|
|
719
|
+
return { pid: 4321, stdin: { write: () => 0 }, stdout: null };
|
|
720
|
+
};
|
|
721
|
+
const originalSleep = Bun.sleep;
|
|
722
|
+
Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
|
|
723
|
+
|
|
724
|
+
try {
|
|
725
|
+
await captureStdout(() =>
|
|
726
|
+
coordinatorCommand(["start", "--no-attach", "--json", "--headless"], deps),
|
|
727
|
+
);
|
|
728
|
+
} finally {
|
|
729
|
+
Bun.sleep = originalSleep;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Headless path: the runtime is spawned directly; tmux is never touched.
|
|
733
|
+
expect(spawnCalls).toHaveLength(1);
|
|
734
|
+
expect(calls.createSession).toHaveLength(0);
|
|
735
|
+
// Session is recorded with an empty tmuxSession and the direct-spawn pid.
|
|
736
|
+
const sessions = loadSessionsFromDb();
|
|
737
|
+
expect(sessions[0]?.tmuxSession).toBe("");
|
|
738
|
+
expect(sessions[0]?.pid).toBe(4321);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test("--headless with a tmux-only runtime (pi) is rejected with ValidationError", async () => {
|
|
742
|
+
const { deps } = makeDeps();
|
|
743
|
+
await expect(
|
|
744
|
+
coordinatorCommand(["start", "--no-attach", "--headless", "--runtime", "pi"], deps),
|
|
745
|
+
).rejects.toThrow(ValidationError);
|
|
746
|
+
});
|
|
747
|
+
|
|
713
748
|
test("--json outputs JSON with expected fields", async () => {
|
|
714
749
|
const { deps } = makeDeps();
|
|
715
750
|
const originalSleep = Bun.sleep;
|
|
@@ -1451,6 +1486,24 @@ describe("resolveAttach", () => {
|
|
|
1451
1486
|
});
|
|
1452
1487
|
});
|
|
1453
1488
|
|
|
1489
|
+
describe("resolveHeadless", () => {
|
|
1490
|
+
test("explicit flag wins on every platform", () => {
|
|
1491
|
+
expect(resolveHeadless(true, "darwin")).toBe(true);
|
|
1492
|
+
expect(resolveHeadless(true, "win32")).toBe(true);
|
|
1493
|
+
expect(resolveHeadless(false, "win32")).toBe(false);
|
|
1494
|
+
expect(resolveHeadless(false, "linux")).toBe(false);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
test("defaults to headless on native Windows when unset", () => {
|
|
1498
|
+
expect(resolveHeadless(undefined, "win32")).toBe(true);
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
test("defaults to tmux on non-Windows platforms when unset", () => {
|
|
1502
|
+
expect(resolveHeadless(undefined, "darwin")).toBe(false);
|
|
1503
|
+
expect(resolveHeadless(undefined, "linux")).toBe(false);
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1454
1507
|
describe("watchdog integration", () => {
|
|
1455
1508
|
describe("startCoordinator with --watchdog", () => {
|
|
1456
1509
|
test("calls watchdog.start() when --watchdog flag is present", async () => {
|
|
@@ -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, getRuntimeNames } from "../runtimes/registry.ts";
|
|
27
|
+
import { getHeadlessRuntimeNames, 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";
|
|
@@ -322,6 +322,20 @@ export function resolveAttach(args: string[], isTTY: boolean): boolean {
|
|
|
322
322
|
return isTTY;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Resolve whether to spawn the coordinator headless (no tmux).
|
|
327
|
+
*
|
|
328
|
+
* An explicit flag wins (`--headless` → true, `--no-headless` → false). When
|
|
329
|
+
* unspecified, default to headless on native Windows — where tmux does not
|
|
330
|
+
* exist — and to the tmux path everywhere else.
|
|
331
|
+
*
|
|
332
|
+
* @param flag - Explicit flag value (true | false) or undefined when unset.
|
|
333
|
+
* @param platform - process.platform of the host running `ap`.
|
|
334
|
+
*/
|
|
335
|
+
export function resolveHeadless(flag: boolean | undefined, platform: NodeJS.Platform): boolean {
|
|
336
|
+
return flag ?? platform === "win32";
|
|
337
|
+
}
|
|
338
|
+
|
|
325
339
|
/**
|
|
326
340
|
* Options for the reusable coordinator session startup core.
|
|
327
341
|
* Used by startCoordinatorSession() and consumed by commands like ap discover.
|
|
@@ -420,6 +434,12 @@ export async function startCoordinatorSession(
|
|
|
420
434
|
);
|
|
421
435
|
}
|
|
422
436
|
|
|
437
|
+
// Resolve the effective spawn mode. An explicit --headless/--no-headless wins;
|
|
438
|
+
// otherwise default to headless on native Windows (where tmux is unavailable)
|
|
439
|
+
// and tmux everywhere else. Programmatic callers that pass an explicit boolean
|
|
440
|
+
// (e.g. the web UI's headless: true) are unaffected.
|
|
441
|
+
const useHeadless = resolveHeadless(headlessFlag, process.platform);
|
|
442
|
+
|
|
423
443
|
if (isRunningAsRoot()) {
|
|
424
444
|
throw new AgentError(
|
|
425
445
|
"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.",
|
|
@@ -528,10 +548,16 @@ export async function startCoordinatorSession(
|
|
|
528
548
|
// Headless start path: bypass tmux entirely and spawn the coordinator
|
|
529
549
|
// process directly via runtime.buildDirectSpawn(). Same hooks, identity,
|
|
530
550
|
// and run-tracking as the tmux path — only the spawn mechanism differs.
|
|
531
|
-
if (
|
|
551
|
+
if (useHeadless) {
|
|
532
552
|
if (!runtime.buildDirectSpawn) {
|
|
553
|
+
const headlessCapable = getHeadlessRuntimeNames().join(", ");
|
|
554
|
+
const winHint =
|
|
555
|
+
process.platform === "win32"
|
|
556
|
+
? ` On native Windows, tmux is unavailable, so tmux-only runtimes like "${runtime.id}" cannot run as a coordinator — use --runtime claude, or run agentplate under WSL2.`
|
|
557
|
+
: "";
|
|
533
558
|
throw new ValidationError(
|
|
534
|
-
`
|
|
559
|
+
`Runtime "${runtime.id}" cannot run a headless coordinator (no direct-spawn support). ` +
|
|
560
|
+
`Headless-capable runtimes: ${headlessCapable}.${winHint}`,
|
|
535
561
|
{ field: "runtime", value: runtime.id },
|
|
536
562
|
);
|
|
537
563
|
}
|
|
@@ -894,6 +920,7 @@ async function startPersistentAgent(
|
|
|
894
920
|
monitor: boolean;
|
|
895
921
|
profile?: string;
|
|
896
922
|
runtime?: string;
|
|
923
|
+
headless?: boolean;
|
|
897
924
|
acceptExistingWatchdog?: boolean;
|
|
898
925
|
},
|
|
899
926
|
deps: CoordinatorDeps = {},
|
|
@@ -1678,6 +1705,11 @@ export function createPersistentAgentCommand(
|
|
|
1678
1705
|
"--runtime <name>",
|
|
1679
1706
|
"Runtime adapter: claude | codex | pi | opencode | gemini (default: config or claude)",
|
|
1680
1707
|
)
|
|
1708
|
+
.option(
|
|
1709
|
+
"--headless",
|
|
1710
|
+
"Spawn without tmux (direct subprocess). Required on native Windows; runtime must be headless-capable (claude)",
|
|
1711
|
+
)
|
|
1712
|
+
.option("--no-headless", "Force the tmux spawn path (overrides the Windows headless default)")
|
|
1681
1713
|
.option("--json", "Output as JSON")
|
|
1682
1714
|
.action(
|
|
1683
1715
|
async (opts: {
|
|
@@ -1688,6 +1720,7 @@ export function createPersistentAgentCommand(
|
|
|
1688
1720
|
json?: boolean;
|
|
1689
1721
|
profile?: string;
|
|
1690
1722
|
runtime?: string;
|
|
1723
|
+
headless?: boolean;
|
|
1691
1724
|
}) => {
|
|
1692
1725
|
// opts.attach = true if --attach, false if --no-attach, undefined if neither
|
|
1693
1726
|
const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
|
|
@@ -1701,6 +1734,8 @@ export function createPersistentAgentCommand(
|
|
|
1701
1734
|
monitor: opts.monitor ?? false,
|
|
1702
1735
|
profile: opts.profile,
|
|
1703
1736
|
runtime: opts.runtime,
|
|
1737
|
+
// true (--headless) | false (--no-headless) | undefined (platform default)
|
|
1738
|
+
headless: opts.headless,
|
|
1704
1739
|
},
|
|
1705
1740
|
deps,
|
|
1706
1741
|
);
|
|
@@ -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, getRuntimeNames } from "./registry.ts";
|
|
10
|
+
import { getHeadlessRuntimeNames, getRuntime, getRuntimeNames } from "./registry.ts";
|
|
11
11
|
|
|
12
12
|
describe("getRuntime", () => {
|
|
13
13
|
it("returns a ClaudeRuntime by default (no args)", () => {
|
|
@@ -209,3 +209,22 @@ describe("getRuntimeNames", () => {
|
|
|
209
209
|
}
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
|
+
|
|
213
|
+
describe("getHeadlessRuntimeNames", () => {
|
|
214
|
+
it("includes claude (buildDirectSpawn) and sapling (static headless)", () => {
|
|
215
|
+
const names = getHeadlessRuntimeNames();
|
|
216
|
+
expect(names).toContain("claude");
|
|
217
|
+
expect(names).toContain("sapling");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("excludes tmux-only runtimes like pi", () => {
|
|
221
|
+
expect(getHeadlessRuntimeNames()).not.toContain("pi");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("every headless name actually resolves to a headless-capable runtime", () => {
|
|
225
|
+
for (const name of getHeadlessRuntimeNames()) {
|
|
226
|
+
const rt = getRuntime(name);
|
|
227
|
+
expect(typeof rt.buildDirectSpawn === "function" || rt.headless === true).toBe(true);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
package/src/runtimes/registry.ts
CHANGED
|
@@ -67,6 +67,21 @@ export function getRuntimeNames(): string[] {
|
|
|
67
67
|
return [...runtimes.keys()];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Names of runtimes that can spawn an agent without tmux — i.e. those that
|
|
72
|
+
* implement `buildDirectSpawn()` or statically declare `headless = true`.
|
|
73
|
+
*
|
|
74
|
+
* These are the only runtimes usable for a headless coordinator (the no-tmux
|
|
75
|
+
* path), which matters on native Windows where tmux is unavailable.
|
|
76
|
+
*
|
|
77
|
+
* @returns Array of headless-capable runtime names (e.g. "claude", "sapling").
|
|
78
|
+
*/
|
|
79
|
+
export function getHeadlessRuntimeNames(): string[] {
|
|
80
|
+
return getAllRuntimes()
|
|
81
|
+
.filter((r) => typeof r.buildDirectSpawn === "function" || r.headless === true)
|
|
82
|
+
.map((r) => r.id);
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
/**
|
|
71
86
|
* Resolve a runtime adapter by name.
|
|
72
87
|
*
|
package/src/version.ts
CHANGED
package/src/worktree/tmux.ts
CHANGED
|
@@ -598,10 +598,24 @@ export async function waitForTuiReady(
|
|
|
598
598
|
* Throws AgentError with a clear message if tmux is not available.
|
|
599
599
|
*/
|
|
600
600
|
export async function ensureTmuxAvailable(): Promise<void> {
|
|
601
|
-
|
|
602
|
-
|
|
601
|
+
// Bun.spawn throws (rather than returning a non-zero exit) when the tmux
|
|
602
|
+
// binary isn't on PATH — e.g. native Windows, where tmux does not exist. Catch
|
|
603
|
+
// that so the operator gets actionable guidance instead of a raw
|
|
604
|
+
// "Executable not found in $PATH" stack.
|
|
605
|
+
let available = false;
|
|
606
|
+
try {
|
|
607
|
+
const { exitCode } = await runCommand(["tmux", "-V"]);
|
|
608
|
+
available = exitCode === 0;
|
|
609
|
+
} catch {
|
|
610
|
+
available = false;
|
|
611
|
+
}
|
|
612
|
+
if (!available) {
|
|
613
|
+
const winHint =
|
|
614
|
+
process.platform === "win32"
|
|
615
|
+
? " On native Windows, tmux is unavailable — start the coordinator with --headless (claude runtime), or run agentplate under WSL2."
|
|
616
|
+
: "";
|
|
603
617
|
throw new AgentError(
|
|
604
|
-
|
|
618
|
+
`tmux is not installed or not on PATH. Install tmux to use agentplate agent orchestration.${winHint}`,
|
|
605
619
|
);
|
|
606
620
|
}
|
|
607
621
|
}
|