@os-eco/overstory-cli 0.6.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/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createSessionStore } from "../sessions/store.ts";
|
|
6
|
+
import type { OverstoryConfig } from "../types.ts";
|
|
7
|
+
import type { ConsistencyCheckDeps } from "./consistency.ts";
|
|
8
|
+
import { checkConsistency } from "./consistency.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mock tmux functions using dependency injection instead of mock.module().
|
|
12
|
+
* This avoids test isolation issues from module-level mocking.
|
|
13
|
+
*/
|
|
14
|
+
const mockListSessions = mock(() => Promise.resolve([] as Array<{ name: string; pid: number }>));
|
|
15
|
+
const mockIsProcessAlive = mock((_pid: number) => true);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a minimal temp git repo for worktree tests.
|
|
19
|
+
*/
|
|
20
|
+
function createTempGitRepo(): string {
|
|
21
|
+
const dir = mkdtempSync(join(tmpdir(), "overstory-test-"));
|
|
22
|
+
const git = (args: string[]) => {
|
|
23
|
+
const proc = Bun.spawnSync(["git", ...args], { cwd: dir, stdout: "ignore", stderr: "pipe" });
|
|
24
|
+
if (proc.exitCode !== 0) {
|
|
25
|
+
throw new Error(`git ${args.join(" ")} failed: ${proc.stderr.toString()}`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
git(["init"]);
|
|
30
|
+
git(["config", "user.email", "test@test.com"]);
|
|
31
|
+
git(["config", "user.name", "Test User"]);
|
|
32
|
+
git(["config", "commit.gpgsign", "false"]);
|
|
33
|
+
writeFileSync(join(dir, "README.md"), "# Test Repo\n");
|
|
34
|
+
git(["add", "."]);
|
|
35
|
+
git(["commit", "-m", "Initial commit"]);
|
|
36
|
+
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a git worktree at the given path.
|
|
42
|
+
*/
|
|
43
|
+
function createWorktree(repoRoot: string, worktreePath: string, branchName: string): void {
|
|
44
|
+
const proc = Bun.spawnSync(["git", "worktree", "add", "-b", branchName, worktreePath, "HEAD"], {
|
|
45
|
+
cwd: repoRoot,
|
|
46
|
+
stdout: "ignore",
|
|
47
|
+
stderr: "pipe",
|
|
48
|
+
});
|
|
49
|
+
if (proc.exitCode !== 0) {
|
|
50
|
+
throw new Error(`Failed to create worktree: ${proc.stderr.toString()}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("checkConsistency", () => {
|
|
55
|
+
let repoRoot: string;
|
|
56
|
+
let overstoryDir: string;
|
|
57
|
+
let config: OverstoryConfig;
|
|
58
|
+
let mockDeps: ConsistencyCheckDeps;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
repoRoot = createTempGitRepo();
|
|
62
|
+
overstoryDir = join(repoRoot, ".overstory");
|
|
63
|
+
mkdirSync(overstoryDir, { recursive: true });
|
|
64
|
+
mkdirSync(join(overstoryDir, "worktrees"), { recursive: true });
|
|
65
|
+
|
|
66
|
+
config = {
|
|
67
|
+
project: {
|
|
68
|
+
name: "testproject",
|
|
69
|
+
root: repoRoot,
|
|
70
|
+
canonicalBranch: "main",
|
|
71
|
+
},
|
|
72
|
+
agents: {
|
|
73
|
+
manifestPath: join(overstoryDir, "agent-manifest.json"),
|
|
74
|
+
baseDir: join(repoRoot, "agents"),
|
|
75
|
+
maxConcurrent: 5,
|
|
76
|
+
staggerDelayMs: 100,
|
|
77
|
+
maxDepth: 2,
|
|
78
|
+
maxSessionsPerRun: 0,
|
|
79
|
+
},
|
|
80
|
+
worktrees: {
|
|
81
|
+
baseDir: join(overstoryDir, "worktrees"),
|
|
82
|
+
},
|
|
83
|
+
taskTracker: {
|
|
84
|
+
backend: "auto",
|
|
85
|
+
enabled: false,
|
|
86
|
+
},
|
|
87
|
+
mulch: {
|
|
88
|
+
enabled: false,
|
|
89
|
+
domains: [],
|
|
90
|
+
primeFormat: "markdown",
|
|
91
|
+
},
|
|
92
|
+
merge: {
|
|
93
|
+
aiResolveEnabled: false,
|
|
94
|
+
reimagineEnabled: false,
|
|
95
|
+
},
|
|
96
|
+
providers: {
|
|
97
|
+
anthropic: { type: "native" },
|
|
98
|
+
},
|
|
99
|
+
watchdog: {
|
|
100
|
+
tier0Enabled: false,
|
|
101
|
+
tier0IntervalMs: 30000,
|
|
102
|
+
tier1Enabled: false,
|
|
103
|
+
tier2Enabled: false,
|
|
104
|
+
staleThresholdMs: 60000,
|
|
105
|
+
zombieThresholdMs: 300000,
|
|
106
|
+
nudgeIntervalMs: 30000,
|
|
107
|
+
},
|
|
108
|
+
models: {},
|
|
109
|
+
logging: {
|
|
110
|
+
verbose: false,
|
|
111
|
+
redactSecrets: true,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Reset mocks and create deps object
|
|
116
|
+
mockListSessions.mockReset();
|
|
117
|
+
mockIsProcessAlive.mockReset();
|
|
118
|
+
mockListSessions.mockResolvedValue([]);
|
|
119
|
+
mockIsProcessAlive.mockReturnValue(true);
|
|
120
|
+
|
|
121
|
+
mockDeps = {
|
|
122
|
+
listSessions: mockListSessions,
|
|
123
|
+
isProcessAlive: mockIsProcessAlive,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
if (existsSync(repoRoot)) {
|
|
129
|
+
rmSync(repoRoot, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("returns all pass when no sessions exist", async () => {
|
|
134
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
135
|
+
|
|
136
|
+
expect(checks.length).toBeGreaterThan(0);
|
|
137
|
+
const passChecks = checks.filter((c) => c.status === "pass");
|
|
138
|
+
expect(passChecks.length).toBeGreaterThan(0);
|
|
139
|
+
|
|
140
|
+
const failChecks = checks.filter((c) => c.status === "fail");
|
|
141
|
+
expect(failChecks.length).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("detects orphaned worktrees", async () => {
|
|
145
|
+
// Create a worktree but don't add it to SessionStore
|
|
146
|
+
const worktreePath = join(overstoryDir, "worktrees", "orphan-agent");
|
|
147
|
+
createWorktree(repoRoot, worktreePath, "overstory/orphan-agent/test-123");
|
|
148
|
+
|
|
149
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
150
|
+
|
|
151
|
+
const orphanCheck = checks.find((c) => c.name === "orphaned-worktrees");
|
|
152
|
+
expect(orphanCheck).toBeDefined();
|
|
153
|
+
expect(orphanCheck?.status).toBe("warn");
|
|
154
|
+
expect(orphanCheck?.message).toContain("1 orphaned worktree");
|
|
155
|
+
expect(orphanCheck?.details?.length).toBe(1);
|
|
156
|
+
expect(orphanCheck?.fixable).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("detects orphaned tmux sessions", async () => {
|
|
160
|
+
// Mock a tmux session that isn't in SessionStore
|
|
161
|
+
mockListSessions.mockResolvedValue([{ name: "overstory-testproject-orphan", pid: 9999 }]);
|
|
162
|
+
|
|
163
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
164
|
+
|
|
165
|
+
const orphanCheck = checks.find((c) => c.name === "orphaned-tmux");
|
|
166
|
+
expect(orphanCheck).toBeDefined();
|
|
167
|
+
expect(orphanCheck?.status).toBe("warn");
|
|
168
|
+
expect(orphanCheck?.message).toContain("1 orphaned tmux session");
|
|
169
|
+
expect(orphanCheck?.fixable).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("ignores tmux sessions from other projects", async () => {
|
|
173
|
+
// Mock tmux sessions from different projects
|
|
174
|
+
mockListSessions.mockResolvedValue([
|
|
175
|
+
{ name: "overstory-otherproject-agent1", pid: 9999 },
|
|
176
|
+
{ name: "my-custom-session", pid: 8888 },
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
180
|
+
|
|
181
|
+
const orphanCheck = checks.find((c) => c.name === "orphaned-tmux");
|
|
182
|
+
expect(orphanCheck).toBeDefined();
|
|
183
|
+
expect(orphanCheck?.status).toBe("pass");
|
|
184
|
+
expect(orphanCheck?.message).toContain("No orphaned tmux sessions");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("detects dead PIDs in SessionStore", async () => {
|
|
188
|
+
// Create a session with a PID that's marked as dead
|
|
189
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
190
|
+
const store = createSessionStore(dbPath);
|
|
191
|
+
|
|
192
|
+
store.upsert({
|
|
193
|
+
id: "session-1",
|
|
194
|
+
agentName: "dead-agent",
|
|
195
|
+
capability: "builder",
|
|
196
|
+
worktreePath: join(overstoryDir, "worktrees", "dead-agent"),
|
|
197
|
+
branchName: "overstory/dead-agent/test-123",
|
|
198
|
+
beadId: "test-123",
|
|
199
|
+
tmuxSession: "overstory-testproject-dead-agent",
|
|
200
|
+
state: "working",
|
|
201
|
+
pid: 99999, // Non-existent PID
|
|
202
|
+
parentAgent: null,
|
|
203
|
+
depth: 0,
|
|
204
|
+
runId: null,
|
|
205
|
+
startedAt: new Date().toISOString(),
|
|
206
|
+
lastActivity: new Date().toISOString(),
|
|
207
|
+
escalationLevel: 0,
|
|
208
|
+
stalledSince: null,
|
|
209
|
+
});
|
|
210
|
+
store.close();
|
|
211
|
+
|
|
212
|
+
// Mock that this PID is not alive
|
|
213
|
+
mockIsProcessAlive.mockReturnValue(false);
|
|
214
|
+
|
|
215
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
216
|
+
|
|
217
|
+
const deadPidCheck = checks.find((c) => c.name === "dead-pids");
|
|
218
|
+
expect(deadPidCheck).toBeDefined();
|
|
219
|
+
expect(deadPidCheck?.status).toBe("warn");
|
|
220
|
+
expect(deadPidCheck?.message).toContain("1 session(s) with dead PIDs");
|
|
221
|
+
expect(deadPidCheck?.fixable).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("passes when all PIDs are alive", async () => {
|
|
225
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
226
|
+
const store = createSessionStore(dbPath);
|
|
227
|
+
|
|
228
|
+
store.upsert({
|
|
229
|
+
id: "session-1",
|
|
230
|
+
agentName: "live-agent",
|
|
231
|
+
capability: "builder",
|
|
232
|
+
worktreePath: join(overstoryDir, "worktrees", "live-agent"),
|
|
233
|
+
branchName: "overstory/live-agent/test-123",
|
|
234
|
+
beadId: "test-123",
|
|
235
|
+
tmuxSession: "overstory-testproject-live-agent",
|
|
236
|
+
state: "working",
|
|
237
|
+
pid: 12345,
|
|
238
|
+
parentAgent: null,
|
|
239
|
+
depth: 0,
|
|
240
|
+
runId: null,
|
|
241
|
+
startedAt: new Date().toISOString(),
|
|
242
|
+
lastActivity: new Date().toISOString(),
|
|
243
|
+
escalationLevel: 0,
|
|
244
|
+
stalledSince: null,
|
|
245
|
+
});
|
|
246
|
+
store.close();
|
|
247
|
+
|
|
248
|
+
// Mock that this PID is alive
|
|
249
|
+
mockIsProcessAlive.mockReturnValue(true);
|
|
250
|
+
|
|
251
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
252
|
+
|
|
253
|
+
const deadPidCheck = checks.find((c) => c.name === "dead-pids");
|
|
254
|
+
expect(deadPidCheck).toBeDefined();
|
|
255
|
+
expect(deadPidCheck?.status).toBe("pass");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("detects missing worktrees for SessionStore entries", async () => {
|
|
259
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
260
|
+
const store = createSessionStore(dbPath);
|
|
261
|
+
|
|
262
|
+
const missingWorktreePath = join(overstoryDir, "worktrees", "missing-agent");
|
|
263
|
+
store.upsert({
|
|
264
|
+
id: "session-1",
|
|
265
|
+
agentName: "missing-agent",
|
|
266
|
+
capability: "builder",
|
|
267
|
+
worktreePath: missingWorktreePath,
|
|
268
|
+
branchName: "overstory/missing-agent/test-123",
|
|
269
|
+
beadId: "test-123",
|
|
270
|
+
tmuxSession: "overstory-testproject-missing-agent",
|
|
271
|
+
state: "working",
|
|
272
|
+
pid: null,
|
|
273
|
+
parentAgent: null,
|
|
274
|
+
depth: 0,
|
|
275
|
+
runId: null,
|
|
276
|
+
startedAt: new Date().toISOString(),
|
|
277
|
+
lastActivity: new Date().toISOString(),
|
|
278
|
+
escalationLevel: 0,
|
|
279
|
+
stalledSince: null,
|
|
280
|
+
});
|
|
281
|
+
store.close();
|
|
282
|
+
|
|
283
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
284
|
+
|
|
285
|
+
const missingCheck = checks.find((c) => c.name === "missing-worktrees");
|
|
286
|
+
expect(missingCheck).toBeDefined();
|
|
287
|
+
expect(missingCheck?.status).toBe("warn");
|
|
288
|
+
expect(missingCheck?.message).toContain("1 session(s) with missing worktrees");
|
|
289
|
+
expect(missingCheck?.fixable).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("detects missing tmux sessions for SessionStore entries", async () => {
|
|
293
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
294
|
+
const store = createSessionStore(dbPath);
|
|
295
|
+
|
|
296
|
+
const worktreePath = join(overstoryDir, "worktrees", "agent-without-tmux");
|
|
297
|
+
createWorktree(repoRoot, worktreePath, "overstory/agent-without-tmux/test-123");
|
|
298
|
+
|
|
299
|
+
store.upsert({
|
|
300
|
+
id: "session-1",
|
|
301
|
+
agentName: "agent-without-tmux",
|
|
302
|
+
capability: "builder",
|
|
303
|
+
worktreePath,
|
|
304
|
+
branchName: "overstory/agent-without-tmux/test-123",
|
|
305
|
+
beadId: "test-123",
|
|
306
|
+
tmuxSession: "overstory-testproject-agent-without-tmux",
|
|
307
|
+
state: "working",
|
|
308
|
+
pid: null,
|
|
309
|
+
parentAgent: null,
|
|
310
|
+
depth: 0,
|
|
311
|
+
runId: null,
|
|
312
|
+
startedAt: new Date().toISOString(),
|
|
313
|
+
lastActivity: new Date().toISOString(),
|
|
314
|
+
escalationLevel: 0,
|
|
315
|
+
stalledSince: null,
|
|
316
|
+
});
|
|
317
|
+
store.close();
|
|
318
|
+
|
|
319
|
+
// Mock empty tmux sessions list
|
|
320
|
+
mockListSessions.mockResolvedValue([]);
|
|
321
|
+
|
|
322
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
323
|
+
|
|
324
|
+
const missingCheck = checks.find((c) => c.name === "missing-tmux");
|
|
325
|
+
expect(missingCheck).toBeDefined();
|
|
326
|
+
expect(missingCheck?.status).toBe("warn");
|
|
327
|
+
expect(missingCheck?.message).toContain("1 session(s) with missing tmux sessions");
|
|
328
|
+
expect(missingCheck?.fixable).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("passes when everything is consistent", async () => {
|
|
332
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
333
|
+
const store = createSessionStore(dbPath);
|
|
334
|
+
|
|
335
|
+
const worktreePath = join(overstoryDir, "worktrees", "consistent-agent");
|
|
336
|
+
createWorktree(repoRoot, worktreePath, "overstory/consistent-agent/test-123");
|
|
337
|
+
|
|
338
|
+
store.upsert({
|
|
339
|
+
id: "session-1",
|
|
340
|
+
agentName: "consistent-agent",
|
|
341
|
+
capability: "builder",
|
|
342
|
+
worktreePath,
|
|
343
|
+
branchName: "overstory/consistent-agent/test-123",
|
|
344
|
+
beadId: "test-123",
|
|
345
|
+
tmuxSession: "overstory-testproject-consistent-agent",
|
|
346
|
+
state: "working",
|
|
347
|
+
pid: 12345,
|
|
348
|
+
parentAgent: null,
|
|
349
|
+
depth: 0,
|
|
350
|
+
runId: null,
|
|
351
|
+
startedAt: new Date().toISOString(),
|
|
352
|
+
lastActivity: new Date().toISOString(),
|
|
353
|
+
escalationLevel: 0,
|
|
354
|
+
stalledSince: null,
|
|
355
|
+
});
|
|
356
|
+
store.close();
|
|
357
|
+
|
|
358
|
+
// Mock matching tmux session
|
|
359
|
+
mockListSessions.mockResolvedValue([
|
|
360
|
+
{ name: "overstory-testproject-consistent-agent", pid: 12345 },
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
// Mock PID as alive
|
|
364
|
+
mockIsProcessAlive.mockReturnValue(true);
|
|
365
|
+
|
|
366
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
367
|
+
|
|
368
|
+
const warnOrFail = checks.filter((c) => c.status === "warn" || c.status === "fail");
|
|
369
|
+
expect(warnOrFail.length).toBe(0);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("handles tmux not installed gracefully", async () => {
|
|
373
|
+
// Mock tmux listing to throw an error
|
|
374
|
+
mockListSessions.mockRejectedValue(new Error("tmux: command not found"));
|
|
375
|
+
|
|
376
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
377
|
+
|
|
378
|
+
const tmuxCheck = checks.find((c) => c.name === "tmux-listing");
|
|
379
|
+
expect(tmuxCheck).toBeDefined();
|
|
380
|
+
expect(tmuxCheck?.status).toBe("warn");
|
|
381
|
+
expect(tmuxCheck?.message).toContain("Failed to list tmux sessions");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("fails early if git worktree list fails", async () => {
|
|
385
|
+
// Use a non-existent repo root to trigger worktree listing failure
|
|
386
|
+
const badConfig = { ...config, project: { ...config.project, root: "/nonexistent" } };
|
|
387
|
+
|
|
388
|
+
const checks = await checkConsistency(badConfig, overstoryDir, mockDeps);
|
|
389
|
+
|
|
390
|
+
expect(checks.length).toBe(1);
|
|
391
|
+
expect(checks[0]?.name).toBe("worktree-listing");
|
|
392
|
+
expect(checks[0]?.status).toBe("fail");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("fails early if SessionStore cannot be opened", async () => {
|
|
396
|
+
// Use a bad overstory directory path
|
|
397
|
+
const badOverstoryDir = "/nonexistent/.overstory";
|
|
398
|
+
|
|
399
|
+
const checks = await checkConsistency(config, badOverstoryDir, mockDeps);
|
|
400
|
+
|
|
401
|
+
const storeCheck = checks.find((c) => c.name === "sessionstore-open");
|
|
402
|
+
expect(storeCheck).toBeDefined();
|
|
403
|
+
expect(storeCheck?.status).toBe("fail");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("reviewer-coverage: leads without reviewers emits warn", async () => {
|
|
407
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
408
|
+
const store = createSessionStore(dbPath);
|
|
409
|
+
|
|
410
|
+
// Add 2 builder sessions under lead-1, no reviewers
|
|
411
|
+
store.upsert({
|
|
412
|
+
id: "session-1",
|
|
413
|
+
agentName: "builder-1",
|
|
414
|
+
capability: "builder",
|
|
415
|
+
worktreePath: join(overstoryDir, "worktrees", "builder-1"),
|
|
416
|
+
branchName: "overstory/builder-1/test-123",
|
|
417
|
+
beadId: "test-123",
|
|
418
|
+
tmuxSession: "overstory-testproject-builder-1",
|
|
419
|
+
state: "working",
|
|
420
|
+
pid: null,
|
|
421
|
+
parentAgent: "lead-1",
|
|
422
|
+
depth: 1,
|
|
423
|
+
runId: null,
|
|
424
|
+
startedAt: new Date().toISOString(),
|
|
425
|
+
lastActivity: new Date().toISOString(),
|
|
426
|
+
escalationLevel: 0,
|
|
427
|
+
stalledSince: null,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
store.upsert({
|
|
431
|
+
id: "session-2",
|
|
432
|
+
agentName: "builder-2",
|
|
433
|
+
capability: "builder",
|
|
434
|
+
worktreePath: join(overstoryDir, "worktrees", "builder-2"),
|
|
435
|
+
branchName: "overstory/builder-2/test-456",
|
|
436
|
+
beadId: "test-456",
|
|
437
|
+
tmuxSession: "overstory-testproject-builder-2",
|
|
438
|
+
state: "working",
|
|
439
|
+
pid: null,
|
|
440
|
+
parentAgent: "lead-1",
|
|
441
|
+
depth: 1,
|
|
442
|
+
runId: null,
|
|
443
|
+
startedAt: new Date().toISOString(),
|
|
444
|
+
lastActivity: new Date().toISOString(),
|
|
445
|
+
escalationLevel: 0,
|
|
446
|
+
stalledSince: null,
|
|
447
|
+
});
|
|
448
|
+
store.close();
|
|
449
|
+
|
|
450
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
451
|
+
|
|
452
|
+
const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
|
|
453
|
+
expect(reviewerCheck).toBeDefined();
|
|
454
|
+
expect(reviewerCheck?.status).toBe("warn");
|
|
455
|
+
expect(reviewerCheck?.message).toContain("without any reviewers");
|
|
456
|
+
expect(reviewerCheck?.details).toBeDefined();
|
|
457
|
+
expect(reviewerCheck?.details?.length).toBeGreaterThan(0);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("reviewer-coverage: partial reviewer coverage emits warn", async () => {
|
|
461
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
462
|
+
const store = createSessionStore(dbPath);
|
|
463
|
+
|
|
464
|
+
// Add 3 builders and 1 reviewer under same parent
|
|
465
|
+
for (let i = 1; i <= 3; i++) {
|
|
466
|
+
store.upsert({
|
|
467
|
+
id: `session-builder-${i}`,
|
|
468
|
+
agentName: `builder-${i}`,
|
|
469
|
+
capability: "builder",
|
|
470
|
+
worktreePath: join(overstoryDir, "worktrees", `builder-${i}`),
|
|
471
|
+
branchName: `overstory/builder-${i}/test-${i}`,
|
|
472
|
+
beadId: `test-${i}`,
|
|
473
|
+
tmuxSession: `overstory-testproject-builder-${i}`,
|
|
474
|
+
state: "working",
|
|
475
|
+
pid: null,
|
|
476
|
+
parentAgent: "lead-1",
|
|
477
|
+
depth: 1,
|
|
478
|
+
runId: null,
|
|
479
|
+
startedAt: new Date().toISOString(),
|
|
480
|
+
lastActivity: new Date().toISOString(),
|
|
481
|
+
escalationLevel: 0,
|
|
482
|
+
stalledSince: null,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
store.upsert({
|
|
487
|
+
id: "session-reviewer-1",
|
|
488
|
+
agentName: "reviewer-1",
|
|
489
|
+
capability: "reviewer",
|
|
490
|
+
worktreePath: join(overstoryDir, "worktrees", "reviewer-1"),
|
|
491
|
+
branchName: "overstory/reviewer-1/test-r1",
|
|
492
|
+
beadId: "test-r1",
|
|
493
|
+
tmuxSession: "overstory-testproject-reviewer-1",
|
|
494
|
+
state: "working",
|
|
495
|
+
pid: null,
|
|
496
|
+
parentAgent: "lead-1",
|
|
497
|
+
depth: 1,
|
|
498
|
+
runId: null,
|
|
499
|
+
startedAt: new Date().toISOString(),
|
|
500
|
+
lastActivity: new Date().toISOString(),
|
|
501
|
+
escalationLevel: 0,
|
|
502
|
+
stalledSince: null,
|
|
503
|
+
});
|
|
504
|
+
store.close();
|
|
505
|
+
|
|
506
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
507
|
+
|
|
508
|
+
const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
|
|
509
|
+
expect(reviewerCheck).toBeDefined();
|
|
510
|
+
expect(reviewerCheck?.status).toBe("warn");
|
|
511
|
+
expect(reviewerCheck?.message).toContain("partial reviewer coverage");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("reviewer-coverage: full reviewer coverage emits pass", async () => {
|
|
515
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
516
|
+
const store = createSessionStore(dbPath);
|
|
517
|
+
|
|
518
|
+
// Add 2 builders and 2 reviewers under same parent
|
|
519
|
+
for (let i = 1; i <= 2; i++) {
|
|
520
|
+
store.upsert({
|
|
521
|
+
id: `session-builder-${i}`,
|
|
522
|
+
agentName: `builder-${i}`,
|
|
523
|
+
capability: "builder",
|
|
524
|
+
worktreePath: join(overstoryDir, "worktrees", `builder-${i}`),
|
|
525
|
+
branchName: `overstory/builder-${i}/test-${i}`,
|
|
526
|
+
beadId: `test-${i}`,
|
|
527
|
+
tmuxSession: `overstory-testproject-builder-${i}`,
|
|
528
|
+
state: "working",
|
|
529
|
+
pid: null,
|
|
530
|
+
parentAgent: "lead-1",
|
|
531
|
+
depth: 1,
|
|
532
|
+
runId: null,
|
|
533
|
+
startedAt: new Date().toISOString(),
|
|
534
|
+
lastActivity: new Date().toISOString(),
|
|
535
|
+
escalationLevel: 0,
|
|
536
|
+
stalledSince: null,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
store.upsert({
|
|
540
|
+
id: `session-reviewer-${i}`,
|
|
541
|
+
agentName: `reviewer-${i}`,
|
|
542
|
+
capability: "reviewer",
|
|
543
|
+
worktreePath: join(overstoryDir, "worktrees", `reviewer-${i}`),
|
|
544
|
+
branchName: `overstory/reviewer-${i}/test-r${i}`,
|
|
545
|
+
beadId: `test-r${i}`,
|
|
546
|
+
tmuxSession: `overstory-testproject-reviewer-${i}`,
|
|
547
|
+
state: "working",
|
|
548
|
+
pid: null,
|
|
549
|
+
parentAgent: "lead-1",
|
|
550
|
+
depth: 1,
|
|
551
|
+
runId: null,
|
|
552
|
+
startedAt: new Date().toISOString(),
|
|
553
|
+
lastActivity: new Date().toISOString(),
|
|
554
|
+
escalationLevel: 0,
|
|
555
|
+
stalledSince: null,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
store.close();
|
|
559
|
+
|
|
560
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
561
|
+
|
|
562
|
+
const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
|
|
563
|
+
expect(reviewerCheck).toBeDefined();
|
|
564
|
+
expect(reviewerCheck?.status).toBe("pass");
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("reviewer-coverage: no builder sessions emits pass", async () => {
|
|
568
|
+
// Don't create any sessions at all
|
|
569
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
570
|
+
|
|
571
|
+
const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
|
|
572
|
+
expect(reviewerCheck).toBeDefined();
|
|
573
|
+
expect(reviewerCheck?.status).toBe("pass");
|
|
574
|
+
expect(reviewerCheck?.message).toContain("No builder sessions found");
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("reviewer-coverage: multiple leads mixed coverage", async () => {
|
|
578
|
+
const dbPath = join(overstoryDir, "sessions.db");
|
|
579
|
+
const store = createSessionStore(dbPath);
|
|
580
|
+
|
|
581
|
+
// Lead-1 has builders + reviewers (good)
|
|
582
|
+
store.upsert({
|
|
583
|
+
id: "session-builder-1",
|
|
584
|
+
agentName: "builder-1",
|
|
585
|
+
capability: "builder",
|
|
586
|
+
worktreePath: join(overstoryDir, "worktrees", "builder-1"),
|
|
587
|
+
branchName: "overstory/builder-1/test-1",
|
|
588
|
+
beadId: "test-1",
|
|
589
|
+
tmuxSession: "overstory-testproject-builder-1",
|
|
590
|
+
state: "working",
|
|
591
|
+
pid: null,
|
|
592
|
+
parentAgent: "lead-1",
|
|
593
|
+
depth: 1,
|
|
594
|
+
runId: null,
|
|
595
|
+
startedAt: new Date().toISOString(),
|
|
596
|
+
lastActivity: new Date().toISOString(),
|
|
597
|
+
escalationLevel: 0,
|
|
598
|
+
stalledSince: null,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
store.upsert({
|
|
602
|
+
id: "session-reviewer-1",
|
|
603
|
+
agentName: "reviewer-1",
|
|
604
|
+
capability: "reviewer",
|
|
605
|
+
worktreePath: join(overstoryDir, "worktrees", "reviewer-1"),
|
|
606
|
+
branchName: "overstory/reviewer-1/test-r1",
|
|
607
|
+
beadId: "test-r1",
|
|
608
|
+
tmuxSession: "overstory-testproject-reviewer-1",
|
|
609
|
+
state: "working",
|
|
610
|
+
pid: null,
|
|
611
|
+
parentAgent: "lead-1",
|
|
612
|
+
depth: 1,
|
|
613
|
+
runId: null,
|
|
614
|
+
startedAt: new Date().toISOString(),
|
|
615
|
+
lastActivity: new Date().toISOString(),
|
|
616
|
+
escalationLevel: 0,
|
|
617
|
+
stalledSince: null,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Lead-2 has builders only (bad)
|
|
621
|
+
store.upsert({
|
|
622
|
+
id: "session-builder-2",
|
|
623
|
+
agentName: "builder-2",
|
|
624
|
+
capability: "builder",
|
|
625
|
+
worktreePath: join(overstoryDir, "worktrees", "builder-2"),
|
|
626
|
+
branchName: "overstory/builder-2/test-2",
|
|
627
|
+
beadId: "test-2",
|
|
628
|
+
tmuxSession: "overstory-testproject-builder-2",
|
|
629
|
+
state: "working",
|
|
630
|
+
pid: null,
|
|
631
|
+
parentAgent: "lead-2",
|
|
632
|
+
depth: 1,
|
|
633
|
+
runId: null,
|
|
634
|
+
startedAt: new Date().toISOString(),
|
|
635
|
+
lastActivity: new Date().toISOString(),
|
|
636
|
+
escalationLevel: 0,
|
|
637
|
+
stalledSince: null,
|
|
638
|
+
});
|
|
639
|
+
store.close();
|
|
640
|
+
|
|
641
|
+
const checks = await checkConsistency(config, overstoryDir, mockDeps);
|
|
642
|
+
|
|
643
|
+
const reviewerCheck = checks.find((c) => c.name === "reviewer-coverage");
|
|
644
|
+
expect(reviewerCheck).toBeDefined();
|
|
645
|
+
expect(reviewerCheck?.status).toBe("warn");
|
|
646
|
+
expect(reviewerCheck?.details).toBeDefined();
|
|
647
|
+
// Should contain lead-2 in the details
|
|
648
|
+
const detailsStr = reviewerCheck?.details?.join(" ");
|
|
649
|
+
expect(detailsStr).toContain("lead-2");
|
|
650
|
+
});
|
|
651
|
+
});
|