@os-eco/overstory-cli 0.6.4 → 0.6.6
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 +61 -61
- package/agents/builder.md +16 -16
- package/agents/coordinator.md +57 -57
- package/agents/issue-reviews.md +71 -0
- package/agents/lead.md +43 -42
- package/agents/merger.md +15 -15
- package/agents/monitor.md +37 -37
- package/agents/pr-reviews.md +60 -0
- package/agents/prioritize.md +110 -0
- package/agents/release.md +56 -0
- package/agents/reviewer.md +15 -15
- package/agents/scout.md +18 -18
- package/agents/supervisor.md +78 -78
- package/package.json +1 -1
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +59 -25
- package/src/agents/hooks-deployer.ts +24 -6
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/overlay.test.ts +14 -14
- package/src/agents/overlay.ts +14 -14
- package/src/commands/agents.test.ts +5 -5
- package/src/commands/agents.ts +10 -9
- package/src/commands/clean.test.ts +5 -5
- package/src/commands/clean.ts +5 -5
- package/src/commands/completions.test.ts +10 -10
- package/src/commands/completions.ts +26 -28
- package/src/commands/coordinator.test.ts +4 -4
- package/src/commands/coordinator.ts +13 -13
- package/src/commands/costs.test.ts +45 -45
- package/src/commands/costs.ts +1 -1
- package/src/commands/dashboard.ts +11 -11
- package/src/commands/doctor.ts +4 -4
- package/src/commands/errors.ts +1 -1
- package/src/commands/feed.ts +1 -1
- package/src/commands/group.ts +3 -3
- package/src/commands/hooks.test.ts +7 -7
- package/src/commands/hooks.ts +7 -7
- package/src/commands/init.test.ts +6 -2
- package/src/commands/init.ts +19 -19
- package/src/commands/inspect.test.ts +16 -16
- package/src/commands/inspect.ts +19 -19
- package/src/commands/log.test.ts +21 -21
- package/src/commands/log.ts +10 -10
- package/src/commands/logs.ts +1 -1
- package/src/commands/mail.test.ts +7 -7
- package/src/commands/mail.ts +28 -11
- package/src/commands/merge.test.ts +8 -8
- package/src/commands/merge.ts +15 -15
- package/src/commands/metrics.test.ts +7 -7
- package/src/commands/metrics.ts +3 -3
- package/src/commands/monitor.test.ts +5 -5
- package/src/commands/monitor.ts +5 -5
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +1 -1
- package/src/commands/prime.test.ts +5 -5
- package/src/commands/prime.ts +8 -8
- package/src/commands/replay.ts +1 -1
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +2 -2
- package/src/commands/sling.test.ts +89 -7
- package/src/commands/sling.ts +109 -18
- package/src/commands/spec.test.ts +2 -2
- package/src/commands/spec.ts +13 -14
- package/src/commands/status.test.ts +99 -3
- package/src/commands/status.ts +19 -20
- package/src/commands/stop.test.ts +1 -1
- package/src/commands/stop.ts +2 -2
- package/src/commands/supervisor.test.ts +10 -10
- package/src/commands/supervisor.ts +14 -14
- package/src/commands/trace.test.ts +7 -7
- package/src/commands/trace.ts +10 -10
- package/src/commands/watch.ts +5 -5
- package/src/commands/worktree.test.ts +208 -32
- package/src/commands/worktree.ts +56 -18
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/dependencies.test.ts +5 -5
- package/src/doctor/dependencies.ts +2 -2
- package/src/doctor/logs.ts +1 -1
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/doctor/structure.test.ts +1 -1
- package/src/doctor/structure.ts +1 -1
- package/src/doctor/version.test.ts +3 -3
- package/src/doctor/version.ts +1 -1
- package/src/e2e/init-sling-lifecycle.test.ts +8 -4
- package/src/errors.ts +1 -1
- package/src/index.ts +13 -11
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +7 -7
- package/src/mail/client.ts +2 -2
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +12 -12
- package/src/merge/queue.ts +2 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/mulch/client.ts +1 -1
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +1 -1
- package/src/sessions/store.test.ts +4 -4
- package/src/sessions/store.ts +2 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +8 -3
- package/src/worktree/tmux.ts +19 -18
- package/templates/CLAUDE.md.tmpl +27 -27
- package/templates/hooks.json.tmpl +15 -11
- package/templates/overlay.md.tmpl +7 -7
|
@@ -106,6 +106,7 @@ const DANGEROUS_BASH_PATTERNS = [
|
|
|
106
106
|
* This whitelist is checked BEFORE the blocklist.
|
|
107
107
|
*/
|
|
108
108
|
const SAFE_BASH_PREFIXES = [
|
|
109
|
+
"ov ",
|
|
109
110
|
"overstory ",
|
|
110
111
|
"bd ",
|
|
111
112
|
"sd ",
|
|
@@ -205,6 +206,20 @@ export function getPathBoundaryGuards(): HookEntry[] {
|
|
|
205
206
|
];
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Escape a string for use inside a single-quoted POSIX shell string.
|
|
211
|
+
*
|
|
212
|
+
* POSIX single-quoted strings cannot contain single quotes at all.
|
|
213
|
+
* The standard technique is to end the single-quoted segment, emit an escaped
|
|
214
|
+
* single quote using $'\'', then start a new single-quoted segment:
|
|
215
|
+
* 'it'\''s fine' → it's fine
|
|
216
|
+
*
|
|
217
|
+
* Exported so tests can verify escaping directly.
|
|
218
|
+
*/
|
|
219
|
+
export function escapeForSingleQuotedShell(str: string): string {
|
|
220
|
+
return str.replace(/'/g, "'\\''");
|
|
221
|
+
}
|
|
222
|
+
|
|
208
223
|
/**
|
|
209
224
|
* Build a PreToolUse guard that blocks a specific tool.
|
|
210
225
|
*
|
|
@@ -218,7 +233,7 @@ function blockGuard(toolName: string, reason: string): HookEntry {
|
|
|
218
233
|
hooks: [
|
|
219
234
|
{
|
|
220
235
|
type: "command",
|
|
221
|
-
command: `${ENV_GUARD} echo '${response}'`,
|
|
236
|
+
command: `${ENV_GUARD} echo '${escapeForSingleQuotedShell(response)}'`,
|
|
222
237
|
},
|
|
223
238
|
],
|
|
224
239
|
};
|
|
@@ -242,7 +257,7 @@ function buildBashGuardScript(agentName: string): string {
|
|
|
242
257
|
'CMD=$(echo "$INPUT" | sed \'s/.*"command": *"\\([^"]*\\)".*/\\1/\');',
|
|
243
258
|
// Check 1: Block all git push — agents must never push to remote
|
|
244
259
|
"if echo \"$CMD\" | grep -qE '\\bgit\\s+push\\b'; then",
|
|
245
|
-
' echo \'{"decision":"block","reason":"git push is blocked — use
|
|
260
|
+
' echo \'{"decision":"block","reason":"git push is blocked — use ov merge to integrate changes, push manually when ready"}\';',
|
|
246
261
|
" exit 0;",
|
|
247
262
|
"fi;",
|
|
248
263
|
// Check 2: Block git reset --hard
|
|
@@ -447,7 +462,7 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
|
|
|
447
462
|
const teamToolGuards = NATIVE_TEAM_TOOLS.map((tool) =>
|
|
448
463
|
blockGuard(
|
|
449
464
|
tool,
|
|
450
|
-
`Overstory agents must use '
|
|
465
|
+
`Overstory agents must use 'ov sling' for delegation — ${tool} is not allowed`,
|
|
451
466
|
),
|
|
452
467
|
);
|
|
453
468
|
guards.push(...teamToolGuards);
|
|
@@ -458,7 +473,7 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
|
|
|
458
473
|
const interactiveGuards = INTERACTIVE_TOOLS.map((tool) =>
|
|
459
474
|
blockGuard(
|
|
460
475
|
tool,
|
|
461
|
-
`${tool} requires human interaction -- agents run non-interactively. Use
|
|
476
|
+
`${tool} requires human interaction -- agents run non-interactively. Use ov mail (--type question) to escalate`,
|
|
462
477
|
),
|
|
463
478
|
);
|
|
464
479
|
guards.push(...interactiveGuards);
|
|
@@ -495,13 +510,16 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
|
|
|
495
510
|
/**
|
|
496
511
|
* Check whether a hook entry is overstory-managed.
|
|
497
512
|
*
|
|
498
|
-
* Overstory hook commands always reference either `overstory` (CLI commands)
|
|
513
|
+
* Overstory hook commands always reference either `ov ` / `overstory` (CLI commands)
|
|
499
514
|
* or `OVERSTORY_` (env var guards like OVERSTORY_AGENT_NAME, OVERSTORY_WORKTREE_PATH).
|
|
500
515
|
* User hooks will not contain these patterns.
|
|
501
516
|
*/
|
|
502
517
|
export function isOverstoryHookEntry(entry: HookEntry): boolean {
|
|
503
518
|
return entry.hooks.some(
|
|
504
|
-
(h) =>
|
|
519
|
+
(h) =>
|
|
520
|
+
h.command.includes("ov ") ||
|
|
521
|
+
h.command.includes("overstory") ||
|
|
522
|
+
h.command.includes("OVERSTORY_"),
|
|
505
523
|
);
|
|
506
524
|
}
|
|
507
525
|
|
|
@@ -72,12 +72,12 @@ describe("identity", () => {
|
|
|
72
72
|
expertiseDomains: [],
|
|
73
73
|
recentTasks: [
|
|
74
74
|
{
|
|
75
|
-
|
|
75
|
+
taskId: "beads-001",
|
|
76
76
|
summary: "Fixed authentication bug",
|
|
77
77
|
completedAt: "2024-01-15T12:00:00Z",
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
|
-
|
|
80
|
+
taskId: "beads-002",
|
|
81
81
|
summary: "Added user profile page",
|
|
82
82
|
completedAt: "2024-01-16T14:30:00Z",
|
|
83
83
|
},
|
|
@@ -89,10 +89,10 @@ describe("identity", () => {
|
|
|
89
89
|
const filePath = join(tempDir, "test-agent", "identity.yaml");
|
|
90
90
|
const content = await Bun.file(filePath).text();
|
|
91
91
|
expect(content).toContain("recentTasks:");
|
|
92
|
-
expect(content).toContain("\t-
|
|
92
|
+
expect(content).toContain("\t- taskId: beads-001");
|
|
93
93
|
expect(content).toContain("\t\tsummary: Fixed authentication bug");
|
|
94
94
|
expect(content).toContain('\t\tcompletedAt: "2024-01-15T12:00:00Z"');
|
|
95
|
-
expect(content).toContain("\t-
|
|
95
|
+
expect(content).toContain("\t- taskId: beads-002");
|
|
96
96
|
expect(content).toContain("\t\tsummary: Added user profile page");
|
|
97
97
|
expect(content).toContain('\t\tcompletedAt: "2024-01-16T14:30:00Z"');
|
|
98
98
|
});
|
|
@@ -106,7 +106,7 @@ describe("identity", () => {
|
|
|
106
106
|
expertiseDomains: ["domain: with colon", "domain#with hash", " leading space"],
|
|
107
107
|
recentTasks: [
|
|
108
108
|
{
|
|
109
|
-
|
|
109
|
+
taskId: "beads-001",
|
|
110
110
|
summary: 'Fixed bug: "memory leak"',
|
|
111
111
|
completedAt: "2024-01-15T12:00:00Z",
|
|
112
112
|
},
|
|
@@ -198,7 +198,7 @@ describe("identity", () => {
|
|
|
198
198
|
expertiseDomains: ["typescript", "testing"],
|
|
199
199
|
recentTasks: [
|
|
200
200
|
{
|
|
201
|
-
|
|
201
|
+
taskId: "beads-001",
|
|
202
202
|
summary: "Fixed bug",
|
|
203
203
|
completedAt: "2024-01-15T12:00:00Z",
|
|
204
204
|
},
|
|
@@ -216,7 +216,7 @@ describe("identity", () => {
|
|
|
216
216
|
expect(loaded?.sessionsCompleted).toBe(7);
|
|
217
217
|
expect(loaded?.expertiseDomains).toEqual(["typescript", "testing"]);
|
|
218
218
|
expect(loaded?.recentTasks).toHaveLength(1);
|
|
219
|
-
expect(loaded?.recentTasks[0]?.
|
|
219
|
+
expect(loaded?.recentTasks[0]?.taskId).toBe("beads-001");
|
|
220
220
|
expect(loaded?.recentTasks[0]?.summary).toBe("Fixed bug");
|
|
221
221
|
expect(loaded?.recentTasks[0]?.completedAt).toBe("2024-01-15T12:00:00Z");
|
|
222
222
|
});
|
|
@@ -252,17 +252,17 @@ describe("identity", () => {
|
|
|
252
252
|
expertiseDomains: [],
|
|
253
253
|
recentTasks: [
|
|
254
254
|
{
|
|
255
|
-
|
|
255
|
+
taskId: "beads-001",
|
|
256
256
|
summary: "Task 1",
|
|
257
257
|
completedAt: "2024-01-15T12:00:00Z",
|
|
258
258
|
},
|
|
259
259
|
{
|
|
260
|
-
|
|
260
|
+
taskId: "beads-002",
|
|
261
261
|
summary: "Task 2",
|
|
262
262
|
completedAt: "2024-01-16T12:00:00Z",
|
|
263
263
|
},
|
|
264
264
|
{
|
|
265
|
-
|
|
265
|
+
taskId: "beads-003",
|
|
266
266
|
summary: "Task 3",
|
|
267
267
|
completedAt: "2024-01-17T12:00:00Z",
|
|
268
268
|
},
|
|
@@ -273,9 +273,9 @@ describe("identity", () => {
|
|
|
273
273
|
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
274
274
|
|
|
275
275
|
expect(loaded?.recentTasks).toHaveLength(3);
|
|
276
|
-
expect(loaded?.recentTasks[0]?.
|
|
277
|
-
expect(loaded?.recentTasks[1]?.
|
|
278
|
-
expect(loaded?.recentTasks[2]?.
|
|
276
|
+
expect(loaded?.recentTasks[0]?.taskId).toBe("beads-001");
|
|
277
|
+
expect(loaded?.recentTasks[1]?.taskId).toBe("beads-002");
|
|
278
|
+
expect(loaded?.recentTasks[2]?.taskId).toBe("beads-003");
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
test("handles quoted strings with special characters", async () => {
|
|
@@ -287,7 +287,7 @@ describe("identity", () => {
|
|
|
287
287
|
expertiseDomains: ["domain: with colon", "domain#with hash"],
|
|
288
288
|
recentTasks: [
|
|
289
289
|
{
|
|
290
|
-
|
|
290
|
+
taskId: "beads-001",
|
|
291
291
|
summary: 'Fixed bug: "memory leak"',
|
|
292
292
|
completedAt: "2024-01-15T12:00:00Z",
|
|
293
293
|
},
|
|
@@ -311,7 +311,7 @@ describe("identity", () => {
|
|
|
311
311
|
expertiseDomains: [],
|
|
312
312
|
recentTasks: [
|
|
313
313
|
{
|
|
314
|
-
|
|
314
|
+
taskId: "beads-001",
|
|
315
315
|
summary: "Path: C:\\Users\\test\\file.txt",
|
|
316
316
|
completedAt: "2024-01-15T12:00:00Z",
|
|
317
317
|
},
|
|
@@ -435,14 +435,14 @@ recentTasks: []
|
|
|
435
435
|
const beforeUpdate = Date.now();
|
|
436
436
|
const updated = await updateIdentity(tempDir, "test-agent", {
|
|
437
437
|
completedTask: {
|
|
438
|
-
|
|
438
|
+
taskId: "beads-001",
|
|
439
439
|
summary: "Fixed authentication bug",
|
|
440
440
|
},
|
|
441
441
|
});
|
|
442
442
|
const afterUpdate = Date.now();
|
|
443
443
|
|
|
444
444
|
expect(updated.recentTasks).toHaveLength(1);
|
|
445
|
-
expect(updated.recentTasks[0]?.
|
|
445
|
+
expect(updated.recentTasks[0]?.taskId).toBe("beads-001");
|
|
446
446
|
expect(updated.recentTasks[0]?.summary).toBe("Fixed authentication bug");
|
|
447
447
|
|
|
448
448
|
// Verify timestamp is within the update window
|
|
@@ -454,7 +454,7 @@ recentTasks: []
|
|
|
454
454
|
test("caps recentTasks at 20 entries, dropping oldest", async () => {
|
|
455
455
|
// Create identity with 19 tasks
|
|
456
456
|
const existingTasks = Array.from({ length: 19 }, (_, i) => ({
|
|
457
|
-
|
|
457
|
+
taskId: `beads-${i.toString().padStart(3, "0")}`,
|
|
458
458
|
summary: `Task ${i}`,
|
|
459
459
|
completedAt: `2024-01-${(i + 1).toString().padStart(2, "0")}T12:00:00Z`,
|
|
460
460
|
}));
|
|
@@ -472,20 +472,20 @@ recentTasks: []
|
|
|
472
472
|
|
|
473
473
|
// Add two more tasks (total would be 21)
|
|
474
474
|
let updated = await updateIdentity(tempDir, "test-agent", {
|
|
475
|
-
completedTask: {
|
|
475
|
+
completedTask: { taskId: "beads-019", summary: "Task 19" },
|
|
476
476
|
});
|
|
477
477
|
|
|
478
478
|
expect(updated.recentTasks).toHaveLength(20);
|
|
479
|
-
expect(updated.recentTasks[0]?.
|
|
479
|
+
expect(updated.recentTasks[0]?.taskId).toBe("beads-000");
|
|
480
480
|
|
|
481
481
|
updated = await updateIdentity(tempDir, "test-agent", {
|
|
482
|
-
completedTask: {
|
|
482
|
+
completedTask: { taskId: "beads-020", summary: "Task 20" },
|
|
483
483
|
});
|
|
484
484
|
|
|
485
485
|
expect(updated.recentTasks).toHaveLength(20);
|
|
486
486
|
// Oldest task (beads-000) should be dropped
|
|
487
|
-
expect(updated.recentTasks[0]?.
|
|
488
|
-
expect(updated.recentTasks[19]?.
|
|
487
|
+
expect(updated.recentTasks[0]?.taskId).toBe("beads-001");
|
|
488
|
+
expect(updated.recentTasks[19]?.taskId).toBe("beads-020");
|
|
489
489
|
});
|
|
490
490
|
|
|
491
491
|
test("applies multiple updates simultaneously", async () => {
|
|
@@ -503,7 +503,7 @@ recentTasks: []
|
|
|
503
503
|
sessionsCompleted: 2,
|
|
504
504
|
expertiseDomains: ["testing", "architecture"],
|
|
505
505
|
completedTask: {
|
|
506
|
-
|
|
506
|
+
taskId: "beads-001",
|
|
507
507
|
summary: "Completed task",
|
|
508
508
|
},
|
|
509
509
|
});
|
|
@@ -554,12 +554,12 @@ recentTasks: []
|
|
|
554
554
|
expertiseDomains: ["typescript", "testing", "architecture"],
|
|
555
555
|
recentTasks: [
|
|
556
556
|
{
|
|
557
|
-
|
|
557
|
+
taskId: "beads-001",
|
|
558
558
|
summary: "Implemented feature X",
|
|
559
559
|
completedAt: "2024-01-15T12:00:00Z",
|
|
560
560
|
},
|
|
561
561
|
{
|
|
562
|
-
|
|
562
|
+
taskId: "beads-002",
|
|
563
563
|
summary: "Fixed bug in module Y",
|
|
564
564
|
completedAt: "2024-01-16T14:30:00Z",
|
|
565
565
|
},
|
|
@@ -587,7 +587,7 @@ recentTasks: []
|
|
|
587
587
|
],
|
|
588
588
|
recentTasks: [
|
|
589
589
|
{
|
|
590
|
-
|
|
590
|
+
taskId: "beads-001",
|
|
591
591
|
summary: 'Summary with "quotes" and: colons',
|
|
592
592
|
completedAt: "2024-01-15T12:00:00Z",
|
|
593
593
|
},
|
package/src/agents/identity.ts
CHANGED
|
@@ -39,7 +39,7 @@ function serializeIdentityYaml(identity: AgentIdentity): string {
|
|
|
39
39
|
} else {
|
|
40
40
|
lines.push("recentTasks:");
|
|
41
41
|
for (const task of identity.recentTasks) {
|
|
42
|
-
lines.push(`\t-
|
|
42
|
+
lines.push(`\t- taskId: ${quoteIfNeeded(task.taskId)}`);
|
|
43
43
|
lines.push(`\t\tsummary: ${quoteIfNeeded(task.summary)}`);
|
|
44
44
|
lines.push(`\t\tcompletedAt: ${quoteIfNeeded(task.completedAt)}`);
|
|
45
45
|
}
|
|
@@ -82,7 +82,7 @@ function quoteIfNeeded(value: string): string {
|
|
|
82
82
|
* This is a purpose-built parser for the identity YAML format. It handles:
|
|
83
83
|
* - Simple key: value pairs (strings, numbers)
|
|
84
84
|
* - Arrays of scalars (expertiseDomains)
|
|
85
|
-
* - Arrays of objects (recentTasks with
|
|
85
|
+
* - Arrays of objects (recentTasks with taskId, summary, completedAt)
|
|
86
86
|
* - Empty arrays (`[]`)
|
|
87
87
|
* - Quoted strings
|
|
88
88
|
* - Tab indentation
|
|
@@ -95,10 +95,10 @@ function parseIdentityYaml(text: string): AgentIdentity {
|
|
|
95
95
|
let created = "";
|
|
96
96
|
let sessionsCompleted = 0;
|
|
97
97
|
const expertiseDomains: string[] = [];
|
|
98
|
-
const recentTasks: Array<{
|
|
98
|
+
const recentTasks: Array<{ taskId: string; summary: string; completedAt: string }> = [];
|
|
99
99
|
|
|
100
100
|
let currentSection: "none" | "expertiseDomains" | "recentTasks" = "none";
|
|
101
|
-
let currentTask: {
|
|
101
|
+
let currentTask: { taskId: string; summary: string; completedAt: string } | null = null;
|
|
102
102
|
|
|
103
103
|
for (const rawLine of lines) {
|
|
104
104
|
const trimmed = rawLine.trim();
|
|
@@ -169,7 +169,7 @@ function parseIdentityYaml(text: string): AgentIdentity {
|
|
|
169
169
|
if (currentTask !== null) {
|
|
170
170
|
recentTasks.push(currentTask);
|
|
171
171
|
}
|
|
172
|
-
currentTask = {
|
|
172
|
+
currentTask = { taskId: "", summary: "", completedAt: "" };
|
|
173
173
|
|
|
174
174
|
// Parse the key-value on the same line as the dash
|
|
175
175
|
const itemContent = trimmed.slice(2).trim();
|
|
@@ -210,13 +210,13 @@ function parseIdentityYaml(text: string): AgentIdentity {
|
|
|
210
210
|
* Assign a parsed field value to a task object by key name.
|
|
211
211
|
*/
|
|
212
212
|
function assignTaskField(
|
|
213
|
-
task: {
|
|
213
|
+
task: { taskId: string; summary: string; completedAt: string },
|
|
214
214
|
key: string,
|
|
215
215
|
value: string,
|
|
216
216
|
): void {
|
|
217
217
|
switch (key) {
|
|
218
|
-
case "
|
|
219
|
-
task.
|
|
218
|
+
case "taskId":
|
|
219
|
+
task.taskId = value;
|
|
220
220
|
break;
|
|
221
221
|
case "summary":
|
|
222
222
|
task.summary = value;
|
|
@@ -336,7 +336,7 @@ export async function updateIdentity(
|
|
|
336
336
|
baseDir: string,
|
|
337
337
|
name: string,
|
|
338
338
|
update: Partial<Pick<AgentIdentity, "sessionsCompleted" | "expertiseDomains">> & {
|
|
339
|
-
completedTask?: {
|
|
339
|
+
completedTask?: { taskId: string; summary: string };
|
|
340
340
|
},
|
|
341
341
|
): Promise<AgentIdentity> {
|
|
342
342
|
const identity = await loadIdentity(baseDir, name);
|
|
@@ -364,7 +364,7 @@ export async function updateIdentity(
|
|
|
364
364
|
// Append completed task
|
|
365
365
|
if (update.completedTask !== undefined) {
|
|
366
366
|
identity.recentTasks.push({
|
|
367
|
-
|
|
367
|
+
taskId: update.completedTask.taskId,
|
|
368
368
|
summary: update.completedTask.summary,
|
|
369
369
|
completedAt: new Date().toISOString(),
|
|
370
370
|
});
|
|
@@ -23,7 +23,7 @@ describe("lifecycle", () => {
|
|
|
23
23
|
agentsDir,
|
|
24
24
|
agentName: "builder-1",
|
|
25
25
|
sessionId: "session-100",
|
|
26
|
-
|
|
26
|
+
taskId: "overstory-xyz1",
|
|
27
27
|
reason: "compaction",
|
|
28
28
|
progressSummary: "Built the widget",
|
|
29
29
|
pendingWork: "Tests remain",
|
|
@@ -56,7 +56,7 @@ describe("lifecycle", () => {
|
|
|
56
56
|
agentsDir,
|
|
57
57
|
agentName: "builder-2",
|
|
58
58
|
sessionId: "session-200",
|
|
59
|
-
|
|
59
|
+
taskId: "overstory-abc2",
|
|
60
60
|
reason: "crash",
|
|
61
61
|
progressSummary: "Halfway done",
|
|
62
62
|
pendingWork: "Finish implementation",
|
|
@@ -82,7 +82,7 @@ describe("lifecycle", () => {
|
|
|
82
82
|
agentsDir,
|
|
83
83
|
agentName: "builder-3",
|
|
84
84
|
sessionId: "session-300",
|
|
85
|
-
|
|
85
|
+
taskId: "overstory-def3",
|
|
86
86
|
reason: "manual",
|
|
87
87
|
progressSummary: "Done with phase 1",
|
|
88
88
|
pendingWork: "Phase 2",
|
|
@@ -116,7 +116,7 @@ describe("lifecycle", () => {
|
|
|
116
116
|
agentsDir,
|
|
117
117
|
agentName: "builder-4",
|
|
118
118
|
sessionId: "session-400",
|
|
119
|
-
|
|
119
|
+
taskId: "overstory-ghi4",
|
|
120
120
|
reason: "compaction",
|
|
121
121
|
progressSummary: "First session work",
|
|
122
122
|
pendingWork: "Continue",
|
|
@@ -137,7 +137,7 @@ describe("lifecycle", () => {
|
|
|
137
137
|
agentsDir,
|
|
138
138
|
agentName: "builder-4",
|
|
139
139
|
sessionId: "session-401",
|
|
140
|
-
|
|
140
|
+
taskId: "overstory-ghi4",
|
|
141
141
|
reason: "timeout",
|
|
142
142
|
progressSummary: "Second session work",
|
|
143
143
|
pendingWork: "Finish up",
|
|
@@ -172,7 +172,7 @@ describe("lifecycle", () => {
|
|
|
172
172
|
agentsDir,
|
|
173
173
|
agentName: "builder-5",
|
|
174
174
|
sessionId: "session-500",
|
|
175
|
-
|
|
175
|
+
taskId: "overstory-jkl5",
|
|
176
176
|
reason: "compaction",
|
|
177
177
|
progressSummary: "Done",
|
|
178
178
|
pendingWork: "Nothing",
|
package/src/agents/lifecycle.ts
CHANGED
|
@@ -73,7 +73,7 @@ export async function initiateHandoff(options: {
|
|
|
73
73
|
agentsDir: string;
|
|
74
74
|
agentName: string;
|
|
75
75
|
sessionId: string;
|
|
76
|
-
|
|
76
|
+
taskId: string;
|
|
77
77
|
reason: SessionHandoff["reason"];
|
|
78
78
|
progressSummary: string;
|
|
79
79
|
pendingWork: string;
|
|
@@ -83,7 +83,7 @@ export async function initiateHandoff(options: {
|
|
|
83
83
|
}): Promise<SessionHandoff> {
|
|
84
84
|
const checkpoint: SessionCheckpoint = {
|
|
85
85
|
agentName: options.agentName,
|
|
86
|
-
|
|
86
|
+
taskId: options.taskId,
|
|
87
87
|
sessionId: options.sessionId,
|
|
88
88
|
timestamp: new Date().toISOString(),
|
|
89
89
|
progressSummary: options.progressSummary,
|
|
@@ -25,7 +25,7 @@ Read your assignment. Execute immediately.
|
|
|
25
25
|
function makeConfig(overrides?: Partial<OverlayConfig>): OverlayConfig {
|
|
26
26
|
return {
|
|
27
27
|
agentName: "test-builder",
|
|
28
|
-
|
|
28
|
+
taskId: "overstory-abc",
|
|
29
29
|
specPath: ".overstory/specs/overstory-abc.md",
|
|
30
30
|
branchName: "agent/test-builder/overstory-abc",
|
|
31
31
|
worktreePath: "/tmp/test-project/.overstory/worktrees/test-builder",
|
|
@@ -48,8 +48,8 @@ describe("generateOverlay", () => {
|
|
|
48
48
|
expect(output).toContain("my-scout");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test("output contains
|
|
52
|
-
const config = makeConfig({
|
|
51
|
+
test("output contains task ID", async () => {
|
|
52
|
+
const config = makeConfig({ taskId: "overstory-xyz" });
|
|
53
53
|
const output = await generateOverlay(config);
|
|
54
54
|
|
|
55
55
|
expect(output).toContain("overstory-xyz");
|
|
@@ -134,7 +134,7 @@ describe("generateOverlay", () => {
|
|
|
134
134
|
const config = makeConfig({ mulchDomains: ["typescript", "testing"] });
|
|
135
135
|
const output = await generateOverlay(config);
|
|
136
136
|
|
|
137
|
-
expect(output).toContain("
|
|
137
|
+
expect(output).toContain("ml prime typescript testing");
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
test("empty mulch domains shows fallback text", async () => {
|
|
@@ -159,7 +159,7 @@ describe("generateOverlay", () => {
|
|
|
159
159
|
});
|
|
160
160
|
const output = await generateOverlay(config);
|
|
161
161
|
|
|
162
|
-
expect(output).toContain("
|
|
162
|
+
expect(output).toContain("ov sling");
|
|
163
163
|
expect(output).toContain("--parent lead-alpha");
|
|
164
164
|
expect(output).toContain("--depth 2");
|
|
165
165
|
});
|
|
@@ -261,13 +261,13 @@ describe("generateOverlay", () => {
|
|
|
261
261
|
const config = makeConfig({
|
|
262
262
|
capability: "scout",
|
|
263
263
|
agentName: "recon-1",
|
|
264
|
-
|
|
264
|
+
taskId: "overstory-task1",
|
|
265
265
|
parentAgent: "lead-alpha",
|
|
266
266
|
});
|
|
267
267
|
const output = await generateOverlay(config);
|
|
268
268
|
|
|
269
269
|
expect(output).toContain("bd close overstory-task1");
|
|
270
|
-
expect(output).toContain("
|
|
270
|
+
expect(output).toContain("ov mail send --to lead-alpha");
|
|
271
271
|
});
|
|
272
272
|
|
|
273
273
|
test("reviewer completion section uses coordinator when no parent", async () => {
|
|
@@ -284,8 +284,8 @@ describe("generateOverlay", () => {
|
|
|
284
284
|
const config = makeConfig({ agentName: "worker-42" });
|
|
285
285
|
const output = await generateOverlay(config);
|
|
286
286
|
|
|
287
|
-
expect(output).toContain("
|
|
288
|
-
expect(output).toContain("
|
|
287
|
+
expect(output).toContain("ov mail check --agent worker-42");
|
|
288
|
+
expect(output).toContain("ov mail send --to");
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
test("output includes base agent definition content (Layer 1)", async () => {
|
|
@@ -419,7 +419,7 @@ describe("generateOverlay", () => {
|
|
|
419
419
|
});
|
|
420
420
|
|
|
421
421
|
test("default trackerCli renders as bd in quality gates", async () => {
|
|
422
|
-
const config = makeConfig({ capability: "builder",
|
|
422
|
+
const config = makeConfig({ capability: "builder", taskId: "overstory-task1" });
|
|
423
423
|
const output = await generateOverlay(config);
|
|
424
424
|
|
|
425
425
|
expect(output).toContain("bd close overstory-task1");
|
|
@@ -429,7 +429,7 @@ describe("generateOverlay", () => {
|
|
|
429
429
|
const config = makeConfig({
|
|
430
430
|
capability: "builder",
|
|
431
431
|
trackerCli: "sd",
|
|
432
|
-
|
|
432
|
+
taskId: "overstory-test1",
|
|
433
433
|
});
|
|
434
434
|
const output = await generateOverlay(config);
|
|
435
435
|
|
|
@@ -451,7 +451,7 @@ describe("generateOverlay", () => {
|
|
|
451
451
|
const config = makeConfig({
|
|
452
452
|
capability: "scout",
|
|
453
453
|
trackerCli: "sd",
|
|
454
|
-
|
|
454
|
+
taskId: "overstory-test2",
|
|
455
455
|
});
|
|
456
456
|
const output = await generateOverlay(config);
|
|
457
457
|
|
|
@@ -482,7 +482,7 @@ describe("generateOverlay", () => {
|
|
|
482
482
|
});
|
|
483
483
|
|
|
484
484
|
test("defaults backward-compatible: no trackerCli/trackerName produces bd/beads", async () => {
|
|
485
|
-
const config = makeConfig({ capability: "builder",
|
|
485
|
+
const config = makeConfig({ capability: "builder", taskId: "overstory-back" });
|
|
486
486
|
const output = await generateOverlay(config);
|
|
487
487
|
|
|
488
488
|
expect(output).toContain("bd close overstory-back");
|
|
@@ -521,7 +521,7 @@ describe("writeOverlay", () => {
|
|
|
521
521
|
const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
|
|
522
522
|
const content = await Bun.file(outputPath).text();
|
|
523
523
|
expect(content).toContain("file-writer-test");
|
|
524
|
-
expect(content).toContain(config.
|
|
524
|
+
expect(content).toContain(config.taskId);
|
|
525
525
|
expect(content).toContain(config.branchName);
|
|
526
526
|
});
|
|
527
527
|
|
package/src/agents/overlay.ts
CHANGED
|
@@ -24,14 +24,14 @@ function formatFileScope(fileScope: readonly string[]): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Format mulch domains as a `
|
|
27
|
+
* Format mulch domains as a `ml prime` command.
|
|
28
28
|
* Returns a human-readable fallback if no domains are configured.
|
|
29
29
|
*/
|
|
30
30
|
function formatMulchDomains(domains: readonly string[]): string {
|
|
31
31
|
if (domains.length === 0) {
|
|
32
32
|
return "No specific expertise domains configured";
|
|
33
33
|
}
|
|
34
|
-
return `\`\`\`bash\
|
|
34
|
+
return `\`\`\`bash\nml prime ${domains.join(" ")}\n\`\`\``;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -90,9 +90,9 @@ function formatQualityGates(config: OverlayConfig): string {
|
|
|
90
90
|
"",
|
|
91
91
|
"Before reporting completion:",
|
|
92
92
|
"",
|
|
93
|
-
`1. **Record mulch learnings:** \`
|
|
94
|
-
`2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.
|
|
95
|
-
`3. **Send results:** \`
|
|
93
|
+
`1. **Record mulch learnings:** \`ml record <domain> --type <convention|pattern|reference> --description "..."\` — capture reusable knowledge from your work`,
|
|
94
|
+
`2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of findings"\``,
|
|
95
|
+
`3. **Send results:** \`ov mail send --to ${config.parentAgent ?? "coordinator"} --subject "done" --body "Summary" --type result --agent ${config.agentName}\``,
|
|
96
96
|
"",
|
|
97
97
|
"You are a read-only agent. Do NOT commit, modify files, or run quality gates.",
|
|
98
98
|
].join("\n");
|
|
@@ -112,12 +112,12 @@ function formatQualityGates(config: OverlayConfig): string {
|
|
|
112
112
|
"",
|
|
113
113
|
...gateLines,
|
|
114
114
|
`${gateLines.length + 1}. **Commit:** all changes committed to your branch (${config.branchName})`,
|
|
115
|
-
`${gateLines.length + 2}. **Record mulch learnings:** \`
|
|
116
|
-
`${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`
|
|
117
|
-
`${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.
|
|
115
|
+
`${gateLines.length + 2}. **Record mulch learnings:** \`ml record <domain> --type <convention|pattern|failure|decision> --description "..." --outcome-status success --outcome-agent ${config.agentName}\` — capture insights from your work`,
|
|
116
|
+
`${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`ov mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.taskId}" --body "Quality gates passed." --type worker_done --agent ${config.agentName}\``,
|
|
117
|
+
`${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of changes"\``,
|
|
118
118
|
"",
|
|
119
119
|
"Do NOT push to the canonical branch. Your work will be merged by the",
|
|
120
|
-
"coordinator via `
|
|
120
|
+
"coordinator via `ov merge`.",
|
|
121
121
|
].join("\n");
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -132,7 +132,7 @@ function formatConstraints(config: OverlayConfig): string {
|
|
|
132
132
|
"",
|
|
133
133
|
"- You are **read-only**: do NOT modify, create, or delete any files",
|
|
134
134
|
"- Do NOT commit, push, or make any git state changes",
|
|
135
|
-
`- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`
|
|
135
|
+
`- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`ov mail send --type result\``,
|
|
136
136
|
"- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
|
|
137
137
|
].join("\n");
|
|
138
138
|
}
|
|
@@ -145,7 +145,7 @@ function formatConstraints(config: OverlayConfig): string {
|
|
|
145
145
|
"- Only modify files in your File Scope",
|
|
146
146
|
`- Commit only to your branch: ${config.branchName}`,
|
|
147
147
|
"- Never push to the canonical branch",
|
|
148
|
-
`- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`
|
|
148
|
+
`- Report completion via \`${config.trackerCli ?? "bd"} close\` AND \`ov mail send --type result\``,
|
|
149
149
|
"- If you encounter a blocking issue, send mail with `--priority urgent --type error`",
|
|
150
150
|
].join("\n");
|
|
151
151
|
}
|
|
@@ -159,10 +159,10 @@ function formatCanSpawn(config: OverlayConfig): string {
|
|
|
159
159
|
return "You may NOT spawn sub-workers.";
|
|
160
160
|
}
|
|
161
161
|
return [
|
|
162
|
-
"You may spawn sub-workers using `
|
|
162
|
+
"You may spawn sub-workers using `ov sling`. Example:",
|
|
163
163
|
"",
|
|
164
164
|
"```bash",
|
|
165
|
-
"
|
|
165
|
+
"ov sling <task-id> --capability builder --name <worker-name> \\",
|
|
166
166
|
` --parent ${config.agentName} --depth ${config.depth + 1}`,
|
|
167
167
|
"```",
|
|
168
168
|
].join("\n");
|
|
@@ -205,7 +205,7 @@ export async function generateOverlay(config: OverlayConfig): Promise<string> {
|
|
|
205
205
|
|
|
206
206
|
const replacements: Record<string, string> = {
|
|
207
207
|
"{{AGENT_NAME}}": config.agentName,
|
|
208
|
-
"{{BEAD_ID}}": config.
|
|
208
|
+
"{{BEAD_ID}}": config.taskId,
|
|
209
209
|
"{{SPEC_PATH}}": config.specPath ?? "No spec file provided",
|
|
210
210
|
"{{BRANCH_NAME}}": config.branchName,
|
|
211
211
|
"{{WORKTREE_PATH}}": config.worktreePath,
|
|
@@ -97,7 +97,7 @@ describe("discoverAgents", () => {
|
|
|
97
97
|
capability: "builder",
|
|
98
98
|
worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
|
|
99
99
|
branchName: "overstory/builder-test/task-123",
|
|
100
|
-
|
|
100
|
+
taskId: "task-123",
|
|
101
101
|
tmuxSession: "overstory-test-builder",
|
|
102
102
|
state: "working",
|
|
103
103
|
pid: 12345,
|
|
@@ -129,7 +129,7 @@ describe("discoverAgents", () => {
|
|
|
129
129
|
capability: "builder",
|
|
130
130
|
worktreePath: join(tempDir, ".overstory", "worktrees", "builder-test"),
|
|
131
131
|
branchName: "overstory/builder-test/task-123",
|
|
132
|
-
|
|
132
|
+
taskId: "task-123",
|
|
133
133
|
tmuxSession: "overstory-test-builder",
|
|
134
134
|
state: "working",
|
|
135
135
|
pid: 12345,
|
|
@@ -148,7 +148,7 @@ describe("discoverAgents", () => {
|
|
|
148
148
|
capability: "scout",
|
|
149
149
|
worktreePath: join(tempDir, ".overstory", "worktrees", "scout-test"),
|
|
150
150
|
branchName: "overstory/scout-test/task-456",
|
|
151
|
-
|
|
151
|
+
taskId: "task-456",
|
|
152
152
|
tmuxSession: "overstory-test-scout",
|
|
153
153
|
state: "working",
|
|
154
154
|
pid: 12346,
|
|
@@ -180,7 +180,7 @@ describe("discoverAgents", () => {
|
|
|
180
180
|
capability: "builder",
|
|
181
181
|
worktreePath: join(tempDir, ".overstory", "worktrees", "builder-working"),
|
|
182
182
|
branchName: "overstory/builder-working/task-123",
|
|
183
|
-
|
|
183
|
+
taskId: "task-123",
|
|
184
184
|
tmuxSession: "overstory-test-working",
|
|
185
185
|
state: "working",
|
|
186
186
|
pid: 12345,
|
|
@@ -199,7 +199,7 @@ describe("discoverAgents", () => {
|
|
|
199
199
|
capability: "builder",
|
|
200
200
|
worktreePath: join(tempDir, ".overstory", "worktrees", "builder-completed"),
|
|
201
201
|
branchName: "overstory/builder-completed/task-456",
|
|
202
|
-
|
|
202
|
+
taskId: "task-456",
|
|
203
203
|
tmuxSession: "overstory-test-completed",
|
|
204
204
|
state: "completed",
|
|
205
205
|
pid: null,
|