@os-eco/overstory-cli 0.6.9 → 0.6.11
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 +161 -265
- package/agents/builder.md +6 -15
- package/agents/lead.md +13 -6
- package/agents/merger.md +5 -13
- package/agents/reviewer.md +2 -9
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +105 -0
- package/src/agents/hooks-deployer.ts +26 -11
- package/src/agents/manifest.test.ts +1 -0
- package/src/agents/overlay.test.ts +235 -1
- package/src/agents/overlay.ts +107 -9
- package/src/commands/completions.test.ts +8 -20
- package/src/commands/completions.ts +7 -5
- package/src/commands/coordinator.ts +4 -4
- package/src/commands/doctor.ts +97 -48
- package/src/commands/ecosystem.ts +291 -0
- package/src/commands/feed.ts +2 -2
- package/src/commands/group.ts +4 -4
- package/src/commands/mail.test.ts +63 -1
- package/src/commands/mail.ts +18 -1
- package/src/commands/merge.ts +2 -2
- package/src/commands/monitor.ts +2 -2
- package/src/commands/sling.test.ts +174 -27
- package/src/commands/sling.ts +96 -12
- package/src/commands/status.ts +1 -1
- package/src/commands/supervisor.ts +4 -4
- package/src/commands/trace.ts +2 -2
- package/src/commands/upgrade.test.ts +46 -0
- package/src/commands/upgrade.ts +259 -0
- package/src/config.test.ts +22 -0
- package/src/config.ts +12 -0
- package/src/doctor/agents.test.ts +1 -0
- package/src/doctor/config-check.test.ts +1 -0
- package/src/doctor/consistency.test.ts +1 -0
- package/src/doctor/databases.test.ts +39 -0
- package/src/doctor/databases.ts +7 -10
- package/src/doctor/dependencies.test.ts +1 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +155 -0
- package/src/doctor/logs.test.ts +1 -0
- package/src/doctor/merge-queue.test.ts +99 -0
- package/src/doctor/merge-queue.ts +23 -0
- package/src/doctor/structure.test.ts +131 -1
- package/src/doctor/structure.ts +87 -1
- package/src/doctor/types.ts +5 -2
- package/src/doctor/version.test.ts +1 -0
- package/src/index.ts +29 -4
- package/src/types.ts +11 -0
- package/templates/overlay.md.tmpl +3 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
-
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
9
|
+
import { mkdir, mkdtemp, rm, utimes } from "node:fs/promises";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { OverstoryConfig } from "../types.ts";
|
|
@@ -34,6 +34,7 @@ describe("checkStructure", () => {
|
|
|
34
34
|
staggerDelayMs: 1000,
|
|
35
35
|
maxDepth: 2,
|
|
36
36
|
maxSessionsPerRun: 0,
|
|
37
|
+
maxAgentsPerLead: 5,
|
|
37
38
|
},
|
|
38
39
|
worktrees: {
|
|
39
40
|
baseDir: ".overstory/worktrees",
|
|
@@ -288,4 +289,133 @@ describe("checkStructure", () => {
|
|
|
288
289
|
expect(tempFilesCheck).toBeDefined();
|
|
289
290
|
expect(tempFilesCheck?.status).toBe("pass");
|
|
290
291
|
});
|
|
292
|
+
|
|
293
|
+
test("fix() creates missing subdirectories", async () => {
|
|
294
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
295
|
+
|
|
296
|
+
const checks = await checkStructure(mockConfig, overstoryDir);
|
|
297
|
+
|
|
298
|
+
const dirsCheck = checks.find((c) => c.name === "Required subdirectories");
|
|
299
|
+
expect(dirsCheck?.status).toBe("fail");
|
|
300
|
+
expect(dirsCheck?.fix).toBeDefined();
|
|
301
|
+
|
|
302
|
+
const actions = await dirsCheck?.fix?.();
|
|
303
|
+
expect(actions).toBeDefined();
|
|
304
|
+
expect(actions?.length).toBeGreaterThan(0);
|
|
305
|
+
expect(actions?.some((a) => a.includes("agents/"))).toBe(true);
|
|
306
|
+
expect(actions?.some((a) => a.includes("worktrees/"))).toBe(true);
|
|
307
|
+
expect(actions?.some((a) => a.includes("specs/"))).toBe(true);
|
|
308
|
+
expect(actions?.some((a) => a.includes("logs/"))).toBe(true);
|
|
309
|
+
|
|
310
|
+
// Verify directories were actually created
|
|
311
|
+
const { stat: fsStat } = await import("node:fs/promises");
|
|
312
|
+
const agentsStat = await fsStat(join(overstoryDir, "agents"));
|
|
313
|
+
expect(agentsStat.isDirectory()).toBe(true);
|
|
314
|
+
const worktreesStat = await fsStat(join(overstoryDir, "worktrees"));
|
|
315
|
+
expect(worktreesStat.isDirectory()).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("fix() appends missing .gitignore entries", async () => {
|
|
319
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
320
|
+
await Bun.write(join(overstoryDir, ".gitignore"), `*\n!.gitignore\n!config.yaml\n`);
|
|
321
|
+
|
|
322
|
+
const checks = await checkStructure(mockConfig, overstoryDir);
|
|
323
|
+
|
|
324
|
+
const gitignoreCheck = checks.find((c) => c.name === ".gitignore entries");
|
|
325
|
+
expect(gitignoreCheck?.status).toBe("warn");
|
|
326
|
+
expect(gitignoreCheck?.fix).toBeDefined();
|
|
327
|
+
|
|
328
|
+
const actions = await gitignoreCheck?.fix?.();
|
|
329
|
+
expect(actions).toBeDefined();
|
|
330
|
+
expect(actions?.length).toBeGreaterThan(0);
|
|
331
|
+
expect(actions?.some((a) => a.includes("!agent-manifest.json"))).toBe(true);
|
|
332
|
+
|
|
333
|
+
// Verify entries were appended
|
|
334
|
+
const content = await Bun.file(join(overstoryDir, ".gitignore")).text();
|
|
335
|
+
expect(content).toContain("!agent-manifest.json");
|
|
336
|
+
expect(content).toContain("!hooks.json");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("fix() removes leftover temp files", async () => {
|
|
340
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
341
|
+
const tmpFile = join(overstoryDir, "config.yaml.tmp");
|
|
342
|
+
const bakFile = join(overstoryDir, "old.bak");
|
|
343
|
+
await Bun.write(tmpFile, "temp content");
|
|
344
|
+
await Bun.write(bakFile, "backup content");
|
|
345
|
+
|
|
346
|
+
const checks = await checkStructure(mockConfig, overstoryDir);
|
|
347
|
+
|
|
348
|
+
const tempCheck = checks.find((c) => c.name === "Leftover temp files");
|
|
349
|
+
expect(tempCheck?.status).toBe("warn");
|
|
350
|
+
expect(tempCheck?.fix).toBeDefined();
|
|
351
|
+
|
|
352
|
+
const actions = await tempCheck?.fix?.();
|
|
353
|
+
expect(actions).toBeDefined();
|
|
354
|
+
expect(actions?.some((a) => a.includes("config.yaml.tmp"))).toBe(true);
|
|
355
|
+
expect(actions?.some((a) => a.includes("old.bak"))).toBe(true);
|
|
356
|
+
|
|
357
|
+
// Verify files were deleted
|
|
358
|
+
expect(await Bun.file(tmpFile).exists()).toBe(false);
|
|
359
|
+
expect(await Bun.file(bakFile).exists()).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("passes when no stale lock files exist", async () => {
|
|
363
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
364
|
+
|
|
365
|
+
const checks = await checkStructure(mockConfig, overstoryDir);
|
|
366
|
+
|
|
367
|
+
const lockCheck = checks.find((c) => c.name === "Stale lock files");
|
|
368
|
+
expect(lockCheck).toBeDefined();
|
|
369
|
+
expect(lockCheck?.status).toBe("pass");
|
|
370
|
+
expect(lockCheck?.fix).toBeUndefined();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("warns when stale lock files exist", async () => {
|
|
374
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
375
|
+
const lockFile = join(overstoryDir, "mail.lock");
|
|
376
|
+
await Bun.write(lockFile, "locked");
|
|
377
|
+
// Set mtime to 10 minutes ago
|
|
378
|
+
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
|
379
|
+
await utimes(lockFile, tenMinutesAgo, tenMinutesAgo);
|
|
380
|
+
|
|
381
|
+
const checks = await checkStructure(mockConfig, overstoryDir);
|
|
382
|
+
|
|
383
|
+
const lockCheck = checks.find((c) => c.name === "Stale lock files");
|
|
384
|
+
expect(lockCheck).toBeDefined();
|
|
385
|
+
expect(lockCheck?.status).toBe("warn");
|
|
386
|
+
expect(lockCheck?.details).toContain("mail.lock");
|
|
387
|
+
expect(lockCheck?.fixable).toBe(true);
|
|
388
|
+
expect(lockCheck?.fix).toBeDefined();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("does not warn about fresh lock files", async () => {
|
|
392
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
393
|
+
// Write a fresh lock file (just created = now)
|
|
394
|
+
await Bun.write(join(overstoryDir, "sessions.lock"), "locked");
|
|
395
|
+
|
|
396
|
+
const checks = await checkStructure(mockConfig, overstoryDir);
|
|
397
|
+
|
|
398
|
+
const lockCheck = checks.find((c) => c.name === "Stale lock files");
|
|
399
|
+
expect(lockCheck?.status).toBe("pass");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("fix() removes stale lock files", async () => {
|
|
403
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
404
|
+
const lockFile = join(overstoryDir, "stale.lock");
|
|
405
|
+
await Bun.write(lockFile, "locked");
|
|
406
|
+
// Set mtime to 10 minutes ago
|
|
407
|
+
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
|
408
|
+
await utimes(lockFile, tenMinutesAgo, tenMinutesAgo);
|
|
409
|
+
|
|
410
|
+
const checks = await checkStructure(mockConfig, overstoryDir);
|
|
411
|
+
|
|
412
|
+
const lockCheck = checks.find((c) => c.name === "Stale lock files");
|
|
413
|
+
expect(lockCheck?.fix).toBeDefined();
|
|
414
|
+
|
|
415
|
+
const actions = await lockCheck?.fix?.();
|
|
416
|
+
expect(actions?.some((a) => a.includes("stale.lock"))).toBe(true);
|
|
417
|
+
|
|
418
|
+
// Verify the lock file was removed
|
|
419
|
+
expect(await Bun.file(lockFile).exists()).toBe(false);
|
|
420
|
+
});
|
|
291
421
|
});
|
package/src/doctor/structure.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, constants } from "node:fs/promises";
|
|
1
|
+
import { access, constants, mkdir, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { AgentManifest } from "../types.ts";
|
|
4
4
|
import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
|
|
@@ -87,6 +87,18 @@ export const checkStructure: DoctorCheckFn = async (
|
|
|
87
87
|
: `Missing ${missingDirs.length} subdirectory(ies)`,
|
|
88
88
|
details: missingDirs.length > 0 ? missingDirs : undefined,
|
|
89
89
|
fixable: missingDirs.length > 0,
|
|
90
|
+
fix:
|
|
91
|
+
missingDirs.length > 0
|
|
92
|
+
? async () => {
|
|
93
|
+
const actions: string[] = [];
|
|
94
|
+
for (const dir of missingDirs) {
|
|
95
|
+
const dirPath = join(overstoryDir, dir.replace(/\/$/, ""));
|
|
96
|
+
await mkdir(dirPath, { recursive: true });
|
|
97
|
+
actions.push(`Created missing directory: ${dir}`);
|
|
98
|
+
}
|
|
99
|
+
return actions;
|
|
100
|
+
}
|
|
101
|
+
: undefined,
|
|
90
102
|
});
|
|
91
103
|
|
|
92
104
|
// Check 4: .gitignore contents — validate wildcard+whitelist model
|
|
@@ -115,6 +127,19 @@ export const checkStructure: DoctorCheckFn = async (
|
|
|
115
127
|
: `Missing ${missingEntries.length} entry(ies)`,
|
|
116
128
|
details: missingEntries.length > 0 ? missingEntries : undefined,
|
|
117
129
|
fixable: missingEntries.length > 0,
|
|
130
|
+
fix:
|
|
131
|
+
missingEntries.length > 0
|
|
132
|
+
? async () => {
|
|
133
|
+
const actions: string[] = [];
|
|
134
|
+
const content = await Bun.file(gitignorePath).text();
|
|
135
|
+
const suffix = content.endsWith("\n") ? "" : "\n";
|
|
136
|
+
await Bun.write(gitignorePath, `${content + suffix + missingEntries.join("\n")}\n`);
|
|
137
|
+
for (const entry of missingEntries) {
|
|
138
|
+
actions.push(`Added .gitignore entry: ${entry}`);
|
|
139
|
+
}
|
|
140
|
+
return actions;
|
|
141
|
+
}
|
|
142
|
+
: undefined,
|
|
118
143
|
});
|
|
119
144
|
} catch {
|
|
120
145
|
// .gitignore doesn't exist, already reported in required files check
|
|
@@ -189,10 +214,71 @@ export const checkStructure: DoctorCheckFn = async (
|
|
|
189
214
|
tempFiles.length === 0 ? "No temp files found" : `Found ${tempFiles.length} temp file(s)`,
|
|
190
215
|
details: tempFiles.length > 0 ? tempFiles : undefined,
|
|
191
216
|
fixable: tempFiles.length > 0,
|
|
217
|
+
fix:
|
|
218
|
+
tempFiles.length > 0
|
|
219
|
+
? async () => {
|
|
220
|
+
const actions: string[] = [];
|
|
221
|
+
for (const file of tempFiles) {
|
|
222
|
+
await rm(join(overstoryDir, file), { force: true });
|
|
223
|
+
actions.push(`Removed temp file: ${file}`);
|
|
224
|
+
}
|
|
225
|
+
return actions;
|
|
226
|
+
}
|
|
227
|
+
: undefined,
|
|
192
228
|
});
|
|
193
229
|
} catch {
|
|
194
230
|
// Ignore errors scanning for temp files
|
|
195
231
|
}
|
|
196
232
|
|
|
233
|
+
// Check 7: Stale lock files (older than 5 minutes)
|
|
234
|
+
try {
|
|
235
|
+
const lockEntries = await Array.fromAsync(new Bun.Glob("*.lock").scan({ cwd: overstoryDir }));
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
const staleLockThresholdMs = 5 * 60 * 1000;
|
|
238
|
+
const staleLockFiles: string[] = [];
|
|
239
|
+
|
|
240
|
+
for (const lockFile of lockEntries) {
|
|
241
|
+
try {
|
|
242
|
+
const lockPath = join(overstoryDir, lockFile);
|
|
243
|
+
const stats = await stat(lockPath);
|
|
244
|
+
const ageMs = now - stats.mtimeMs;
|
|
245
|
+
if (ageMs > staleLockThresholdMs) {
|
|
246
|
+
staleLockFiles.push(lockFile);
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// ignore stat errors
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
checks.push({
|
|
254
|
+
name: "Stale lock files",
|
|
255
|
+
category: "structure",
|
|
256
|
+
status: staleLockFiles.length === 0 ? "pass" : "warn",
|
|
257
|
+
message:
|
|
258
|
+
staleLockFiles.length === 0
|
|
259
|
+
? "No stale lock files found"
|
|
260
|
+
: `Found ${staleLockFiles.length} stale lock file(s)`,
|
|
261
|
+
details: staleLockFiles.length > 0 ? staleLockFiles : undefined,
|
|
262
|
+
fixable: staleLockFiles.length > 0,
|
|
263
|
+
fix:
|
|
264
|
+
staleLockFiles.length > 0
|
|
265
|
+
? async () => {
|
|
266
|
+
const actions: string[] = [];
|
|
267
|
+
for (const file of staleLockFiles) {
|
|
268
|
+
try {
|
|
269
|
+
await rm(join(overstoryDir, file), { force: true });
|
|
270
|
+
actions.push(`Removed stale lock file: ${file}`);
|
|
271
|
+
} catch {
|
|
272
|
+
// ignore removal errors
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return actions;
|
|
276
|
+
}
|
|
277
|
+
: undefined,
|
|
278
|
+
});
|
|
279
|
+
} catch {
|
|
280
|
+
// ignore errors scanning for lock files
|
|
281
|
+
}
|
|
282
|
+
|
|
197
283
|
return checks;
|
|
198
284
|
};
|
package/src/doctor/types.ts
CHANGED
|
@@ -12,7 +12,8 @@ export type DoctorCategory =
|
|
|
12
12
|
| "agents"
|
|
13
13
|
| "merge"
|
|
14
14
|
| "logs"
|
|
15
|
-
| "version"
|
|
15
|
+
| "version"
|
|
16
|
+
| "ecosystem";
|
|
16
17
|
|
|
17
18
|
/** Result of a single doctor health check. */
|
|
18
19
|
export interface DoctorCheck {
|
|
@@ -21,8 +22,10 @@ export interface DoctorCheck {
|
|
|
21
22
|
status: "pass" | "warn" | "fail";
|
|
22
23
|
message: string;
|
|
23
24
|
details?: string[];
|
|
24
|
-
/** Whether this check issues can be auto-fixed
|
|
25
|
+
/** Whether this check issues can be auto-fixed via --fix. */
|
|
25
26
|
fixable?: boolean;
|
|
27
|
+
/** Auto-fix closure — called when --fix flag is passed. Captures context at construction time. */
|
|
28
|
+
fix?: () => Promise<string[]> | string[];
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { createCoordinatorCommand } from "./commands/coordinator.ts";
|
|
|
15
15
|
import { createCostsCommand } from "./commands/costs.ts";
|
|
16
16
|
import { createDashboardCommand } from "./commands/dashboard.ts";
|
|
17
17
|
import { createDoctorCommand } from "./commands/doctor.ts";
|
|
18
|
+
import { createEcosystemCommand } from "./commands/ecosystem.ts";
|
|
18
19
|
import { createErrorsCommand } from "./commands/errors.ts";
|
|
19
20
|
import { createFeedCommand } from "./commands/feed.ts";
|
|
20
21
|
import { createGroupCommand } from "./commands/group.ts";
|
|
@@ -37,13 +38,14 @@ import { createStatusCommand } from "./commands/status.ts";
|
|
|
37
38
|
import { stopCommand } from "./commands/stop.ts";
|
|
38
39
|
import { createSupervisorCommand } from "./commands/supervisor.ts";
|
|
39
40
|
import { traceCommand } from "./commands/trace.ts";
|
|
41
|
+
import { createUpgradeCommand } from "./commands/upgrade.ts";
|
|
40
42
|
import { createWatchCommand } from "./commands/watch.ts";
|
|
41
43
|
import { createWorktreeCommand } from "./commands/worktree.ts";
|
|
42
44
|
import { OverstoryError, WorktreeError } from "./errors.ts";
|
|
43
45
|
import { jsonError } from "./json.ts";
|
|
44
46
|
import { brand, chalk, muted, setQuiet } from "./logging/color.ts";
|
|
45
47
|
|
|
46
|
-
export const VERSION = "0.6.
|
|
48
|
+
export const VERSION = "0.6.11";
|
|
47
49
|
|
|
48
50
|
const rawArgs = process.argv.slice(2);
|
|
49
51
|
|
|
@@ -86,12 +88,14 @@ const COMMANDS = [
|
|
|
86
88
|
"logs",
|
|
87
89
|
"watch",
|
|
88
90
|
"trace",
|
|
91
|
+
"ecosystem",
|
|
89
92
|
"feed",
|
|
90
93
|
"errors",
|
|
91
94
|
"replay",
|
|
92
95
|
"run",
|
|
93
96
|
"costs",
|
|
94
97
|
"metrics",
|
|
98
|
+
"upgrade",
|
|
95
99
|
];
|
|
96
100
|
|
|
97
101
|
function editDistance(a: string, b: string): number {
|
|
@@ -129,6 +133,8 @@ function suggestCommand(input: string): string | undefined {
|
|
|
129
133
|
|
|
130
134
|
const program = new Command();
|
|
131
135
|
|
|
136
|
+
let timingStart: number | undefined;
|
|
137
|
+
|
|
132
138
|
program
|
|
133
139
|
.name("ov")
|
|
134
140
|
.description("Multi-agent orchestration for Claude Code")
|
|
@@ -136,6 +142,7 @@ program
|
|
|
136
142
|
.option("-q, --quiet", "Suppress non-error output")
|
|
137
143
|
.option("--json", "JSON output")
|
|
138
144
|
.option("--verbose", "Verbose output")
|
|
145
|
+
.option("--timing", "Print command execution time to stderr")
|
|
139
146
|
.addHelpCommand(false)
|
|
140
147
|
.configureHelp({
|
|
141
148
|
formatHelp(cmd, helper): string {
|
|
@@ -191,6 +198,17 @@ program.hook("preAction", (thisCmd) => {
|
|
|
191
198
|
if (opts.quiet) {
|
|
192
199
|
setQuiet(true);
|
|
193
200
|
}
|
|
201
|
+
if (opts.timing) {
|
|
202
|
+
timingStart = performance.now();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
program.hook("postAction", () => {
|
|
206
|
+
if (program.opts().timing && timingStart !== undefined) {
|
|
207
|
+
const elapsed = performance.now() - timingStart;
|
|
208
|
+
const formatted =
|
|
209
|
+
elapsed < 1000 ? `${Math.round(elapsed)}ms` : `${(elapsed / 1000).toFixed(2)}s`;
|
|
210
|
+
process.stderr.write(`${muted(`Done in ${formatted}`)}\n`);
|
|
211
|
+
}
|
|
194
212
|
});
|
|
195
213
|
|
|
196
214
|
// Migrated commands — use addCommand() with createXCommand() factories
|
|
@@ -234,6 +252,9 @@ program
|
|
|
234
252
|
.option("--skip-scout", "Skip scout phase for lead agents")
|
|
235
253
|
.option("--skip-task-check", "Skip task existence validation")
|
|
236
254
|
.option("--force-hierarchy", "Bypass hierarchy validation")
|
|
255
|
+
.option("--max-agents <n>", "Max children per lead (overrides config)")
|
|
256
|
+
.option("--skip-review", "Skip review phase for lead agents")
|
|
257
|
+
.option("--dispatch-max-agents <n>", "Per-lead max agents ceiling (injected into overlay)")
|
|
237
258
|
.option("--json", "Output result as JSON")
|
|
238
259
|
.action(async (taskId, opts) => {
|
|
239
260
|
await slingCommand(taskId, opts);
|
|
@@ -243,8 +264,8 @@ const specCmd = program.command("spec").description("Manage task specifications"
|
|
|
243
264
|
|
|
244
265
|
specCmd
|
|
245
266
|
.command("write")
|
|
246
|
-
.description("Write a spec file to .overstory/specs/<
|
|
247
|
-
.argument("<
|
|
267
|
+
.description("Write a spec file to .overstory/specs/<task-id>.md")
|
|
268
|
+
.argument("<task-id>", "Task ID for the spec file")
|
|
248
269
|
.option("--body <content>", "Spec content (or pipe via stdin)")
|
|
249
270
|
.option("--agent <name>", "Agent writing the spec (for attribution)")
|
|
250
271
|
.action(async (taskId, opts) => {
|
|
@@ -335,7 +356,7 @@ program
|
|
|
335
356
|
|
|
336
357
|
program
|
|
337
358
|
.command("trace")
|
|
338
|
-
.description("Chronological event timeline for agent
|
|
359
|
+
.description("Chronological event timeline for agent or task")
|
|
339
360
|
.allowUnknownOption()
|
|
340
361
|
.allowExcessArguments()
|
|
341
362
|
.action(async (_opts, cmd) => {
|
|
@@ -344,6 +365,8 @@ program
|
|
|
344
365
|
|
|
345
366
|
program.addCommand(createFeedCommand());
|
|
346
367
|
|
|
368
|
+
program.addCommand(createEcosystemCommand());
|
|
369
|
+
|
|
347
370
|
program.addCommand(createErrorsCommand());
|
|
348
371
|
|
|
349
372
|
program.addCommand(createReplayCommand());
|
|
@@ -354,6 +377,8 @@ program.addCommand(createCostsCommand());
|
|
|
354
377
|
|
|
355
378
|
program.addCommand(createMetricsCommand());
|
|
356
379
|
|
|
380
|
+
program.addCommand(createUpgradeCommand());
|
|
381
|
+
|
|
357
382
|
// Handle unknown commands with Levenshtein fuzzy-match suggestions
|
|
358
383
|
program.on("command:*", (operands) => {
|
|
359
384
|
const unknown = operands[0] ?? "";
|
package/src/types.ts
CHANGED
|
@@ -53,6 +53,7 @@ export interface OverstoryConfig {
|
|
|
53
53
|
staggerDelayMs: number; // Delay between spawns
|
|
54
54
|
maxDepth: number; // Hierarchy depth limit (default 2)
|
|
55
55
|
maxSessionsPerRun: number; // Max total sessions per run (0 = unlimited)
|
|
56
|
+
maxAgentsPerLead: number; // Max children a single lead can spawn (0 = unlimited)
|
|
56
57
|
};
|
|
57
58
|
worktrees: {
|
|
58
59
|
baseDir: string; // Where worktrees live
|
|
@@ -258,6 +259,12 @@ export interface DispatchPayload {
|
|
|
258
259
|
specPath: string;
|
|
259
260
|
capability: Capability;
|
|
260
261
|
fileScope: string[];
|
|
262
|
+
/** Optional: skip scout phase for lead agents */
|
|
263
|
+
skipScouts?: boolean;
|
|
264
|
+
/** Optional: skip review phase for lead agents */
|
|
265
|
+
skipReview?: boolean;
|
|
266
|
+
/** Optional: per-lead max agent ceiling override */
|
|
267
|
+
maxAgents?: number;
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
/** Supervisor assigns work to a specific worker. */
|
|
@@ -300,6 +307,10 @@ export interface OverlayConfig {
|
|
|
300
307
|
mulchExpertise?: string;
|
|
301
308
|
/** When true, lead agents should skip Phase 1 (scout) and go straight to Phase 2 (build). */
|
|
302
309
|
skipScout?: boolean;
|
|
310
|
+
/** When true, lead agents should skip Phase 3 review and self-verify instead. */
|
|
311
|
+
skipReview?: boolean;
|
|
312
|
+
/** Per-lead max agents ceiling override from dispatch. Injected into overlay for lead visibility. */
|
|
313
|
+
maxAgentsOverride?: number;
|
|
303
314
|
trackerCli?: string; // "sd" or "bd"
|
|
304
315
|
trackerName?: string; // "seeds" or "beads"
|
|
305
316
|
/** Quality gate commands for the agent overlay. Falls back to defaults if undefined. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
## Your Assignment
|
|
11
11
|
|
|
12
12
|
- **Agent Name:** {{AGENT_NAME}}
|
|
13
|
-
- **Task ID:** {{
|
|
13
|
+
- **Task ID:** {{TASK_ID}}
|
|
14
14
|
- **Spec:** {{SPEC_PATH}}
|
|
15
15
|
- **Branch:** {{BRANCH_NAME}}
|
|
16
16
|
- **Worktree:** {{WORKTREE_PATH}}
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
{{SKIP_SCOUT}}
|
|
23
23
|
|
|
24
|
+
{{DISPATCH_OVERRIDES}}
|
|
25
|
+
|
|
24
26
|
## Working Directory
|
|
25
27
|
|
|
26
28
|
Your worktree root is: `{{WORKTREE_PATH}}`
|