@os-eco/overstory-cli 0.6.4 → 0.6.5
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 +4 -4
- package/package.json +1 -1
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +37 -0
- package/src/agents/hooks-deployer.ts +15 -1
- 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 +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +5 -5
- package/src/commands/agents.ts +3 -3
- package/src/commands/clean.test.ts +5 -5
- package/src/commands/coordinator.test.ts +2 -2
- package/src/commands/coordinator.ts +1 -1
- package/src/commands/costs.test.ts +45 -45
- package/src/commands/dashboard.ts +3 -3
- package/src/commands/inspect.test.ts +16 -16
- package/src/commands/inspect.ts +1 -1
- package/src/commands/log.test.ts +21 -21
- package/src/commands/log.ts +7 -7
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/merge.test.ts +8 -8
- package/src/commands/merge.ts +8 -8
- package/src/commands/metrics.test.ts +6 -6
- package/src/commands/metrics.ts +1 -1
- package/src/commands/monitor.ts +1 -1
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/prime.test.ts +4 -4
- package/src/commands/prime.ts +6 -6
- package/src/commands/run.test.ts +1 -1
- package/src/commands/sling.test.ts +5 -5
- package/src/commands/sling.ts +13 -10
- package/src/commands/spec.test.ts +2 -2
- package/src/commands/spec.ts +8 -8
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +17 -16
- package/src/commands/stop.test.ts +1 -1
- package/src/commands/supervisor.test.ts +9 -9
- package/src/commands/supervisor.ts +11 -11
- package/src/commands/trace.test.ts +6 -6
- package/src/commands/trace.ts +6 -6
- package/src/commands/worktree.test.ts +205 -29
- package/src/commands/worktree.ts +47 -9
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +2 -2
- package/src/errors.ts +1 -1
- package/src/index.ts +3 -3
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- 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/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/README.md
CHANGED
|
@@ -222,7 +222,7 @@ overstory inspect <agent> Deep per-agent inspection
|
|
|
222
222
|
--no-tmux Skip tmux capture
|
|
223
223
|
--limit <n> Limit events shown
|
|
224
224
|
|
|
225
|
-
overstory spec write <
|
|
225
|
+
overstory spec write <task-id> Write a task specification
|
|
226
226
|
--body <content> Spec content (or pipe via stdin)
|
|
227
227
|
|
|
228
228
|
overstory errors Aggregated error view across agents
|
|
@@ -270,16 +270,16 @@ Global Flags:
|
|
|
270
270
|
## Tech Stack
|
|
271
271
|
|
|
272
272
|
- **Runtime**: Bun (TypeScript directly, no build step)
|
|
273
|
-
- **Dependencies**: Minimal runtime — `chalk` (color output), core I/O via Bun built-in APIs
|
|
273
|
+
- **Dependencies**: Minimal runtime — `chalk` (color output), `commander` (CLI framework), core I/O via Bun built-in APIs
|
|
274
274
|
- **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
|
|
275
275
|
- **Linting**: Biome (formatter + linter)
|
|
276
|
-
- **Testing**: `bun test` (
|
|
276
|
+
- **Testing**: `bun test` (2145 tests across 76 files, colocated with source)
|
|
277
277
|
- **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
|
|
278
278
|
|
|
279
279
|
## Development
|
|
280
280
|
|
|
281
281
|
```bash
|
|
282
|
-
# Run tests (
|
|
282
|
+
# Run tests (2145 tests across 76 files)
|
|
283
283
|
bun test
|
|
284
284
|
|
|
285
285
|
# Run a single test
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-eco/overstory-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code — spawn worker agents in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution",
|
|
5
5
|
"author": "Jaymin West",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,7 @@ import { clearCheckpoint, loadCheckpoint, saveCheckpoint } from "./checkpoint.ts
|
|
|
9
9
|
function makeCheckpoint(overrides?: Partial<SessionCheckpoint>): SessionCheckpoint {
|
|
10
10
|
return {
|
|
11
11
|
agentName: "test-agent",
|
|
12
|
-
|
|
12
|
+
taskId: "overstory-abc1",
|
|
13
13
|
sessionId: "session-001",
|
|
14
14
|
timestamp: "2025-01-01T00:00:00.000Z",
|
|
15
15
|
progressSummary: "Implemented checkpoint module",
|
|
@@ -40,7 +40,7 @@ describe("checkpoint", () => {
|
|
|
40
40
|
|
|
41
41
|
expect(loaded).not.toBeNull();
|
|
42
42
|
expect(loaded?.agentName).toBe("test-agent");
|
|
43
|
-
expect(loaded?.
|
|
43
|
+
expect(loaded?.taskId).toBe("overstory-abc1");
|
|
44
44
|
expect(loaded?.sessionId).toBe("session-001");
|
|
45
45
|
expect(loaded?.progressSummary).toBe("Implemented checkpoint module");
|
|
46
46
|
expect(loaded?.filesModified).toEqual(["src/agents/checkpoint.ts"]);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
buildBashPathBoundaryScript,
|
|
9
9
|
buildPathBoundaryGuardScript,
|
|
10
10
|
deployHooks,
|
|
11
|
+
escapeForSingleQuotedShell,
|
|
11
12
|
getBashPathBoundaryGuards,
|
|
12
13
|
getCapabilityGuards,
|
|
13
14
|
getDangerGuards,
|
|
@@ -2116,3 +2117,39 @@ describe("bash path boundary integration", () => {
|
|
|
2116
2117
|
expect(universalGuard.hooks[0].command).toContain('"decision":"block"');
|
|
2117
2118
|
});
|
|
2118
2119
|
});
|
|
2120
|
+
|
|
2121
|
+
describe("escapeForSingleQuotedShell", () => {
|
|
2122
|
+
test("no single quotes: string passes through unchanged", () => {
|
|
2123
|
+
expect(escapeForSingleQuotedShell("hello world")).toBe("hello world");
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
test("single quotes escaped: it's becomes it'\\''s", () => {
|
|
2127
|
+
expect(escapeForSingleQuotedShell("it's")).toBe("it'\\''s");
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
test("multiple single quotes: each one is escaped independently", () => {
|
|
2131
|
+
expect(escapeForSingleQuotedShell("can't won't")).toBe("can'\\''t won'\\''t");
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
test("empty string: returns empty string", () => {
|
|
2135
|
+
expect(escapeForSingleQuotedShell("")).toBe("");
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
test("blockGuard shell command outputs valid JSON when executed", async () => {
|
|
2139
|
+
const guards = getCapabilityGuards("builder");
|
|
2140
|
+
const taskGuard = guards.find((g) => g.matcher === "Task");
|
|
2141
|
+
expect(taskGuard).toBeDefined();
|
|
2142
|
+
const cmd = taskGuard?.hooks[0]?.command ?? "";
|
|
2143
|
+
const echoCmd = cmd.replace('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0; ', "");
|
|
2144
|
+
const proc = Bun.spawn(["sh", "-c", echoCmd], {
|
|
2145
|
+
stdout: "pipe",
|
|
2146
|
+
stderr: "pipe",
|
|
2147
|
+
env: { ...process.env, OVERSTORY_AGENT_NAME: "test-agent" },
|
|
2148
|
+
});
|
|
2149
|
+
const output = await new Response(proc.stdout).text();
|
|
2150
|
+
await proc.exited;
|
|
2151
|
+
const parsed = JSON.parse(output.trim());
|
|
2152
|
+
expect(parsed.decision).toBe("block");
|
|
2153
|
+
expect(parsed.reason).toContain("overstory sling");
|
|
2154
|
+
});
|
|
2155
|
+
});
|
|
@@ -205,6 +205,20 @@ export function getPathBoundaryGuards(): HookEntry[] {
|
|
|
205
205
|
];
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Escape a string for use inside a single-quoted POSIX shell string.
|
|
210
|
+
*
|
|
211
|
+
* POSIX single-quoted strings cannot contain single quotes at all.
|
|
212
|
+
* The standard technique is to end the single-quoted segment, emit an escaped
|
|
213
|
+
* single quote using $'\'', then start a new single-quoted segment:
|
|
214
|
+
* 'it'\''s fine' → it's fine
|
|
215
|
+
*
|
|
216
|
+
* Exported so tests can verify escaping directly.
|
|
217
|
+
*/
|
|
218
|
+
export function escapeForSingleQuotedShell(str: string): string {
|
|
219
|
+
return str.replace(/'/g, "'\\''");
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
/**
|
|
209
223
|
* Build a PreToolUse guard that blocks a specific tool.
|
|
210
224
|
*
|
|
@@ -218,7 +232,7 @@ function blockGuard(toolName: string, reason: string): HookEntry {
|
|
|
218
232
|
hooks: [
|
|
219
233
|
{
|
|
220
234
|
type: "command",
|
|
221
|
-
command: `${ENV_GUARD} echo '${response}'`,
|
|
235
|
+
command: `${ENV_GUARD} echo '${escapeForSingleQuotedShell(response)}'`,
|
|
222
236
|
},
|
|
223
237
|
],
|
|
224
238
|
};
|
|
@@ -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");
|
|
@@ -261,7 +261,7 @@ 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);
|
|
@@ -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
|
@@ -91,7 +91,7 @@ function formatQualityGates(config: OverlayConfig): string {
|
|
|
91
91
|
"Before reporting completion:",
|
|
92
92
|
"",
|
|
93
93
|
`1. **Record mulch learnings:** \`mulch record <domain> --type <convention|pattern|reference> --description "..."\` — capture reusable knowledge from your work`,
|
|
94
|
-
`2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.
|
|
94
|
+
`2. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.taskId} --reason "summary of findings"\``,
|
|
95
95
|
`3. **Send results:** \`overstory 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.",
|
|
@@ -113,8 +113,8 @@ function formatQualityGates(config: OverlayConfig): string {
|
|
|
113
113
|
...gateLines,
|
|
114
114
|
`${gateLines.length + 1}. **Commit:** all changes committed to your branch (${config.branchName})`,
|
|
115
115
|
`${gateLines.length + 2}. **Record mulch learnings:** \`mulch 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"}: \`overstory mail send --to ${config.parentAgent ?? "coordinator"} --subject "Worker done: ${config.
|
|
117
|
-
`${gateLines.length + 4}. **Close issue:** \`${config.trackerCli ?? "bd"} close ${config.
|
|
116
|
+
`${gateLines.length + 3}. **Signal completion:** send \`worker_done\` mail to ${config.parentAgent ?? "coordinator"}: \`overstory 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
120
|
"coordinator via `overstory merge`.",
|
|
@@ -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,
|
package/src/commands/agents.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface DiscoveredAgent {
|
|
|
18
18
|
agentName: string;
|
|
19
19
|
capability: string;
|
|
20
20
|
state: string;
|
|
21
|
-
|
|
21
|
+
taskId: string;
|
|
22
22
|
branchName: string;
|
|
23
23
|
parentAgent: string | null;
|
|
24
24
|
depth: number;
|
|
@@ -117,7 +117,7 @@ export async function discoverAgents(
|
|
|
117
117
|
agentName: session.agentName,
|
|
118
118
|
capability: session.capability,
|
|
119
119
|
state: session.state,
|
|
120
|
-
|
|
120
|
+
taskId: session.taskId,
|
|
121
121
|
branchName: session.branchName,
|
|
122
122
|
parentAgent: session.parentAgent,
|
|
123
123
|
depth: session.depth,
|
|
@@ -166,7 +166,7 @@ function printAgents(agents: DiscoveredAgent[]): void {
|
|
|
166
166
|
for (const agent of agents) {
|
|
167
167
|
const icon = getStateIcon(agent.state);
|
|
168
168
|
w(` ${icon} ${agent.agentName} [${agent.capability}]\n`);
|
|
169
|
-
w(` State: ${agent.state} | Task: ${agent.
|
|
169
|
+
w(` State: ${agent.state} | Task: ${agent.taskId}\n`);
|
|
170
170
|
w(` Branch: ${agent.branchName}\n`);
|
|
171
171
|
w(` Parent: ${agent.parentAgent ?? "none"} | Depth: ${agent.depth}\n`);
|
|
172
172
|
|