@os-eco/overstory-cli 0.6.6 → 0.6.7
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 +1 -1
- package/src/commands/coordinator.ts +3 -3
- package/src/commands/monitor.ts +2 -2
- package/src/commands/sling.ts +3 -3
- package/src/commands/status.ts +12 -9
- package/src/commands/supervisor.ts +3 -3
- package/src/index.ts +1 -1
- package/src/worktree/tmux.test.ts +3 -3
- package/src/worktree/tmux.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-eco/overstory-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code — spawn worker agents in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution",
|
|
5
5
|
"author": "Jaymin West",
|
|
6
6
|
"license": "MIT",
|
|
@@ -282,7 +282,7 @@ async function startCoordinator(
|
|
|
282
282
|
|
|
283
283
|
if (isRunningAsRoot()) {
|
|
284
284
|
throw new AgentError(
|
|
285
|
-
"Cannot spawn agents as root (UID 0). The claude CLI rejects --
|
|
285
|
+
"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 overstory as a non-root user.",
|
|
286
286
|
);
|
|
287
287
|
}
|
|
288
288
|
|
|
@@ -357,7 +357,7 @@ async function startCoordinator(
|
|
|
357
357
|
// (overstory-gaio, overstory-0kwf).
|
|
358
358
|
const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "coordinator.md");
|
|
359
359
|
const agentDefFile = Bun.file(agentDefPath);
|
|
360
|
-
let claudeCmd = `claude --model ${model} --
|
|
360
|
+
let claudeCmd = `claude --model ${model} --permission-mode bypassPermissions`;
|
|
361
361
|
if (await agentDefFile.exists()) {
|
|
362
362
|
const agentDef = await agentDefFile.text();
|
|
363
363
|
// Single-quote the content for safe shell expansion (only escape single quotes)
|
|
@@ -417,7 +417,7 @@ async function startCoordinator(
|
|
|
417
417
|
await tmux.sendKeys(tmuxSession, beacon);
|
|
418
418
|
|
|
419
419
|
// Follow-up Enters with increasing delays to ensure submission
|
|
420
|
-
for (const delay of [1_000, 2_000]) {
|
|
420
|
+
for (const delay of [1_000, 2_000, 3_000, 5_000]) {
|
|
421
421
|
await Bun.sleep(delay);
|
|
422
422
|
await tmux.sendKeys(tmuxSession, "");
|
|
423
423
|
}
|
package/src/commands/monitor.ts
CHANGED
|
@@ -67,7 +67,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
67
67
|
|
|
68
68
|
if (isRunningAsRoot()) {
|
|
69
69
|
throw new AgentError(
|
|
70
|
-
"Cannot spawn agents as root (UID 0). The claude CLI rejects --
|
|
70
|
+
"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 overstory as a non-root user.",
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -137,7 +137,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
137
137
|
// Spawn tmux session at project root with Claude Code (interactive mode).
|
|
138
138
|
const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "monitor.md");
|
|
139
139
|
const agentDefFile = Bun.file(agentDefPath);
|
|
140
|
-
let claudeCmd = `claude --model ${model} --
|
|
140
|
+
let claudeCmd = `claude --model ${model} --permission-mode bypassPermissions`;
|
|
141
141
|
if (await agentDefFile.exists()) {
|
|
142
142
|
const agentDef = await agentDefFile.text();
|
|
143
143
|
const escaped = agentDef.replace(/'/g, "'\\''");
|
package/src/commands/sling.ts
CHANGED
|
@@ -308,7 +308,7 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
|
|
|
308
308
|
|
|
309
309
|
if (isRunningAsRoot()) {
|
|
310
310
|
throw new AgentError(
|
|
311
|
-
"Cannot spawn agents as root (UID 0). The claude CLI rejects --
|
|
311
|
+
"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 overstory as a non-root user.",
|
|
312
312
|
{ agentName: name },
|
|
313
313
|
);
|
|
314
314
|
}
|
|
@@ -614,7 +614,7 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
|
|
|
614
614
|
// 12. Create tmux session running claude in interactive mode
|
|
615
615
|
const tmuxSessionName = `overstory-${config.project.name}-${name}`;
|
|
616
616
|
const { model, env } = resolveModel(config, manifest, capability, agentDef.model);
|
|
617
|
-
const claudeCmd = `claude --model ${model} --
|
|
617
|
+
const claudeCmd = `claude --model ${model} --permission-mode bypassPermissions`;
|
|
618
618
|
const pid = await createSession(tmuxSessionName, worktreePath, claudeCmd, {
|
|
619
619
|
...env,
|
|
620
620
|
OVERSTORY_AGENT_NAME: name,
|
|
@@ -673,7 +673,7 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
|
|
|
673
673
|
// 13c. Follow-up Enters with increasing delays to ensure submission.
|
|
674
674
|
// Claude Code's TUI may consume early Enters during late initialization
|
|
675
675
|
// (overstory-yhv6). An Enter on an empty input line is harmless.
|
|
676
|
-
for (const delay of [1_000, 2_000]) {
|
|
676
|
+
for (const delay of [1_000, 2_000, 3_000, 5_000]) {
|
|
677
677
|
await Bun.sleep(delay);
|
|
678
678
|
await sendKeys(tmuxSessionName, "");
|
|
679
679
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { loadConfig } from "../config.ts";
|
|
11
11
|
import { ValidationError } from "../errors.ts";
|
|
12
|
+
import { color } from "../logging/color.ts";
|
|
12
13
|
import { createMailStore } from "../mail/store.ts";
|
|
13
14
|
import { createMergeQueue } from "../merge/queue.ts";
|
|
14
15
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
@@ -254,15 +255,15 @@ export function printStatus(data: StatusData): void {
|
|
|
254
255
|
const now = Date.now();
|
|
255
256
|
const w = process.stdout.write.bind(process.stdout);
|
|
256
257
|
|
|
257
|
-
w("
|
|
258
|
+
w("Overstory Status\n");
|
|
258
259
|
w(`${"═".repeat(60)}\n\n`);
|
|
259
260
|
if (data.currentRunId) {
|
|
260
|
-
w(
|
|
261
|
+
w(`Run: ${data.currentRunId}\n`);
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
// Active agents
|
|
264
265
|
const active = data.agents.filter((a) => a.state !== "zombie" && a.state !== "completed");
|
|
265
|
-
w(
|
|
266
|
+
w(`Agents: ${active.length} active\n`);
|
|
266
267
|
if (active.length > 0) {
|
|
267
268
|
const tmuxSessionNames = new Set(data.tmuxSessions.map((s) => s.name));
|
|
268
269
|
for (const agent of active) {
|
|
@@ -272,7 +273,7 @@ export function printStatus(data: StatusData): void {
|
|
|
272
273
|
: now;
|
|
273
274
|
const duration = formatDuration(endTime - new Date(agent.startedAt).getTime());
|
|
274
275
|
const tmuxAlive = tmuxSessionNames.has(agent.tmuxSession);
|
|
275
|
-
const aliveMarker = tmuxAlive ? "
|
|
276
|
+
const aliveMarker = tmuxAlive ? color.green(">") : color.red("x");
|
|
276
277
|
w(` ${aliveMarker} ${agent.agentName} [${agent.capability}] `);
|
|
277
278
|
w(`${agent.state} | ${agent.taskId} | ${duration}\n`);
|
|
278
279
|
|
|
@@ -291,7 +292,7 @@ export function printStatus(data: StatusData): void {
|
|
|
291
292
|
|
|
292
293
|
// Worktrees
|
|
293
294
|
const overstoryWts = data.worktrees.filter((wt) => wt.branch.startsWith("overstory/"));
|
|
294
|
-
w(
|
|
295
|
+
w(`Worktrees: ${overstoryWts.length}\n`);
|
|
295
296
|
for (const wt of overstoryWts) {
|
|
296
297
|
w(` ${wt.branch}\n`);
|
|
297
298
|
}
|
|
@@ -301,13 +302,13 @@ export function printStatus(data: StatusData): void {
|
|
|
301
302
|
w("\n");
|
|
302
303
|
|
|
303
304
|
// Mail
|
|
304
|
-
w(
|
|
305
|
+
w(`Mail: ${data.unreadMailCount} unread\n`);
|
|
305
306
|
|
|
306
307
|
// Merge queue
|
|
307
|
-
w(
|
|
308
|
+
w(`Merge queue: ${data.mergeQueueCount} pending\n`);
|
|
308
309
|
|
|
309
310
|
// Metrics
|
|
310
|
-
w(
|
|
311
|
+
w(`Sessions recorded: ${data.recentMetricsCount}\n`);
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
interface StatusOpts {
|
|
@@ -347,7 +348,9 @@ async function executeStatus(opts: StatusOpts): Promise<void> {
|
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
if (watch) {
|
|
350
|
-
process.stderr.write(
|
|
351
|
+
process.stderr.write(
|
|
352
|
+
"Warning: --watch is deprecated. Use 'ov dashboard' for live monitoring.\n\n",
|
|
353
|
+
);
|
|
351
354
|
// Polling loop (kept for one release cycle)
|
|
352
355
|
while (true) {
|
|
353
356
|
// Clear screen
|
|
@@ -92,7 +92,7 @@ async function startSupervisor(opts: {
|
|
|
92
92
|
|
|
93
93
|
if (isRunningAsRoot()) {
|
|
94
94
|
throw new AgentError(
|
|
95
|
-
"Cannot spawn agents as root (UID 0). The claude CLI rejects --
|
|
95
|
+
"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 overstory as a non-root user.",
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -165,7 +165,7 @@ async function startSupervisor(opts: {
|
|
|
165
165
|
const tmuxSession = `overstory-${config.project.name}-supervisor-${opts.name}`;
|
|
166
166
|
const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "supervisor.md");
|
|
167
167
|
const agentDefFile = Bun.file(agentDefPath);
|
|
168
|
-
let claudeCmd = `claude --model ${model} --
|
|
168
|
+
let claudeCmd = `claude --model ${model} --permission-mode bypassPermissions`;
|
|
169
169
|
if (await agentDefFile.exists()) {
|
|
170
170
|
const agentDef = await agentDefFile.text();
|
|
171
171
|
const escaped = agentDef.replace(/'/g, "'\\''");
|
|
@@ -190,7 +190,7 @@ async function startSupervisor(opts: {
|
|
|
190
190
|
await sendKeys(tmuxSession, beacon);
|
|
191
191
|
|
|
192
192
|
// Follow-up Enters with increasing delays to ensure submission
|
|
193
|
-
for (const delay of [1_000, 2_000]) {
|
|
193
|
+
for (const delay of [1_000, 2_000, 3_000, 5_000]) {
|
|
194
194
|
await Bun.sleep(delay);
|
|
195
195
|
await sendKeys(tmuxSession, "");
|
|
196
196
|
}
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { createWorktreeCommand } from "./commands/worktree.ts";
|
|
|
42
42
|
import { OverstoryError, WorktreeError } from "./errors.ts";
|
|
43
43
|
import { setQuiet } from "./logging/color.ts";
|
|
44
44
|
|
|
45
|
-
const VERSION = "0.6.
|
|
45
|
+
const VERSION = "0.6.7";
|
|
46
46
|
|
|
47
47
|
const COMMANDS = [
|
|
48
48
|
"agents",
|
|
@@ -957,7 +957,7 @@ describe("waitForTuiReady", () => {
|
|
|
957
957
|
});
|
|
958
958
|
|
|
959
959
|
test("returns true immediately when pane has content on first poll", async () => {
|
|
960
|
-
spawnSpy.mockImplementation(() => mockSpawnResult("
|
|
960
|
+
spawnSpy.mockImplementation(() => mockSpawnResult('Try "help" to get started', "", 0));
|
|
961
961
|
|
|
962
962
|
const ready = await waitForTuiReady("overstory-agent", 5_000, 500);
|
|
963
963
|
|
|
@@ -977,7 +977,7 @@ describe("waitForTuiReady", () => {
|
|
|
977
977
|
return mockSpawnResult("", "", 0);
|
|
978
978
|
}
|
|
979
979
|
// 4th poll: content appears
|
|
980
|
-
return mockSpawnResult("Welcome to Claude Code
|
|
980
|
+
return mockSpawnResult("Welcome to Claude Code!\n\n\u276f", "", 0);
|
|
981
981
|
}
|
|
982
982
|
// has-session: session is alive throughout
|
|
983
983
|
return mockSpawnResult("", "", 0);
|
|
@@ -1011,7 +1011,7 @@ describe("waitForTuiReady", () => {
|
|
|
1011
1011
|
|
|
1012
1012
|
test("uses default timeout and poll interval", async () => {
|
|
1013
1013
|
// Return content immediately
|
|
1014
|
-
spawnSpy.mockImplementation(() => mockSpawnResult("
|
|
1014
|
+
spawnSpy.mockImplementation(() => mockSpawnResult('Try "help"', "", 0));
|
|
1015
1015
|
|
|
1016
1016
|
const ready = await waitForTuiReady("overstory-agent");
|
|
1017
1017
|
|
package/src/worktree/tmux.ts
CHANGED
|
@@ -447,13 +447,13 @@ export async function capturePaneContent(name: string, lines = 50): Promise<stri
|
|
|
447
447
|
*/
|
|
448
448
|
export async function waitForTuiReady(
|
|
449
449
|
name: string,
|
|
450
|
-
timeoutMs =
|
|
450
|
+
timeoutMs = 30_000,
|
|
451
451
|
pollIntervalMs = 500,
|
|
452
452
|
): Promise<boolean> {
|
|
453
453
|
const maxAttempts = Math.ceil(timeoutMs / pollIntervalMs);
|
|
454
454
|
for (let i = 0; i < maxAttempts; i++) {
|
|
455
455
|
const content = await capturePaneContent(name);
|
|
456
|
-
if (content !== null) {
|
|
456
|
+
if (content !== null && (content.includes("\u276f") || content.includes('Try "'))) {
|
|
457
457
|
return true;
|
|
458
458
|
}
|
|
459
459
|
// Check if session died — no point waiting if it's gone
|