@orgloop/agentctl 1.0.1 → 1.2.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/README.md +145 -3
- package/dist/adapters/claude-code.js +10 -10
- package/dist/adapters/codex.d.ts +72 -0
- package/dist/adapters/codex.js +692 -0
- package/dist/adapters/openclaw.d.ts +60 -9
- package/dist/adapters/openclaw.js +195 -38
- package/dist/adapters/opencode.d.ts +143 -0
- package/dist/adapters/opencode.js +672 -0
- package/dist/adapters/pi-rust.d.ts +89 -0
- package/dist/adapters/pi-rust.js +743 -0
- package/dist/adapters/pi.d.ts +96 -0
- package/dist/adapters/pi.js +855 -0
- package/dist/cli.js +277 -59
- package/dist/core/types.d.ts +1 -0
- package/dist/daemon/server.js +34 -4
- package/dist/daemon/session-tracker.d.ts +20 -0
- package/dist/daemon/session-tracker.js +150 -4
- package/dist/daemon/state.d.ts +1 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +4 -0
- package/dist/launch-orchestrator.d.ts +60 -0
- package/dist/launch-orchestrator.js +198 -0
- package/dist/matrix-parser.d.ts +40 -0
- package/dist/matrix-parser.js +69 -0
- package/dist/utils/partial-read.d.ts +20 -0
- package/dist/utils/partial-read.js +66 -0
- package/dist/worktree.d.ts +22 -0
- package/dist/worktree.js +68 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Universal agent supervision interface. Monitor and control AI coding agents from a single CLI.
|
|
4
4
|
|
|
5
|
-
agentctl reads from native sources (Claude Code's `~/.claude/` directory, running processes) and provides a standard interface to list, inspect, stop, launch, and resume agent sessions. It never replicates state — it reads what's actually happening.
|
|
5
|
+
agentctl reads from native sources (Claude Code's `~/.claude/` directory, Pi's `~/.pi/` directory, running processes) and provides a standard interface to list, inspect, stop, launch, and resume agent sessions. It never replicates state — it reads what's actually happening.
|
|
6
6
|
|
|
7
7
|
## Layer Model
|
|
8
8
|
|
|
@@ -48,6 +48,9 @@ agentctl peek <session-id>
|
|
|
48
48
|
# Launch a new Claude Code session
|
|
49
49
|
agentctl launch -p "Read the spec and implement phase 2"
|
|
50
50
|
|
|
51
|
+
# Launch a new Pi session
|
|
52
|
+
agentctl launch pi -p "Refactor the auth module"
|
|
53
|
+
|
|
51
54
|
# Stop a session
|
|
52
55
|
agentctl stop <session-id>
|
|
53
56
|
|
|
@@ -57,13 +60,79 @@ agentctl resume <session-id> "fix the failing tests"
|
|
|
57
60
|
|
|
58
61
|
Session IDs support prefix matching — `agentctl peek abc123` matches any session starting with `abc123`.
|
|
59
62
|
|
|
63
|
+
### Parallel Multi-Adapter Launch
|
|
64
|
+
|
|
65
|
+
Launch the same prompt across multiple adapters (or the same adapter with different models). Each gets its own git worktree and runs in isolation:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Launch across 3 adapters
|
|
69
|
+
agentctl launch \
|
|
70
|
+
--adapter claude-code \
|
|
71
|
+
--adapter codex \
|
|
72
|
+
--adapter pi \
|
|
73
|
+
--cwd ~/code/mono \
|
|
74
|
+
-p "Implement the caching layer"
|
|
75
|
+
|
|
76
|
+
# Launched 3 sessions (group: g-a1b2c3):
|
|
77
|
+
# claude-code → ~/code/mono-try-g-a1b2c3-cc
|
|
78
|
+
# codex → ~/code/mono-try-g-a1b2c3-codex
|
|
79
|
+
# pi → ~/code/mono-try-g-a1b2c3-pi
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Same adapter with different models:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
agentctl launch \
|
|
86
|
+
--adapter claude-code --model claude-opus-4-6 \
|
|
87
|
+
--adapter claude-code --model claude-sonnet-4-5 \
|
|
88
|
+
--adapter codex \
|
|
89
|
+
--cwd ~/code/mono \
|
|
90
|
+
-p "Refactor the auth module"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Groups show up in `agentctl list` automatically:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
agentctl list
|
|
97
|
+
# ID Status Model Group CWD Prompt
|
|
98
|
+
# f3a1... running opus-4-6 g-x1y2z3 ~/mono-try-g-x1y2z3-cc-opus Refactor...
|
|
99
|
+
# 8b2c... running sonnet-4-5 g-x1y2z3 ~/mono-try-g-x1y2z3-cc-son Refactor...
|
|
100
|
+
# c4d3... done gpt-5.2-codex g-x1y2z3 ~/mono-try-g-x1y2z3-codex Refactor...
|
|
101
|
+
|
|
102
|
+
# Filter by group
|
|
103
|
+
agentctl list --group g-x1y2z3
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Matrix Files
|
|
107
|
+
|
|
108
|
+
For advanced sweep configurations, use a YAML matrix file:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
# matrix.yaml
|
|
112
|
+
prompt: "Implement the caching layer"
|
|
113
|
+
cwd: ~/code/mono
|
|
114
|
+
matrix:
|
|
115
|
+
- adapter: claude-code
|
|
116
|
+
model:
|
|
117
|
+
- claude-opus-4-6
|
|
118
|
+
- claude-sonnet-4-5
|
|
119
|
+
- adapter: codex
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
agentctl launch --matrix matrix.yaml
|
|
124
|
+
# Launches 3 sessions: claude-code×opus, claude-code×sonnet, codex
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Array values in `model` are expanded via cross-product.
|
|
128
|
+
|
|
60
129
|
## CLI Reference
|
|
61
130
|
|
|
62
131
|
### Session Management
|
|
63
132
|
|
|
64
133
|
```bash
|
|
65
134
|
agentctl list [options]
|
|
66
|
-
--adapter <name> Filter by adapter (claude-code, openclaw)
|
|
135
|
+
--adapter <name> Filter by adapter (claude-code, codex, opencode, pi, pi-rust, openclaw)
|
|
67
136
|
--status <status> Filter by status (running|stopped|idle|error)
|
|
68
137
|
-a, --all Include stopped sessions (last 7 days)
|
|
69
138
|
--json Output as JSON
|
|
@@ -81,6 +150,9 @@ agentctl launch [adapter] [options]
|
|
|
81
150
|
--spec <path> Spec file path
|
|
82
151
|
--cwd <dir> Working directory
|
|
83
152
|
--model <model> Model to use (e.g. sonnet, opus)
|
|
153
|
+
--adapter <name> Adapter to launch (repeatable for parallel launch)
|
|
154
|
+
--matrix <file> YAML matrix file for advanced sweep launch
|
|
155
|
+
--group <id> Filter by launch group (for list command)
|
|
84
156
|
--force Override directory locks
|
|
85
157
|
|
|
86
158
|
agentctl stop <id> [options]
|
|
@@ -114,6 +186,29 @@ agentctl locks [options]
|
|
|
114
186
|
--json Output as JSON
|
|
115
187
|
```
|
|
116
188
|
|
|
189
|
+
### Worktree Management
|
|
190
|
+
|
|
191
|
+
Manage git worktrees created by parallel launches or `--worktree` flag:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
agentctl worktree list <repo>
|
|
195
|
+
--json Output as JSON
|
|
196
|
+
|
|
197
|
+
agentctl worktree clean <path> [options]
|
|
198
|
+
--repo <path> Main repo path (auto-detected if omitted)
|
|
199
|
+
--delete-branch Also delete the worktree's branch
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# List all worktrees for a repo
|
|
206
|
+
agentctl worktree list ~/code/mono
|
|
207
|
+
|
|
208
|
+
# Clean up a worktree and its branch
|
|
209
|
+
agentctl worktree clean ~/code/mono-try-g-a1b2c3-cc --delete-branch
|
|
210
|
+
```
|
|
211
|
+
|
|
117
212
|
### Lifecycle Hooks
|
|
118
213
|
|
|
119
214
|
Hooks are shell commands that run at specific points in a session's lifecycle. Pass them as flags to `launch` or `merge`:
|
|
@@ -146,6 +241,8 @@ Hook scripts receive context via environment variables:
|
|
|
146
241
|
| `AGENTCTL_ADAPTER` | Adapter name (e.g. `claude-code`) |
|
|
147
242
|
| `AGENTCTL_BRANCH` | Git branch (when using `--worktree`) |
|
|
148
243
|
| `AGENTCTL_EXIT_CODE` | Process exit code (in `--on-complete`) |
|
|
244
|
+
| `AGENTCTL_GROUP` | Launch group ID (in parallel launches) |
|
|
245
|
+
| `AGENTCTL_MODEL` | Model name (when specified) |
|
|
149
246
|
|
|
150
247
|
Hooks run with a 60-second timeout. If a hook fails, its stderr is printed but execution continues.
|
|
151
248
|
|
|
@@ -268,7 +365,7 @@ scrape_configs:
|
|
|
268
365
|
|
|
269
366
|
agentctl is structured in three layers: the **CLI** parses commands and formats output, the **daemon** provides persistent state (session tracking, directory locks, fuse timers, Prometheus metrics), and **adapters** bridge to specific agent runtimes. The CLI communicates with the daemon over a Unix socket at `~/.agentctl/agentctl.sock`.
|
|
270
367
|
|
|
271
|
-
All session state is derived from native sources — agentctl never maintains its own session registry. The Claude Code adapter reads `~/.claude/projects/` and cross-references running processes; other adapters connect to their respective APIs. This means agentctl always reflects ground truth.
|
|
368
|
+
All session state is derived from native sources — agentctl never maintains its own session registry. The Claude Code adapter reads `~/.claude/projects/` and cross-references running processes; the Pi adapter reads `~/.pi/agent/sessions/` JSONL files; other adapters connect to their respective APIs. This means agentctl always reflects ground truth.
|
|
272
369
|
|
|
273
370
|
## Adapters
|
|
274
371
|
|
|
@@ -278,6 +375,42 @@ agentctl uses an adapter model to support different agent runtimes.
|
|
|
278
375
|
|
|
279
376
|
Reads session data from `~/.claude/projects/` and cross-references with running `claude` processes. Detects PID recycling via process start time verification. Tracks detached processes that survive wrapper exit.
|
|
280
377
|
|
|
378
|
+
### Codex CLI
|
|
379
|
+
|
|
380
|
+
Reads session data from `~/.codex/sessions/` and cross-references with running `codex` processes. Supports `codex exec` non-interactive mode for launching headless sessions. Detects PID recycling via process start time verification, same as the Claude Code adapter.
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
# Launch a Codex session
|
|
384
|
+
agentctl launch codex -p "implement the feature"
|
|
385
|
+
|
|
386
|
+
# Launch with specific model
|
|
387
|
+
agentctl launch codex -p "fix the bug" --model gpt-5.2-codex
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### OpenCode
|
|
391
|
+
|
|
392
|
+
Reads session data from `~/.local/share/opencode/storage/` and cross-references with running `opencode` processes. Supports headless execution via `opencode run`.
|
|
393
|
+
|
|
394
|
+
OpenCode stores sessions as individual JSON files organized by project hash (SHA1 of the working directory path):
|
|
395
|
+
|
|
396
|
+
- **Session files**: `storage/session/<projectHash>/<sessionId>.json` — session metadata (title, directory, timestamps, summary)
|
|
397
|
+
- **Message files**: `storage/message/<sessionId>/<messageId>.json` — individual messages with token counts, cost, and model info
|
|
398
|
+
- **Part files**: `storage/part/<messageId>/` — message content parts
|
|
399
|
+
|
|
400
|
+
The adapter detects PID recycling via process start time verification, tracks detached processes that survive wrapper exit, and supports prefix matching for session IDs.
|
|
401
|
+
|
|
402
|
+
Launch sessions with `agentctl launch opencode -p "your prompt"`.
|
|
403
|
+
|
|
404
|
+
### Pi
|
|
405
|
+
|
|
406
|
+
Reads session data from `~/.pi/agent/sessions/` and cross-references with running `pi` processes. Pi stores sessions as JSONL files organized by cwd slug — each file starts with a `type:'session'` header containing metadata (id, cwd, provider, modelId, thinkingLevel, version).
|
|
407
|
+
|
|
408
|
+
Detects PID recycling via process start time verification. Tracks detached processes that survive wrapper exit. Persists session metadata in `~/.pi/agentctl/sessions/` for status checks after the launching wrapper exits.
|
|
409
|
+
|
|
410
|
+
Launch uses Pi's print mode (`pi -p "prompt"`) for headless execution. Resume launches a new Pi session in the same working directory since Pi doesn't have a native `--continue` flag.
|
|
411
|
+
|
|
412
|
+
Requires the `pi` binary (npm: `@mariozechner/pi-coding-agent`) to be available on PATH.
|
|
413
|
+
|
|
281
414
|
### OpenClaw
|
|
282
415
|
|
|
283
416
|
Connects to the OpenClaw gateway via WebSocket RPC. Read-only — sessions are managed through the gateway.
|
|
@@ -339,8 +472,17 @@ npm run lint # biome check
|
|
|
339
472
|
src/
|
|
340
473
|
cli.ts # CLI entry point (commander)
|
|
341
474
|
core/types.ts # Core interfaces
|
|
475
|
+
launch-orchestrator.ts # Parallel multi-adapter launch orchestration
|
|
476
|
+
matrix-parser.ts # YAML matrix file parser + cross-product expansion
|
|
477
|
+
worktree.ts # Git worktree create/list/clean
|
|
478
|
+
hooks.ts # Lifecycle hook runner
|
|
479
|
+
merge.ts # Git commit/push/PR for sessions
|
|
342
480
|
adapters/claude-code.ts # Claude Code adapter
|
|
481
|
+
adapters/codex.ts # Codex CLI adapter
|
|
343
482
|
adapters/openclaw.ts # OpenClaw gateway adapter
|
|
483
|
+
adapters/opencode.ts # OpenCode adapter
|
|
484
|
+
adapters/pi.ts # Pi coding agent adapter
|
|
485
|
+
adapters/pi-rust.ts # Pi Rust adapter
|
|
344
486
|
daemon/server.ts # Daemon: Unix socket server + HTTP metrics
|
|
345
487
|
daemon/session-tracker.ts # Session lifecycle tracking
|
|
346
488
|
daemon/lock-manager.ts # Directory locks
|
|
@@ -5,6 +5,7 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { readHead, readTail } from "../utils/partial-read.js";
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
10
11
|
// Default: only show stopped sessions from the last 7 days
|
|
@@ -129,10 +130,11 @@ export class ClaudeCodeAdapter {
|
|
|
129
130
|
await fs.mkdir(this.sessionsMetaDir, { recursive: true });
|
|
130
131
|
const logPath = path.join(this.sessionsMetaDir, `launch-${Date.now()}.log`);
|
|
131
132
|
const logFd = await fs.open(logPath, "w");
|
|
133
|
+
// Capture stderr to the same log file for debugging launch failures
|
|
132
134
|
const child = spawn("claude", args, {
|
|
133
135
|
cwd,
|
|
134
136
|
env,
|
|
135
|
-
stdio: ["ignore", logFd.fd,
|
|
137
|
+
stdio: ["ignore", logFd.fd, logFd.fd],
|
|
136
138
|
detached: true,
|
|
137
139
|
});
|
|
138
140
|
// Fully detach: child runs in its own process group.
|
|
@@ -148,7 +150,7 @@ export class ClaudeCodeAdapter {
|
|
|
148
150
|
if (pid) {
|
|
149
151
|
resolvedSessionId = await this.pollForSessionId(logPath, pid, 5000);
|
|
150
152
|
}
|
|
151
|
-
const sessionId = resolvedSessionId || crypto.randomUUID();
|
|
153
|
+
const sessionId = resolvedSessionId || (pid ? `pending-${pid}` : crypto.randomUUID());
|
|
152
154
|
// Persist session metadata so status checks work after wrapper exits
|
|
153
155
|
if (pid) {
|
|
154
156
|
await this.writeSessionMeta({
|
|
@@ -348,12 +350,12 @@ export class ClaudeCodeAdapter {
|
|
|
348
350
|
catch {
|
|
349
351
|
continue;
|
|
350
352
|
}
|
|
351
|
-
// Read first few lines for prompt and cwd
|
|
353
|
+
// Read first few lines for prompt and cwd (only first 8KB, not entire file)
|
|
352
354
|
let firstPrompt = "";
|
|
353
355
|
let sessionCwd = "";
|
|
354
356
|
try {
|
|
355
|
-
const
|
|
356
|
-
for (const l of
|
|
357
|
+
const headLines = await readHead(fullPath, 20, 8192);
|
|
358
|
+
for (const l of headLines) {
|
|
357
359
|
try {
|
|
358
360
|
const msg = JSON.parse(l);
|
|
359
361
|
if (msg.cwd && !sessionCwd)
|
|
@@ -540,13 +542,11 @@ export class ClaudeCodeAdapter {
|
|
|
540
542
|
}
|
|
541
543
|
async parseSessionTail(jsonlPath) {
|
|
542
544
|
try {
|
|
543
|
-
const content = await fs.readFile(jsonlPath, "utf-8");
|
|
544
|
-
const lines = content.trim().split("\n");
|
|
545
545
|
let model;
|
|
546
546
|
let totalIn = 0;
|
|
547
547
|
let totalOut = 0;
|
|
548
|
-
// Read
|
|
549
|
-
const tail =
|
|
548
|
+
// Read only the last 64KB for the tail (not entire file)
|
|
549
|
+
const tail = await readTail(jsonlPath, 100, 65536);
|
|
550
550
|
for (const line of tail) {
|
|
551
551
|
try {
|
|
552
552
|
const msg = JSON.parse(line);
|
|
@@ -565,7 +565,7 @@ export class ClaudeCodeAdapter {
|
|
|
565
565
|
}
|
|
566
566
|
// Also scan first few lines for model if we didn't find it
|
|
567
567
|
if (!model) {
|
|
568
|
-
const head =
|
|
568
|
+
const head = await readHead(jsonlPath, 20, 8192);
|
|
569
569
|
for (const line of head) {
|
|
570
570
|
try {
|
|
571
571
|
const msg = JSON.parse(line);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
|
+
export interface CodexPidInfo {
|
|
3
|
+
pid: number;
|
|
4
|
+
cwd: string;
|
|
5
|
+
args: string;
|
|
6
|
+
startTime?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Metadata persisted by launch() so status checks survive wrapper exit */
|
|
9
|
+
export interface CodexSessionMeta {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
pid: number;
|
|
12
|
+
startTime?: string;
|
|
13
|
+
wrapperPid?: number;
|
|
14
|
+
cwd: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
prompt?: string;
|
|
17
|
+
launchedAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CodexAdapterOpts {
|
|
20
|
+
codexDir?: string;
|
|
21
|
+
sessionsMetaDir?: string;
|
|
22
|
+
getPids?: () => Promise<Map<number, CodexPidInfo>>;
|
|
23
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Codex CLI adapter — reads session data from ~/.codex/sessions/
|
|
27
|
+
* and cross-references with running PIDs.
|
|
28
|
+
*/
|
|
29
|
+
export declare class CodexAdapter implements AgentAdapter {
|
|
30
|
+
readonly id = "codex";
|
|
31
|
+
private readonly codexDir;
|
|
32
|
+
private readonly sessionsDir;
|
|
33
|
+
private readonly sessionsMetaDir;
|
|
34
|
+
private readonly getPids;
|
|
35
|
+
private readonly isProcessAlive;
|
|
36
|
+
constructor(opts?: CodexAdapterOpts);
|
|
37
|
+
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
38
|
+
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
39
|
+
status(sessionId: string): Promise<AgentSession>;
|
|
40
|
+
launch(opts: LaunchOpts): Promise<AgentSession>;
|
|
41
|
+
/**
|
|
42
|
+
* Poll the launch log file for up to `timeoutMs` to extract the session/thread ID.
|
|
43
|
+
* Codex outputs {"type":"thread.started","thread_id":"..."} early in its JSONL stream.
|
|
44
|
+
*/
|
|
45
|
+
private pollForSessionId;
|
|
46
|
+
stop(sessionId: string, opts?: StopOpts): Promise<void>;
|
|
47
|
+
resume(sessionId: string, message: string): Promise<void>;
|
|
48
|
+
events(): AsyncIterable<LifecycleEvent>;
|
|
49
|
+
/**
|
|
50
|
+
* Discover all Codex sessions by scanning ~/.codex/sessions/ recursively.
|
|
51
|
+
* Sessions are stored as: sessions/YYYY/MM/DD/rollout-<datetime>-<session-id>.jsonl
|
|
52
|
+
*/
|
|
53
|
+
private discoverSessions;
|
|
54
|
+
/** Recursively find all .jsonl files under a directory */
|
|
55
|
+
private findJsonlFiles;
|
|
56
|
+
/** Parse a Codex session JSONL file to extract session info */
|
|
57
|
+
private parseSessionFile;
|
|
58
|
+
private buildSession;
|
|
59
|
+
private isSessionRunning;
|
|
60
|
+
private processStartedAfterSession;
|
|
61
|
+
private findMatchingPid;
|
|
62
|
+
private findSession;
|
|
63
|
+
private findPidForSession;
|
|
64
|
+
writeSessionMeta(meta: Omit<CodexSessionMeta, "startTime">): Promise<void>;
|
|
65
|
+
readSessionMeta(sessionId: string): Promise<CodexSessionMeta | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Synchronous-style read of session metadata (reads from cache/disk).
|
|
68
|
+
* Used by isSessionRunning which is called in a tight loop.
|
|
69
|
+
* Falls back to null if not found.
|
|
70
|
+
*/
|
|
71
|
+
private readSessionMetaSync;
|
|
72
|
+
}
|