@os-eco/overstory-cli 0.7.9 → 0.8.2
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 +16 -7
- package/agents/coordinator.md +41 -0
- package/agents/orchestrator.md +239 -0
- package/package.json +1 -1
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/commands/coordinator.test.ts +334 -0
- package/src/commands/coordinator.ts +366 -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/init.test.ts +2 -1
- package/src/commands/init.ts +2 -2
- 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.ts +218 -121
- package/src/commands/status.test.ts +77 -0
- package/src/commands/status.ts +6 -3
- package/src/commands/stop.test.ts +134 -0
- package/src/commands/stop.ts +41 -11
- package/src/commands/trace.test.ts +8 -0
- package/src/commands/update.test.ts +465 -0
- package/src/commands/update.ts +263 -0
- package/src/config.test.ts +65 -1
- package/src/config.ts +23 -0
- package/src/e2e/init-sling-lifecycle.test.ts +3 -2
- package/src/index.ts +21 -2
- package/src/logging/theme.ts +4 -0
- 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 +5 -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
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
|
|
5
|
+
import type { Spawner } from "./init.ts";
|
|
6
|
+
import {
|
|
7
|
+
buildAgentManifest,
|
|
8
|
+
buildHooksJson,
|
|
9
|
+
initCommand,
|
|
10
|
+
OVERSTORY_GITIGNORE,
|
|
11
|
+
OVERSTORY_README,
|
|
12
|
+
} from "./init.ts";
|
|
13
|
+
import { executeUpdate } from "./update.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tests for `ov update` — refresh .overstory/ managed files.
|
|
17
|
+
*
|
|
18
|
+
* Uses real temp git repos. Suppresses stdout to keep test output clean.
|
|
19
|
+
* Requires a pre-initialized .overstory/ directory (via initCommand).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** No-op spawner that treats all ecosystem tools as "not installed". */
|
|
23
|
+
const noopSpawner: Spawner = async () => ({ exitCode: 1, stdout: "", stderr: "not found" });
|
|
24
|
+
|
|
25
|
+
const AGENT_DEF_FILES = [
|
|
26
|
+
"builder.md",
|
|
27
|
+
"coordinator.md",
|
|
28
|
+
"lead.md",
|
|
29
|
+
"merger.md",
|
|
30
|
+
"monitor.md",
|
|
31
|
+
"orchestrator.md",
|
|
32
|
+
"reviewer.md",
|
|
33
|
+
"scout.md",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** Resolve the source agents directory (same logic as init.ts). */
|
|
37
|
+
const SOURCE_AGENTS_DIR = join(import.meta.dir, "..", "..", "agents");
|
|
38
|
+
|
|
39
|
+
describe("executeUpdate: not initialized", () => {
|
|
40
|
+
let tempDir: string;
|
|
41
|
+
let originalCwd: string;
|
|
42
|
+
let originalWrite: typeof process.stdout.write;
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
tempDir = await createTempGitRepo();
|
|
46
|
+
originalCwd = process.cwd();
|
|
47
|
+
process.chdir(tempDir);
|
|
48
|
+
|
|
49
|
+
originalWrite = process.stdout.write;
|
|
50
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
process.chdir(originalCwd);
|
|
55
|
+
process.stdout.write = originalWrite;
|
|
56
|
+
await cleanupTempDir(tempDir);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("errors when .overstory/config.yaml does not exist", async () => {
|
|
60
|
+
await expect(executeUpdate({})).rejects.toThrow("Not initialized");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("error message hints to run ov init", async () => {
|
|
64
|
+
try {
|
|
65
|
+
await executeUpdate({});
|
|
66
|
+
expect.unreachable("Should have thrown");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
expect((err as Error).message).toContain("ov init");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("executeUpdate: refresh all (no flags)", () => {
|
|
74
|
+
let tempDir: string;
|
|
75
|
+
let originalCwd: string;
|
|
76
|
+
let originalWrite: typeof process.stdout.write;
|
|
77
|
+
|
|
78
|
+
beforeEach(async () => {
|
|
79
|
+
tempDir = await createTempGitRepo();
|
|
80
|
+
originalCwd = process.cwd();
|
|
81
|
+
process.chdir(tempDir);
|
|
82
|
+
|
|
83
|
+
originalWrite = process.stdout.write;
|
|
84
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
85
|
+
|
|
86
|
+
// Initialize .overstory/
|
|
87
|
+
await initCommand({ _spawner: noopSpawner });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(async () => {
|
|
91
|
+
process.chdir(originalCwd);
|
|
92
|
+
process.stdout.write = originalWrite;
|
|
93
|
+
await cleanupTempDir(tempDir);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("refreshes all managed files when no flags given", async () => {
|
|
97
|
+
// Tamper with agent defs
|
|
98
|
+
const scoutPath = join(tempDir, ".overstory", "agent-defs", "scout.md");
|
|
99
|
+
await Bun.write(scoutPath, "# tampered\n");
|
|
100
|
+
|
|
101
|
+
// Tamper with manifest
|
|
102
|
+
await Bun.write(join(tempDir, ".overstory", "agent-manifest.json"), "{}");
|
|
103
|
+
|
|
104
|
+
// Tamper with hooks
|
|
105
|
+
await Bun.write(join(tempDir, ".overstory", "hooks.json"), "{}");
|
|
106
|
+
|
|
107
|
+
// Tamper with gitignore
|
|
108
|
+
await Bun.write(join(tempDir, ".overstory", ".gitignore"), "# old\n");
|
|
109
|
+
|
|
110
|
+
// Tamper with readme
|
|
111
|
+
await Bun.write(join(tempDir, ".overstory", "README.md"), "# old\n");
|
|
112
|
+
|
|
113
|
+
await executeUpdate({});
|
|
114
|
+
|
|
115
|
+
// Verify all files restored
|
|
116
|
+
const scoutContent = await Bun.file(scoutPath).text();
|
|
117
|
+
const sourceScout = await Bun.file(join(SOURCE_AGENTS_DIR, "scout.md")).text();
|
|
118
|
+
expect(scoutContent).toBe(sourceScout);
|
|
119
|
+
|
|
120
|
+
const manifestContent = await Bun.file(
|
|
121
|
+
join(tempDir, ".overstory", "agent-manifest.json"),
|
|
122
|
+
).text();
|
|
123
|
+
const expectedManifest = `${JSON.stringify(buildAgentManifest(), null, "\t")}\n`;
|
|
124
|
+
expect(manifestContent).toBe(expectedManifest);
|
|
125
|
+
|
|
126
|
+
const hooksContent = await Bun.file(join(tempDir, ".overstory", "hooks.json")).text();
|
|
127
|
+
expect(hooksContent).toBe(buildHooksJson());
|
|
128
|
+
|
|
129
|
+
const gitignoreContent = await Bun.file(join(tempDir, ".overstory", ".gitignore")).text();
|
|
130
|
+
expect(gitignoreContent).toBe(OVERSTORY_GITIGNORE);
|
|
131
|
+
|
|
132
|
+
const readmeContent = await Bun.file(join(tempDir, ".overstory", "README.md")).text();
|
|
133
|
+
expect(readmeContent).toBe(OVERSTORY_README);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("does not touch config.yaml", async () => {
|
|
137
|
+
const configPath = join(tempDir, ".overstory", "config.yaml");
|
|
138
|
+
const originalConfig = await Bun.file(configPath).text();
|
|
139
|
+
|
|
140
|
+
await executeUpdate({});
|
|
141
|
+
|
|
142
|
+
const afterUpdate = await Bun.file(configPath).text();
|
|
143
|
+
expect(afterUpdate).toBe(originalConfig);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("does not touch databases", async () => {
|
|
147
|
+
// Create fake database files
|
|
148
|
+
const mailDbPath = join(tempDir, ".overstory", "mail.db");
|
|
149
|
+
const sessionsDbPath = join(tempDir, ".overstory", "sessions.db");
|
|
150
|
+
await Bun.write(mailDbPath, "fake-mail-db");
|
|
151
|
+
await Bun.write(sessionsDbPath, "fake-sessions-db");
|
|
152
|
+
|
|
153
|
+
await executeUpdate({});
|
|
154
|
+
|
|
155
|
+
const mailDb = await Bun.file(mailDbPath).text();
|
|
156
|
+
expect(mailDb).toBe("fake-mail-db");
|
|
157
|
+
|
|
158
|
+
const sessionsDb = await Bun.file(sessionsDbPath).text();
|
|
159
|
+
expect(sessionsDb).toBe("fake-sessions-db");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("handles already-up-to-date files gracefully (idempotent)", async () => {
|
|
163
|
+
// Run update twice — second should report nothing changed
|
|
164
|
+
await executeUpdate({});
|
|
165
|
+
|
|
166
|
+
// Capture JSON output of second run
|
|
167
|
+
let captured = "";
|
|
168
|
+
const restoreWrite = process.stdout.write;
|
|
169
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
170
|
+
captured += String(chunk);
|
|
171
|
+
return true;
|
|
172
|
+
}) as typeof process.stdout.write;
|
|
173
|
+
|
|
174
|
+
await executeUpdate({ json: true });
|
|
175
|
+
|
|
176
|
+
process.stdout.write = restoreWrite;
|
|
177
|
+
|
|
178
|
+
const parsed = JSON.parse(captured.trim()) as Record<string, unknown>;
|
|
179
|
+
expect(parsed.success).toBe(true);
|
|
180
|
+
|
|
181
|
+
const agentDefs = parsed.agentDefs as { updated: string[]; unchanged: string[] };
|
|
182
|
+
expect(agentDefs.updated).toHaveLength(0);
|
|
183
|
+
expect(agentDefs.unchanged.length).toBeGreaterThan(0);
|
|
184
|
+
|
|
185
|
+
expect(parsed.manifest).toEqual({ updated: false });
|
|
186
|
+
expect(parsed.hooks).toEqual({ updated: false });
|
|
187
|
+
expect(parsed.gitignore).toEqual({ updated: false });
|
|
188
|
+
expect(parsed.readme).toEqual({ updated: false });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("executeUpdate: granular flags", () => {
|
|
193
|
+
let tempDir: string;
|
|
194
|
+
let originalCwd: string;
|
|
195
|
+
let originalWrite: typeof process.stdout.write;
|
|
196
|
+
|
|
197
|
+
beforeEach(async () => {
|
|
198
|
+
tempDir = await createTempGitRepo();
|
|
199
|
+
originalCwd = process.cwd();
|
|
200
|
+
process.chdir(tempDir);
|
|
201
|
+
|
|
202
|
+
originalWrite = process.stdout.write;
|
|
203
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
204
|
+
|
|
205
|
+
await initCommand({ _spawner: noopSpawner });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
afterEach(async () => {
|
|
209
|
+
process.chdir(originalCwd);
|
|
210
|
+
process.stdout.write = originalWrite;
|
|
211
|
+
await cleanupTempDir(tempDir);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("--agents only refreshes agent-defs", async () => {
|
|
215
|
+
// Tamper with agent def and manifest
|
|
216
|
+
await Bun.write(join(tempDir, ".overstory", "agent-defs", "scout.md"), "# tampered\n");
|
|
217
|
+
await Bun.write(join(tempDir, ".overstory", "agent-manifest.json"), "{}");
|
|
218
|
+
await Bun.write(join(tempDir, ".overstory", "hooks.json"), "{}");
|
|
219
|
+
|
|
220
|
+
await executeUpdate({ agents: true });
|
|
221
|
+
|
|
222
|
+
// Agent def should be restored
|
|
223
|
+
const scoutContent = await Bun.file(
|
|
224
|
+
join(tempDir, ".overstory", "agent-defs", "scout.md"),
|
|
225
|
+
).text();
|
|
226
|
+
const sourceScout = await Bun.file(join(SOURCE_AGENTS_DIR, "scout.md")).text();
|
|
227
|
+
expect(scoutContent).toBe(sourceScout);
|
|
228
|
+
|
|
229
|
+
// Manifest should NOT be restored (--agents only)
|
|
230
|
+
const manifestContent = await Bun.file(
|
|
231
|
+
join(tempDir, ".overstory", "agent-manifest.json"),
|
|
232
|
+
).text();
|
|
233
|
+
expect(manifestContent).toBe("{}");
|
|
234
|
+
|
|
235
|
+
// Hooks should NOT be restored (--agents only)
|
|
236
|
+
const hooksContent = await Bun.file(join(tempDir, ".overstory", "hooks.json")).text();
|
|
237
|
+
expect(hooksContent).toBe("{}");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("--manifest only refreshes agent-manifest.json", async () => {
|
|
241
|
+
await Bun.write(join(tempDir, ".overstory", "agent-manifest.json"), "{}");
|
|
242
|
+
await Bun.write(join(tempDir, ".overstory", "agent-defs", "scout.md"), "# tampered\n");
|
|
243
|
+
|
|
244
|
+
await executeUpdate({ manifest: true });
|
|
245
|
+
|
|
246
|
+
// Manifest should be restored
|
|
247
|
+
const manifestContent = await Bun.file(
|
|
248
|
+
join(tempDir, ".overstory", "agent-manifest.json"),
|
|
249
|
+
).text();
|
|
250
|
+
const expectedManifest = `${JSON.stringify(buildAgentManifest(), null, "\t")}\n`;
|
|
251
|
+
expect(manifestContent).toBe(expectedManifest);
|
|
252
|
+
|
|
253
|
+
// Agent def should NOT be restored
|
|
254
|
+
const scoutContent = await Bun.file(
|
|
255
|
+
join(tempDir, ".overstory", "agent-defs", "scout.md"),
|
|
256
|
+
).text();
|
|
257
|
+
expect(scoutContent).toBe("# tampered\n");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("--hooks only refreshes hooks.json", async () => {
|
|
261
|
+
await Bun.write(join(tempDir, ".overstory", "hooks.json"), "{}");
|
|
262
|
+
await Bun.write(join(tempDir, ".overstory", "agent-manifest.json"), "{}");
|
|
263
|
+
|
|
264
|
+
await executeUpdate({ hooks: true });
|
|
265
|
+
|
|
266
|
+
// Hooks should be restored
|
|
267
|
+
const hooksContent = await Bun.file(join(tempDir, ".overstory", "hooks.json")).text();
|
|
268
|
+
expect(hooksContent).toBe(buildHooksJson());
|
|
269
|
+
|
|
270
|
+
// Manifest should NOT be restored
|
|
271
|
+
const manifestContent = await Bun.file(
|
|
272
|
+
join(tempDir, ".overstory", "agent-manifest.json"),
|
|
273
|
+
).text();
|
|
274
|
+
expect(manifestContent).toBe("{}");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("granular flags do not refresh gitignore or readme", async () => {
|
|
278
|
+
await Bun.write(join(tempDir, ".overstory", ".gitignore"), "# old\n");
|
|
279
|
+
await Bun.write(join(tempDir, ".overstory", "README.md"), "# old\n");
|
|
280
|
+
|
|
281
|
+
await executeUpdate({ agents: true });
|
|
282
|
+
|
|
283
|
+
const gitignoreContent = await Bun.file(join(tempDir, ".overstory", ".gitignore")).text();
|
|
284
|
+
expect(gitignoreContent).toBe("# old\n");
|
|
285
|
+
|
|
286
|
+
const readmeContent = await Bun.file(join(tempDir, ".overstory", "README.md")).text();
|
|
287
|
+
expect(readmeContent).toBe("# old\n");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("executeUpdate: --dry-run", () => {
|
|
292
|
+
let tempDir: string;
|
|
293
|
+
let originalCwd: string;
|
|
294
|
+
let originalWrite: typeof process.stdout.write;
|
|
295
|
+
|
|
296
|
+
beforeEach(async () => {
|
|
297
|
+
tempDir = await createTempGitRepo();
|
|
298
|
+
originalCwd = process.cwd();
|
|
299
|
+
process.chdir(tempDir);
|
|
300
|
+
|
|
301
|
+
originalWrite = process.stdout.write;
|
|
302
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
303
|
+
|
|
304
|
+
await initCommand({ _spawner: noopSpawner });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
afterEach(async () => {
|
|
308
|
+
process.chdir(originalCwd);
|
|
309
|
+
process.stdout.write = originalWrite;
|
|
310
|
+
await cleanupTempDir(tempDir);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("reports changes without writing files", async () => {
|
|
314
|
+
// Tamper with files
|
|
315
|
+
await Bun.write(join(tempDir, ".overstory", "agent-defs", "scout.md"), "# tampered\n");
|
|
316
|
+
await Bun.write(join(tempDir, ".overstory", "agent-manifest.json"), "{}");
|
|
317
|
+
|
|
318
|
+
let captured = "";
|
|
319
|
+
const restoreWrite = process.stdout.write;
|
|
320
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
321
|
+
captured += String(chunk);
|
|
322
|
+
return true;
|
|
323
|
+
}) as typeof process.stdout.write;
|
|
324
|
+
|
|
325
|
+
await executeUpdate({ dryRun: true, json: true });
|
|
326
|
+
|
|
327
|
+
process.stdout.write = restoreWrite;
|
|
328
|
+
|
|
329
|
+
const parsed = JSON.parse(captured.trim()) as Record<string, unknown>;
|
|
330
|
+
expect(parsed.success).toBe(true);
|
|
331
|
+
expect(parsed.dryRun).toBe(true);
|
|
332
|
+
|
|
333
|
+
const agentDefs = parsed.agentDefs as { updated: string[]; unchanged: string[] };
|
|
334
|
+
expect(agentDefs.updated).toContain("scout.md");
|
|
335
|
+
|
|
336
|
+
expect(parsed.manifest).toEqual({ updated: true });
|
|
337
|
+
|
|
338
|
+
// Verify files were NOT actually modified
|
|
339
|
+
const scoutContent = await Bun.file(
|
|
340
|
+
join(tempDir, ".overstory", "agent-defs", "scout.md"),
|
|
341
|
+
).text();
|
|
342
|
+
expect(scoutContent).toBe("# tampered\n");
|
|
343
|
+
|
|
344
|
+
const manifestContent = await Bun.file(
|
|
345
|
+
join(tempDir, ".overstory", "agent-manifest.json"),
|
|
346
|
+
).text();
|
|
347
|
+
expect(manifestContent).toBe("{}");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("executeUpdate: --json output", () => {
|
|
352
|
+
let tempDir: string;
|
|
353
|
+
let originalCwd: string;
|
|
354
|
+
let originalWrite: typeof process.stdout.write;
|
|
355
|
+
|
|
356
|
+
beforeEach(async () => {
|
|
357
|
+
tempDir = await createTempGitRepo();
|
|
358
|
+
originalCwd = process.cwd();
|
|
359
|
+
process.chdir(tempDir);
|
|
360
|
+
|
|
361
|
+
originalWrite = process.stdout.write;
|
|
362
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
363
|
+
|
|
364
|
+
await initCommand({ _spawner: noopSpawner });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
afterEach(async () => {
|
|
368
|
+
process.chdir(originalCwd);
|
|
369
|
+
process.stdout.write = originalWrite;
|
|
370
|
+
await cleanupTempDir(tempDir);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("outputs correct JSON envelope", async () => {
|
|
374
|
+
let captured = "";
|
|
375
|
+
const restoreWrite = process.stdout.write;
|
|
376
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
377
|
+
captured += String(chunk);
|
|
378
|
+
return true;
|
|
379
|
+
}) as typeof process.stdout.write;
|
|
380
|
+
|
|
381
|
+
await executeUpdate({ json: true });
|
|
382
|
+
|
|
383
|
+
process.stdout.write = restoreWrite;
|
|
384
|
+
|
|
385
|
+
const parsed = JSON.parse(captured.trim()) as Record<string, unknown>;
|
|
386
|
+
expect(parsed.success).toBe(true);
|
|
387
|
+
expect(parsed.command).toBe("update");
|
|
388
|
+
expect(parsed.dryRun).toBe(false);
|
|
389
|
+
expect(parsed.agentDefs).toBeDefined();
|
|
390
|
+
expect(parsed.manifest).toBeDefined();
|
|
391
|
+
expect(parsed.hooks).toBeDefined();
|
|
392
|
+
expect(parsed.gitignore).toBeDefined();
|
|
393
|
+
expect(parsed.readme).toBeDefined();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("JSON envelope includes updated file lists", async () => {
|
|
397
|
+
// Tamper with scout
|
|
398
|
+
await Bun.write(join(tempDir, ".overstory", "agent-defs", "scout.md"), "# tampered\n");
|
|
399
|
+
|
|
400
|
+
let captured = "";
|
|
401
|
+
const restoreWrite = process.stdout.write;
|
|
402
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
403
|
+
captured += String(chunk);
|
|
404
|
+
return true;
|
|
405
|
+
}) as typeof process.stdout.write;
|
|
406
|
+
|
|
407
|
+
await executeUpdate({ json: true });
|
|
408
|
+
|
|
409
|
+
process.stdout.write = restoreWrite;
|
|
410
|
+
|
|
411
|
+
const parsed = JSON.parse(captured.trim()) as Record<string, unknown>;
|
|
412
|
+
const agentDefs = parsed.agentDefs as { updated: string[]; unchanged: string[] };
|
|
413
|
+
expect(agentDefs.updated).toContain("scout.md");
|
|
414
|
+
expect(agentDefs.unchanged.length).toBeGreaterThan(0);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe("executeUpdate: agent def exclusions", () => {
|
|
419
|
+
let tempDir: string;
|
|
420
|
+
let originalCwd: string;
|
|
421
|
+
let originalWrite: typeof process.stdout.write;
|
|
422
|
+
|
|
423
|
+
beforeEach(async () => {
|
|
424
|
+
tempDir = await createTempGitRepo();
|
|
425
|
+
originalCwd = process.cwd();
|
|
426
|
+
process.chdir(tempDir);
|
|
427
|
+
|
|
428
|
+
originalWrite = process.stdout.write;
|
|
429
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
430
|
+
|
|
431
|
+
await initCommand({ _spawner: noopSpawner });
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
afterEach(async () => {
|
|
435
|
+
process.chdir(originalCwd);
|
|
436
|
+
process.stdout.write = originalWrite;
|
|
437
|
+
await cleanupTempDir(tempDir);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("does not deploy supervisor.md (deprecated)", async () => {
|
|
441
|
+
await executeUpdate({ agents: true });
|
|
442
|
+
|
|
443
|
+
const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
|
|
444
|
+
const files = await readdir(agentDefsDir);
|
|
445
|
+
expect(files).not.toContain("supervisor.md");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("deploys all non-deprecated agent defs", async () => {
|
|
449
|
+
// Delete all agent defs first
|
|
450
|
+
for (const f of AGENT_DEF_FILES) {
|
|
451
|
+
try {
|
|
452
|
+
const { unlink } = await import("node:fs/promises");
|
|
453
|
+
await unlink(join(tempDir, ".overstory", "agent-defs", f));
|
|
454
|
+
} catch {
|
|
455
|
+
// May not exist
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await executeUpdate({ agents: true });
|
|
460
|
+
|
|
461
|
+
const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
|
|
462
|
+
const files = (await readdir(agentDefsDir)).filter((f) => f.endsWith(".md")).sort();
|
|
463
|
+
expect(files).toEqual(AGENT_DEF_FILES);
|
|
464
|
+
});
|
|
465
|
+
});
|