@os-eco/overstory-cli 0.8.0 → 0.8.3
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 +3 -1
- package/agents/builder.md +2 -2
- package/agents/lead.md +2 -2
- package/agents/merger.md +2 -2
- package/agents/orchestrator.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +2 -2
- package/agents/supervisor.md +3 -3
- package/package.json +1 -1
- package/src/agents/overlay.test.ts +42 -0
- package/src/agents/overlay.ts +1 -0
- package/src/commands/dashboard.test.ts +86 -0
- package/src/commands/dashboard.ts +8 -4
- package/src/commands/feed.test.ts +8 -0
- package/src/commands/inspect.test.ts +156 -1
- package/src/commands/inspect.ts +19 -4
- package/src/commands/replay.test.ts +8 -0
- package/src/commands/sling.test.ts +34 -10
- package/src/commands/sling.ts +249 -136
- package/src/commands/status.test.ts +77 -0
- package/src/commands/status.ts +6 -3
- package/src/commands/stop.test.ts +186 -4
- package/src/commands/stop.ts +46 -14
- package/src/commands/trace.test.ts +8 -0
- package/src/config.test.ts +63 -0
- package/src/config.ts +29 -5
- package/src/index.ts +2 -2
- package/src/logging/theme.ts +4 -0
- package/src/runtimes/codex.test.ts +22 -8
- package/src/runtimes/codex.ts +21 -16
- package/src/runtimes/connections.test.ts +74 -0
- package/src/runtimes/connections.ts +34 -0
- package/src/runtimes/registry.test.ts +1 -1
- package/src/runtimes/registry.ts +2 -0
- package/src/runtimes/sapling.test.ts +1237 -0
- package/src/runtimes/sapling.ts +698 -0
- package/src/runtimes/types.ts +45 -0
- package/src/types.ts +7 -1
- package/src/watchdog/daemon.ts +34 -0
- package/src/watchdog/health.test.ts +102 -0
- package/src/watchdog/health.ts +140 -69
- package/src/worktree/process.test.ts +101 -0
- package/src/worktree/process.ts +111 -0
- package/src/worktree/tmux.ts +5 -0
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Requires [Bun](https://bun.sh) v1.0+, git, and tmux. At least one supported agen
|
|
|
19
19
|
- [GitHub Copilot](https://github.com/features/copilot) (`copilot` CLI)
|
|
20
20
|
- [Codex](https://github.com/openai/codex) (`codex` CLI)
|
|
21
21
|
- [Gemini CLI](https://github.com/google-gemini/gemini-cli) (`gemini` CLI)
|
|
22
|
+
- [Sapling](https://github.com/nichochar/sapling) (`sp` CLI)
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
bun install -g @os-eco/overstory-cli
|
|
@@ -181,6 +182,7 @@ Overstory is runtime-agnostic. The `AgentRuntime` interface (`src/runtimes/types
|
|
|
181
182
|
| Copilot | `copilot` | (none — `--allow-all-tools`) | Active development |
|
|
182
183
|
| Codex | `codex` | OS-level sandbox (Seatbelt/Landlock) | Active development |
|
|
183
184
|
| Gemini | `gemini` | `--sandbox` flag | Active development |
|
|
185
|
+
| Sapling | `sp` | `.sapling/guards.json` | Active development |
|
|
184
186
|
|
|
185
187
|
## How It Works
|
|
186
188
|
|
|
@@ -280,7 +282,7 @@ overstory/
|
|
|
280
282
|
metrics/ SQLite metrics + pricing + transcript parsing
|
|
281
283
|
doctor/ Health check modules (11 checks)
|
|
282
284
|
insights/ Session insight analyzer for auto-expertise
|
|
283
|
-
runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot, Codex, Gemini)
|
|
285
|
+
runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot, Codex, Gemini, Sapling)
|
|
284
286
|
tracker/ Pluggable task tracker (beads + seeds backends)
|
|
285
287
|
mulch/ mulch client (programmatic API + CLI wrapper)
|
|
286
288
|
e2e/ End-to-end lifecycle tests
|
package/agents/builder.md
CHANGED
|
@@ -20,7 +20,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
20
20
|
|
|
21
21
|
## overlay
|
|
22
22
|
|
|
23
|
-
Your task-specific context (task ID, file scope, spec path, branch name, parent agent) is in
|
|
23
|
+
Your task-specific context (task ID, file scope, spec path, branch name, parent agent) is in `{{INSTRUCTION_PATH}}` in your worktree. That file is generated by `ov sling` and tells you WHAT to work on. This file tells you HOW to work.
|
|
24
24
|
|
|
25
25
|
## constraints
|
|
26
26
|
|
|
@@ -108,7 +108,7 @@ You are an implementation specialist. Given a spec and a set of files you own, y
|
|
|
108
108
|
|
|
109
109
|
## workflow
|
|
110
110
|
|
|
111
|
-
1. **Read your overlay** at
|
|
111
|
+
1. **Read your overlay** at `{{INSTRUCTION_PATH}}` in your worktree. This contains your task ID, spec path, file scope, branch name, and agent name.
|
|
112
112
|
2. **Read the task spec** at the path specified in your overlay. Understand what needs to be built.
|
|
113
113
|
3. **Load expertise** via `ml prime [domain]` for domains listed in your overlay. Apply existing patterns and conventions.
|
|
114
114
|
4. **Implement the changes:**
|
package/agents/lead.md
CHANGED
|
@@ -43,7 +43,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
43
43
|
|
|
44
44
|
## overlay
|
|
45
45
|
|
|
46
|
-
Your task-specific context (task ID, spec path, hierarchy depth, agent name, whether you can spawn) is in
|
|
46
|
+
Your task-specific context (task ID, spec path, hierarchy depth, agent name, whether you can spawn) is in `{{INSTRUCTION_PATH}}` in your worktree. That file is generated by `ov sling` and tells you WHAT to coordinate. This file tells you HOW to coordinate.
|
|
47
47
|
|
|
48
48
|
## constraints
|
|
49
49
|
|
|
@@ -160,7 +160,7 @@ Action: Full Scout → Build → Verify pipeline. Spawn scouts for exploration,
|
|
|
160
160
|
|
|
161
161
|
Delegate exploration to scouts so you can focus on decomposition and planning.
|
|
162
162
|
|
|
163
|
-
1. **Read your overlay** at
|
|
163
|
+
1. **Read your overlay** at `{{INSTRUCTION_PATH}}` in your worktree. This contains your task ID, hierarchy depth, and agent name.
|
|
164
164
|
2. **Load expertise** via `ml prime [domain]` for relevant domains.
|
|
165
165
|
3. **Search mulch for relevant context** before decomposing. Run `ml search <task keywords>` and review failure patterns, conventions, and decisions. Factor these insights into your specs.
|
|
166
166
|
4. **Load file-specific expertise** if files are known. Use `ml prime --files <file1,file2,...>` to get file-scoped context. Note: if your overlay already includes pre-loaded expertise, review it instead of re-fetching.
|
package/agents/merger.md
CHANGED
|
@@ -19,7 +19,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
19
19
|
|
|
20
20
|
## overlay
|
|
21
21
|
|
|
22
|
-
Your task-specific context (task ID, branches to merge, target branch, merge order, parent agent) is in
|
|
22
|
+
Your task-specific context (task ID, branches to merge, target branch, merge order, parent agent) is in `{{INSTRUCTION_PATH}}` in your worktree. That file is generated by `overstory sling` and tells you WHAT to merge. This file tells you HOW to merge.
|
|
23
23
|
|
|
24
24
|
## constraints
|
|
25
25
|
|
|
@@ -97,7 +97,7 @@ You are a branch integration specialist. When workers complete their tasks on se
|
|
|
97
97
|
|
|
98
98
|
## workflow
|
|
99
99
|
|
|
100
|
-
1. **Read your overlay** at
|
|
100
|
+
1. **Read your overlay** at `{{INSTRUCTION_PATH}}` in your worktree. This contains your task ID, the branches to merge, the target branch, and your agent name.
|
|
101
101
|
2. **Read the task spec** at the path specified in your overlay. Understand which branches need merging and in what order.
|
|
102
102
|
3. **Review the branches** before merging:
|
|
103
103
|
- `git log <target>..<branch>` to see what each branch contains.
|
package/agents/orchestrator.md
CHANGED
|
@@ -31,7 +31,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
31
31
|
|
|
32
32
|
## overlay
|
|
33
33
|
|
|
34
|
-
Your task-specific context (task ID, file scope, spec path, branch name, parent agent) is in
|
|
34
|
+
Your task-specific context (task ID, file scope, spec path, branch name, parent agent) is in `{{INSTRUCTION_PATH}}` in your worktree. That file is generated by `ov sling` and tells you WHAT to work on. This file tells you HOW to work.
|
|
35
35
|
|
|
36
36
|
## constraints
|
|
37
37
|
|
package/agents/reviewer.md
CHANGED
|
@@ -16,7 +16,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
16
16
|
|
|
17
17
|
## overlay
|
|
18
18
|
|
|
19
|
-
Your task-specific context (task ID, code to review, branch name, parent agent) is in
|
|
19
|
+
Your task-specific context (task ID, code to review, branch name, parent agent) is in `{{INSTRUCTION_PATH}}` in your worktree. That file is generated by `overstory sling` and tells you WHAT to review. This file tells you HOW to review.
|
|
20
20
|
|
|
21
21
|
## constraints
|
|
22
22
|
|
|
@@ -95,7 +95,7 @@ You are a validation specialist. Given code to review, you check it for correctn
|
|
|
95
95
|
|
|
96
96
|
## workflow
|
|
97
97
|
|
|
98
|
-
1. **Read your overlay** at
|
|
98
|
+
1. **Read your overlay** at `{{INSTRUCTION_PATH}}` in your worktree. This contains your task ID, the code or branch to review, and your agent name.
|
|
99
99
|
2. **Read the task spec** at the path specified in your overlay. Understand what was supposed to be built.
|
|
100
100
|
3. **Load expertise** via `ml prime [domain]` to understand project conventions and standards.
|
|
101
101
|
4. **Review the code changes:**
|
package/agents/scout.md
CHANGED
|
@@ -16,7 +16,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
16
16
|
|
|
17
17
|
## overlay
|
|
18
18
|
|
|
19
|
-
Your task-specific context (what to explore, who spawned you, your agent name) is in
|
|
19
|
+
Your task-specific context (what to explore, who spawned you, your agent name) is in `{{INSTRUCTION_PATH}}` in your worktree. That file is generated by `overstory sling` and tells you WHAT to work on. This file tells you HOW to work.
|
|
20
20
|
|
|
21
21
|
## constraints
|
|
22
22
|
|
|
@@ -97,7 +97,7 @@ You perform reconnaissance. Given a research question, exploration target, or an
|
|
|
97
97
|
|
|
98
98
|
## workflow
|
|
99
99
|
|
|
100
|
-
1. **Read your overlay** at
|
|
100
|
+
1. **Read your overlay** at `{{INSTRUCTION_PATH}}` in your worktree. This contains your task assignment, spec path, and agent name.
|
|
101
101
|
2. **Read the task spec** at the path specified in your overlay.
|
|
102
102
|
3. **Load relevant expertise** via `ml prime [domain]` for domains listed in your overlay.
|
|
103
103
|
4. **Explore systematically:**
|
package/agents/supervisor.md
CHANGED
|
@@ -31,7 +31,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
31
31
|
|
|
32
32
|
## overlay
|
|
33
33
|
|
|
34
|
-
Unlike the coordinator (which has no overlay), you receive your task-specific context via the overlay CLAUDE.md at
|
|
34
|
+
Unlike the coordinator (which has no overlay), you receive your task-specific context via the overlay CLAUDE.md at `{{INSTRUCTION_PATH}}` in your worktree root. This file is generated by `ov supervisor start` (or `ov sling` with `--capability supervisor`) and provides:
|
|
35
35
|
|
|
36
36
|
- **Agent Name** (`$OVERSTORY_AGENT_NAME`) -- your mail address
|
|
37
37
|
- **Task ID** -- the issue you are assigned to
|
|
@@ -163,7 +163,7 @@ Before spawning, check `ov status` to ensure non-overlapping file scope across a
|
|
|
163
163
|
|
|
164
164
|
## workflow
|
|
165
165
|
|
|
166
|
-
1. **Receive the dispatch.** Your overlay (
|
|
166
|
+
1. **Receive the dispatch.** Your overlay (`{{INSTRUCTION_PATH}}`) contains your task ID and spec path. The coordinator sends you a `dispatch` mail with task details.
|
|
167
167
|
2. **Read your task spec** at the path specified in your overlay. Understand the full scope of work assigned to you.
|
|
168
168
|
3. **Load expertise** via `ml prime [domain]` for each relevant domain. Check `{{TRACKER_CLI}} show <task-id>` for task details and dependencies.
|
|
169
169
|
4. **Analyze scope and decompose.** Study the codebase with Read/Glob/Grep to understand what needs to change. Determine:
|
|
@@ -418,7 +418,7 @@ You are long-lived within a project. You survive across batches and can recover
|
|
|
418
418
|
- **Checkpoints** are saved to `.overstory/agents/$OVERSTORY_AGENT_NAME/checkpoint.json` before compaction or handoff. The checkpoint contains: agent name, assigned task ID, active worker IDs, task group ID, session ID, progress summary, and files modified.
|
|
419
419
|
- **On recovery**, reload context by:
|
|
420
420
|
1. Reading your checkpoint: `.overstory/agents/$OVERSTORY_AGENT_NAME/checkpoint.json`
|
|
421
|
-
2. Reading your overlay:
|
|
421
|
+
2. Reading your overlay: `{{INSTRUCTION_PATH}}` (task ID, spec path, depth, parent)
|
|
422
422
|
3. Checking active group: `ov group status <group-id>`
|
|
423
423
|
4. Checking worker states: `ov status`
|
|
424
424
|
5. Checking unread mail: `ov mail check --agent $OVERSTORY_AGENT_NAME`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-eco/overstory-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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",
|
|
@@ -875,6 +875,48 @@ describe("formatQualityGatesCapabilities", () => {
|
|
|
875
875
|
});
|
|
876
876
|
});
|
|
877
877
|
|
|
878
|
+
describe("INSTRUCTION_PATH placeholder", () => {
|
|
879
|
+
test("defaults to .claude/CLAUDE.md when instructionPath is not set", async () => {
|
|
880
|
+
const config = makeConfig({
|
|
881
|
+
baseDefinition: "Read your overlay at {{INSTRUCTION_PATH}} in your worktree.",
|
|
882
|
+
});
|
|
883
|
+
const output = await generateOverlay(config);
|
|
884
|
+
|
|
885
|
+
expect(output).toContain("Read your overlay at .claude/CLAUDE.md in your worktree.");
|
|
886
|
+
expect(output).not.toContain("{{INSTRUCTION_PATH}}");
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("uses custom instructionPath when set", async () => {
|
|
890
|
+
const config = makeConfig({
|
|
891
|
+
instructionPath: "SAPLING.md",
|
|
892
|
+
baseDefinition: "Read your overlay at {{INSTRUCTION_PATH}} in your worktree.",
|
|
893
|
+
});
|
|
894
|
+
const output = await generateOverlay(config);
|
|
895
|
+
|
|
896
|
+
expect(output).toContain("Read your overlay at SAPLING.md in your worktree.");
|
|
897
|
+
expect(output).not.toContain("{{INSTRUCTION_PATH}}");
|
|
898
|
+
expect(output).not.toContain(".claude/CLAUDE.md");
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test("INSTRUCTION_PATH in base definition replaced throughout (multiple occurrences)", async () => {
|
|
902
|
+
const config = makeConfig({
|
|
903
|
+
instructionPath: "AGENTS.md",
|
|
904
|
+
baseDefinition: "Step 1: read {{INSTRUCTION_PATH}}.\nContext is in {{INSTRUCTION_PATH}}.",
|
|
905
|
+
});
|
|
906
|
+
const output = await generateOverlay(config);
|
|
907
|
+
|
|
908
|
+
expect(output).not.toContain("{{INSTRUCTION_PATH}}");
|
|
909
|
+
expect(output.split("AGENTS.md").length - 1).toBeGreaterThanOrEqual(2);
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test("no unreplaced INSTRUCTION_PATH placeholders in final output", async () => {
|
|
913
|
+
const config = makeConfig({ instructionPath: "SAPLING.md" });
|
|
914
|
+
const output = await generateOverlay(config);
|
|
915
|
+
|
|
916
|
+
expect(output).not.toContain("{{INSTRUCTION_PATH}}");
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
878
920
|
describe("quality gate placeholders in base definitions", () => {
|
|
879
921
|
test("QUALITY_GATE_INLINE in base definition gets replaced", async () => {
|
|
880
922
|
const config = makeConfig({
|
package/src/agents/overlay.ts
CHANGED
|
@@ -320,6 +320,7 @@ export async function generateOverlay(config: OverlayConfig): Promise<string> {
|
|
|
320
320
|
"{{QUALITY_GATE_CAPABILITIES}}": formatQualityGatesCapabilities(config.qualityGates),
|
|
321
321
|
"{{TRACKER_CLI}}": config.trackerCli ?? "sd",
|
|
322
322
|
"{{TRACKER_NAME}}": config.trackerName ?? "seeds",
|
|
323
|
+
"{{INSTRUCTION_PATH}}": config.instructionPath ?? ".claude/CLAUDE.md",
|
|
323
324
|
};
|
|
324
325
|
|
|
325
326
|
let result = template;
|
|
@@ -409,6 +409,92 @@ describe("renderAgentPanel", () => {
|
|
|
409
409
|
// dimBox.vertical is a dimmed ANSI string — present in output
|
|
410
410
|
expect(out).toContain(dimBox.vertical);
|
|
411
411
|
});
|
|
412
|
+
|
|
413
|
+
test("renders Live column header (not Tmux)", () => {
|
|
414
|
+
const data = makeDashboardData({});
|
|
415
|
+
const out = renderAgentPanel(data, 100, 12, 3);
|
|
416
|
+
expect(out).toContain("Live");
|
|
417
|
+
expect(out).not.toContain("Tmux");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("shows green dot for headless agent with alive PID", () => {
|
|
421
|
+
const alivePid = process.pid; // own PID — guaranteed alive
|
|
422
|
+
const data = {
|
|
423
|
+
...makeDashboardData({}),
|
|
424
|
+
status: {
|
|
425
|
+
currentRunId: null,
|
|
426
|
+
agents: [
|
|
427
|
+
{
|
|
428
|
+
id: "sess-h1",
|
|
429
|
+
agentName: "headless-worker",
|
|
430
|
+
capability: "builder",
|
|
431
|
+
worktreePath: "/tmp/wt/headless",
|
|
432
|
+
branchName: "overstory/headless/task-1",
|
|
433
|
+
taskId: "task-h1",
|
|
434
|
+
tmuxSession: "", // headless
|
|
435
|
+
state: "working" as const,
|
|
436
|
+
pid: alivePid,
|
|
437
|
+
parentAgent: null,
|
|
438
|
+
depth: 0,
|
|
439
|
+
runId: null,
|
|
440
|
+
startedAt: new Date(Date.now() - 10_000).toISOString(),
|
|
441
|
+
lastActivity: new Date().toISOString(),
|
|
442
|
+
escalationLevel: 0,
|
|
443
|
+
stalledSince: null,
|
|
444
|
+
transcriptPath: null,
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
worktrees: [],
|
|
448
|
+
tmuxSessions: [], // no tmux sessions
|
|
449
|
+
unreadMailCount: 0,
|
|
450
|
+
mergeQueueCount: 0,
|
|
451
|
+
recentMetricsCount: 0,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
const out = renderAgentPanel(data, 100, 12, 3);
|
|
455
|
+
// Green ">" for alive headless agent
|
|
456
|
+
expect(out).toContain(">");
|
|
457
|
+
expect(out).toContain("headless-worker");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("shows red dot for headless agent with dead PID", () => {
|
|
461
|
+
const deadPid = 2_147_483_647;
|
|
462
|
+
const data = {
|
|
463
|
+
...makeDashboardData({}),
|
|
464
|
+
status: {
|
|
465
|
+
currentRunId: null,
|
|
466
|
+
agents: [
|
|
467
|
+
{
|
|
468
|
+
id: "sess-h2",
|
|
469
|
+
agentName: "dead-headless", // short enough to not be truncated
|
|
470
|
+
capability: "builder",
|
|
471
|
+
worktreePath: "/tmp/wt/dead-headless",
|
|
472
|
+
branchName: "overstory/dead-headless/task-2",
|
|
473
|
+
taskId: "task-h2",
|
|
474
|
+
tmuxSession: "", // headless
|
|
475
|
+
state: "working" as const,
|
|
476
|
+
pid: deadPid,
|
|
477
|
+
parentAgent: null,
|
|
478
|
+
depth: 0,
|
|
479
|
+
runId: null,
|
|
480
|
+
startedAt: new Date(Date.now() - 10_000).toISOString(),
|
|
481
|
+
lastActivity: new Date().toISOString(),
|
|
482
|
+
escalationLevel: 0,
|
|
483
|
+
stalledSince: null,
|
|
484
|
+
transcriptPath: null,
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
worktrees: [],
|
|
488
|
+
tmuxSessions: [],
|
|
489
|
+
unreadMailCount: 0,
|
|
490
|
+
mergeQueueCount: 0,
|
|
491
|
+
recentMetricsCount: 0,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
const out = renderAgentPanel(data, 100, 12, 3);
|
|
495
|
+
expect(out).toContain("x");
|
|
496
|
+
expect(out).toContain("dead-headless");
|
|
497
|
+
});
|
|
412
498
|
});
|
|
413
499
|
|
|
414
500
|
describe("openDashboardStores", () => {
|
|
@@ -42,6 +42,7 @@ import { createTrackerClient, resolveBackend } from "../tracker/factory.ts";
|
|
|
42
42
|
import type { TrackerIssue } from "../tracker/types.ts";
|
|
43
43
|
import type { EventStore, MailMessage, StoredEvent } from "../types.ts";
|
|
44
44
|
import { evaluateHealth } from "../watchdog/health.ts";
|
|
45
|
+
import { isProcessAlive } from "../worktree/tmux.ts";
|
|
45
46
|
import { getCachedTmuxSessions, getCachedWorktrees, type StatusData } from "./status.ts";
|
|
46
47
|
|
|
47
48
|
const pkgPath = resolve(import.meta.dir, "../../package.json");
|
|
@@ -555,7 +556,7 @@ export function renderAgentPanel(
|
|
|
555
556
|
output += `${CURSOR.cursorTo(startRow, 1)}${headerLine}${headerPadding}${dimBox.vertical}\n`;
|
|
556
557
|
|
|
557
558
|
// Column headers
|
|
558
|
-
const colStr = `${dimBox.vertical} St Name Capability State Task ID Duration
|
|
559
|
+
const colStr = `${dimBox.vertical} St Name Capability State Task ID Duration Live `;
|
|
559
560
|
const colPadding = " ".repeat(
|
|
560
561
|
Math.max(0, leftWidth - visibleLength(colStr) - visibleLength(dimBox.vertical)),
|
|
561
562
|
);
|
|
@@ -595,10 +596,13 @@ export function renderAgentPanel(
|
|
|
595
596
|
: now;
|
|
596
597
|
const duration = formatDuration(endTime - new Date(agent.startedAt).getTime());
|
|
597
598
|
const durationPadded = pad(duration, 9);
|
|
598
|
-
const
|
|
599
|
-
const
|
|
599
|
+
const isHeadless = agent.tmuxSession === "" && agent.pid !== null;
|
|
600
|
+
const alive = isHeadless
|
|
601
|
+
? agent.pid !== null && isProcessAlive(agent.pid)
|
|
602
|
+
: data.status.tmuxSessions.some((s) => s.name === agent.tmuxSession);
|
|
603
|
+
const aliveDot = alive ? color.green(">") : color.red("x");
|
|
600
604
|
|
|
601
|
-
const lineContent = `${dimBox.vertical} ${stateColorFn(icon)} ${name} ${capability} ${stateColorFn(state)} ${taskId} ${durationPadded} ${
|
|
605
|
+
const lineContent = `${dimBox.vertical} ${stateColorFn(icon)} ${name} ${capability} ${stateColorFn(state)} ${taskId} ${durationPadded} ${aliveDot} `;
|
|
602
606
|
const linePadding = " ".repeat(
|
|
603
607
|
Math.max(0, leftWidth - visibleLength(lineContent) - visibleLength(dimBox.vertical)),
|
|
604
608
|
);
|
|
@@ -469,6 +469,10 @@ describe("feedCommand", () => {
|
|
|
469
469
|
"spawn",
|
|
470
470
|
"error",
|
|
471
471
|
"custom",
|
|
472
|
+
"turn_start",
|
|
473
|
+
"turn_end",
|
|
474
|
+
"progress",
|
|
475
|
+
"result",
|
|
472
476
|
] as const;
|
|
473
477
|
for (const eventType of eventTypes) {
|
|
474
478
|
store.insert(
|
|
@@ -494,6 +498,10 @@ describe("feedCommand", () => {
|
|
|
494
498
|
expect(out).toContain("SPAWN");
|
|
495
499
|
expect(out).toContain("ERROR");
|
|
496
500
|
expect(out).toContain("CUSTM");
|
|
501
|
+
expect(out).toContain("TURN+");
|
|
502
|
+
expect(out).toContain("TURN-");
|
|
503
|
+
expect(out).toContain("PROG ");
|
|
504
|
+
expect(out).toContain("RSULT");
|
|
497
505
|
});
|
|
498
506
|
});
|
|
499
507
|
|
|
@@ -18,7 +18,7 @@ import { createMetricsStore } from "../metrics/store.ts";
|
|
|
18
18
|
import { createSessionStore } from "../sessions/store.ts";
|
|
19
19
|
import { cleanupTempDir } from "../test-helpers.ts";
|
|
20
20
|
import type { InsertEvent, SessionMetrics } from "../types.ts";
|
|
21
|
-
import { gatherInspectData, inspectCommand } from "./inspect.ts";
|
|
21
|
+
import { gatherInspectData, inspectCommand, printInspectData } from "./inspect.ts";
|
|
22
22
|
|
|
23
23
|
/** Helper to create an InsertEvent with sensible defaults. */
|
|
24
24
|
function makeEvent(overrides: Partial<InsertEvent> = {}): InsertEvent {
|
|
@@ -565,6 +565,161 @@ describe("inspectCommand", () => {
|
|
|
565
565
|
});
|
|
566
566
|
});
|
|
567
567
|
|
|
568
|
+
// === Headless agent support ===
|
|
569
|
+
|
|
570
|
+
describe("headless agent support", () => {
|
|
571
|
+
test("gatherInspectData skips tmux capture for headless agents (empty tmuxSession)", async () => {
|
|
572
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
573
|
+
const sessionsDbPath = join(overstoryDir, "sessions.db");
|
|
574
|
+
const store = createSessionStore(sessionsDbPath);
|
|
575
|
+
store.upsert({
|
|
576
|
+
id: "sess-h1",
|
|
577
|
+
agentName: "headless-agent",
|
|
578
|
+
capability: "builder",
|
|
579
|
+
worktreePath: "/tmp/wt",
|
|
580
|
+
branchName: "overstory/headless/task-1",
|
|
581
|
+
taskId: "overstory-h01",
|
|
582
|
+
tmuxSession: "", // headless
|
|
583
|
+
state: "working",
|
|
584
|
+
pid: process.pid,
|
|
585
|
+
parentAgent: null,
|
|
586
|
+
depth: 0,
|
|
587
|
+
runId: null,
|
|
588
|
+
startedAt: new Date().toISOString(),
|
|
589
|
+
lastActivity: new Date().toISOString(),
|
|
590
|
+
escalationLevel: 0,
|
|
591
|
+
stalledSince: null,
|
|
592
|
+
transcriptPath: null,
|
|
593
|
+
});
|
|
594
|
+
store.close();
|
|
595
|
+
|
|
596
|
+
// noTmux=false but tmuxSession="" — should skip tmux capture without error
|
|
597
|
+
const data = await gatherInspectData(tempDir, "headless-agent", { noTmux: false });
|
|
598
|
+
// tmuxOutput is null (no tmux) and no events yet → no fallback either
|
|
599
|
+
expect(data.session.agentName).toBe("headless-agent");
|
|
600
|
+
expect(data.session.tmuxSession).toBe("");
|
|
601
|
+
// tmuxOutput may be null (no events) or a string (fallback) — must not throw
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("gatherInspectData provides event-based output for headless agents with tool calls", async () => {
|
|
605
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
606
|
+
const sessionsDbPath = join(overstoryDir, "sessions.db");
|
|
607
|
+
const eventsDbPath = join(overstoryDir, "events.db");
|
|
608
|
+
|
|
609
|
+
const store = createSessionStore(sessionsDbPath);
|
|
610
|
+
store.upsert({
|
|
611
|
+
id: "sess-h2",
|
|
612
|
+
agentName: "headless-events",
|
|
613
|
+
capability: "builder",
|
|
614
|
+
worktreePath: "/tmp/wt",
|
|
615
|
+
branchName: "overstory/headless/task-2",
|
|
616
|
+
taskId: "overstory-h02",
|
|
617
|
+
tmuxSession: "", // headless
|
|
618
|
+
state: "working",
|
|
619
|
+
pid: process.pid,
|
|
620
|
+
parentAgent: null,
|
|
621
|
+
depth: 0,
|
|
622
|
+
runId: null,
|
|
623
|
+
startedAt: new Date().toISOString(),
|
|
624
|
+
lastActivity: new Date().toISOString(),
|
|
625
|
+
escalationLevel: 0,
|
|
626
|
+
stalledSince: null,
|
|
627
|
+
transcriptPath: null,
|
|
628
|
+
});
|
|
629
|
+
store.close();
|
|
630
|
+
|
|
631
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
632
|
+
eventStore.insert(
|
|
633
|
+
makeEvent({ agentName: "headless-events", toolName: "Read", toolDurationMs: 50 }),
|
|
634
|
+
);
|
|
635
|
+
eventStore.insert(
|
|
636
|
+
makeEvent({ agentName: "headless-events", toolName: "Edit", toolDurationMs: 100 }),
|
|
637
|
+
);
|
|
638
|
+
eventStore.close();
|
|
639
|
+
|
|
640
|
+
const data = await gatherInspectData(tempDir, "headless-events", { noTmux: false });
|
|
641
|
+
|
|
642
|
+
// Should have fallback output
|
|
643
|
+
expect(data.tmuxOutput).not.toBeNull();
|
|
644
|
+
expect(data.tmuxOutput).toContain("Headless agent");
|
|
645
|
+
expect(data.tmuxOutput).toContain("Read");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("printInspectData shows PID instead of tmux session for headless agents", () => {
|
|
649
|
+
const data = {
|
|
650
|
+
session: {
|
|
651
|
+
id: "sess-h3",
|
|
652
|
+
agentName: "headless-display",
|
|
653
|
+
capability: "builder",
|
|
654
|
+
worktreePath: "/tmp/wt",
|
|
655
|
+
branchName: "overstory/headless/task-3",
|
|
656
|
+
taskId: "overstory-h03",
|
|
657
|
+
tmuxSession: "", // headless
|
|
658
|
+
state: "working" as const,
|
|
659
|
+
pid: 99999,
|
|
660
|
+
parentAgent: null,
|
|
661
|
+
depth: 0,
|
|
662
|
+
runId: null,
|
|
663
|
+
startedAt: new Date().toISOString(),
|
|
664
|
+
lastActivity: new Date().toISOString(),
|
|
665
|
+
escalationLevel: 0,
|
|
666
|
+
stalledSince: null,
|
|
667
|
+
transcriptPath: null,
|
|
668
|
+
},
|
|
669
|
+
timeSinceLastActivity: 5000,
|
|
670
|
+
recentToolCalls: [],
|
|
671
|
+
currentFile: null,
|
|
672
|
+
toolStats: [],
|
|
673
|
+
tokenUsage: null,
|
|
674
|
+
tmuxOutput: null,
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
printInspectData(data);
|
|
678
|
+
|
|
679
|
+
const out = output();
|
|
680
|
+
expect(out).toContain("Process: PID");
|
|
681
|
+
expect(out).toContain("99999");
|
|
682
|
+
expect(out).toContain("headless");
|
|
683
|
+
expect(out).not.toContain("Tmux:");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("printInspectData shows Recent Activity header for headless agents with tmuxOutput", () => {
|
|
687
|
+
const data = {
|
|
688
|
+
session: {
|
|
689
|
+
id: "sess-h4",
|
|
690
|
+
agentName: "headless-activity",
|
|
691
|
+
capability: "builder",
|
|
692
|
+
worktreePath: "/tmp/wt",
|
|
693
|
+
branchName: "overstory/headless/task-4",
|
|
694
|
+
taskId: "overstory-h04",
|
|
695
|
+
tmuxSession: "", // headless
|
|
696
|
+
state: "working" as const,
|
|
697
|
+
pid: 99998,
|
|
698
|
+
parentAgent: null,
|
|
699
|
+
depth: 0,
|
|
700
|
+
runId: null,
|
|
701
|
+
startedAt: new Date().toISOString(),
|
|
702
|
+
lastActivity: new Date().toISOString(),
|
|
703
|
+
escalationLevel: 0,
|
|
704
|
+
stalledSince: null,
|
|
705
|
+
transcriptPath: null,
|
|
706
|
+
},
|
|
707
|
+
timeSinceLastActivity: 5000,
|
|
708
|
+
recentToolCalls: [],
|
|
709
|
+
currentFile: null,
|
|
710
|
+
toolStats: [],
|
|
711
|
+
tokenUsage: null,
|
|
712
|
+
tmuxOutput: "[Headless agent — showing recent tool events]",
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
printInspectData(data);
|
|
716
|
+
|
|
717
|
+
const out = output();
|
|
718
|
+
expect(out).toContain("Recent Activity (headless)");
|
|
719
|
+
expect(out).not.toContain("Live Tmux Output");
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
568
723
|
// === Human-readable output ===
|
|
569
724
|
|
|
570
725
|
describe("human-readable output", () => {
|
package/src/commands/inspect.ts
CHANGED
|
@@ -193,13 +193,24 @@ export async function gatherInspectData(
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
// tmux capture
|
|
196
|
+
// tmux capture (skipped for headless agents where tmuxSession is empty)
|
|
197
197
|
let tmuxOutput: string | null = null;
|
|
198
198
|
if (!opts.noTmux && session.tmuxSession) {
|
|
199
199
|
const lines = opts.tmuxLines ?? 30;
|
|
200
200
|
tmuxOutput = await captureTmux(session.tmuxSession, lines);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// Headless fallback: show recent events as live output when no tmux
|
|
204
|
+
if (!tmuxOutput && session.tmuxSession === "" && recentToolCalls.length > 0) {
|
|
205
|
+
const lines: string[] = ["[Headless agent — showing recent tool events]", ""];
|
|
206
|
+
for (const call of recentToolCalls.slice(0, 15)) {
|
|
207
|
+
const time = new Date(call.timestamp).toLocaleTimeString();
|
|
208
|
+
const dur = call.durationMs !== null ? `${call.durationMs}ms` : "pending";
|
|
209
|
+
lines.push(` [${time}] ${call.toolName.padEnd(15)} ${dur}`);
|
|
210
|
+
}
|
|
211
|
+
tmuxOutput = lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
return {
|
|
204
215
|
session,
|
|
205
216
|
timeSinceLastActivity,
|
|
@@ -233,7 +244,11 @@ export function printInspectData(data: InspectData): void {
|
|
|
233
244
|
w(`Parent: ${accent(session.parentAgent)} (depth: ${session.depth})\n`);
|
|
234
245
|
}
|
|
235
246
|
w(`Started: ${session.startedAt}\n`);
|
|
236
|
-
|
|
247
|
+
if (session.tmuxSession) {
|
|
248
|
+
w(`Tmux: ${accent(session.tmuxSession)}\n`);
|
|
249
|
+
} else if (session.pid !== null) {
|
|
250
|
+
w(`Process: PID ${accent(String(session.pid))} (headless)\n`);
|
|
251
|
+
}
|
|
237
252
|
w("\n");
|
|
238
253
|
|
|
239
254
|
// Current file
|
|
@@ -287,9 +302,9 @@ export function printInspectData(data: InspectData): void {
|
|
|
287
302
|
w("\n");
|
|
288
303
|
}
|
|
289
304
|
|
|
290
|
-
// tmux output
|
|
305
|
+
// tmux output (or headless fallback)
|
|
291
306
|
if (data.tmuxOutput) {
|
|
292
|
-
w("Live Tmux Output\n");
|
|
307
|
+
w(data.session.tmuxSession ? "Live Tmux Output\n" : "Recent Activity (headless)\n");
|
|
293
308
|
w(`${separator()}\n`);
|
|
294
309
|
w(`${data.tmuxOutput}\n`);
|
|
295
310
|
w(`${separator()}\n`);
|
|
@@ -701,6 +701,10 @@ describe("replayCommand", () => {
|
|
|
701
701
|
"spawn",
|
|
702
702
|
"error",
|
|
703
703
|
"custom",
|
|
704
|
+
"turn_start",
|
|
705
|
+
"turn_end",
|
|
706
|
+
"progress",
|
|
707
|
+
"result",
|
|
704
708
|
] as const;
|
|
705
709
|
for (const eventType of eventTypes) {
|
|
706
710
|
store.insert(
|
|
@@ -724,6 +728,10 @@ describe("replayCommand", () => {
|
|
|
724
728
|
expect(out).toContain("SPAWN");
|
|
725
729
|
expect(out).toContain("ERROR");
|
|
726
730
|
expect(out).toContain("CUSTOM");
|
|
731
|
+
expect(out).toContain("TURN START");
|
|
732
|
+
expect(out).toContain("TURN END");
|
|
733
|
+
expect(out).toContain("PROGRESS");
|
|
734
|
+
expect(out).toContain("RESULT");
|
|
727
735
|
});
|
|
728
736
|
|
|
729
737
|
test("long data values are truncated", async () => {
|