@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 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, "ignore"],
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 content = await fs.readFile(fullPath, "utf-8");
356
- for (const l of content.split("\n").slice(0, 20)) {
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 from the end for efficiency last 100 lines
549
- const tail = lines.slice(-100);
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 = lines.slice(0, 20);
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
+ }