@os-eco/overstory-cli 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { AgentError } from "../errors.ts";
|
|
6
|
+
import type { OverlayConfig, QualityGate } from "../types.ts";
|
|
7
|
+
import { generateOverlay, isCanonicalRoot, writeOverlay } from "./overlay.ts";
|
|
8
|
+
|
|
9
|
+
const SAMPLE_BASE_DEFINITION = `# Builder Agent
|
|
10
|
+
|
|
11
|
+
You are a **builder agent** in the overstory swarm system.
|
|
12
|
+
|
|
13
|
+
## Role
|
|
14
|
+
Implement changes according to a spec.
|
|
15
|
+
|
|
16
|
+
## Propulsion Principle
|
|
17
|
+
Read your assignment. Execute immediately.
|
|
18
|
+
|
|
19
|
+
## Failure Modes
|
|
20
|
+
- FILE_SCOPE_VIOLATION
|
|
21
|
+
- SILENT_FAILURE
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
/** Build a complete OverlayConfig with sensible defaults, overrideable by partial. */
|
|
25
|
+
function makeConfig(overrides?: Partial<OverlayConfig>): OverlayConfig {
|
|
26
|
+
return {
|
|
27
|
+
agentName: "test-builder",
|
|
28
|
+
beadId: "overstory-abc",
|
|
29
|
+
specPath: ".overstory/specs/overstory-abc.md",
|
|
30
|
+
branchName: "agent/test-builder/overstory-abc",
|
|
31
|
+
worktreePath: "/tmp/test-project/.overstory/worktrees/test-builder",
|
|
32
|
+
fileScope: ["src/agents/manifest.ts", "src/agents/overlay.ts"],
|
|
33
|
+
mulchDomains: ["typescript", "testing"],
|
|
34
|
+
parentAgent: "lead-alpha",
|
|
35
|
+
depth: 1,
|
|
36
|
+
canSpawn: false,
|
|
37
|
+
capability: "builder",
|
|
38
|
+
baseDefinition: SAMPLE_BASE_DEFINITION,
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("generateOverlay", () => {
|
|
44
|
+
test("output contains agent name", async () => {
|
|
45
|
+
const config = makeConfig({ agentName: "my-scout" });
|
|
46
|
+
const output = await generateOverlay(config);
|
|
47
|
+
|
|
48
|
+
expect(output).toContain("my-scout");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("output contains bead ID", async () => {
|
|
52
|
+
const config = makeConfig({ beadId: "overstory-xyz" });
|
|
53
|
+
const output = await generateOverlay(config);
|
|
54
|
+
|
|
55
|
+
expect(output).toContain("overstory-xyz");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("output contains branch name", async () => {
|
|
59
|
+
const config = makeConfig({ branchName: "agent/scout/overstory-xyz" });
|
|
60
|
+
const output = await generateOverlay(config);
|
|
61
|
+
|
|
62
|
+
expect(output).toContain("agent/scout/overstory-xyz");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("output contains parent agent name", async () => {
|
|
66
|
+
const config = makeConfig({ parentAgent: "lead-bravo" });
|
|
67
|
+
const output = await generateOverlay(config);
|
|
68
|
+
|
|
69
|
+
expect(output).toContain("lead-bravo");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("output contains depth", async () => {
|
|
73
|
+
const config = makeConfig({ depth: 2 });
|
|
74
|
+
const output = await generateOverlay(config);
|
|
75
|
+
|
|
76
|
+
expect(output).toContain("2");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("output contains spec path when provided", async () => {
|
|
80
|
+
const config = makeConfig({ specPath: ".overstory/specs/my-task.md" });
|
|
81
|
+
const output = await generateOverlay(config);
|
|
82
|
+
|
|
83
|
+
expect(output).toContain(".overstory/specs/my-task.md");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("shows fallback text when specPath is null", async () => {
|
|
87
|
+
const config = makeConfig({ specPath: null });
|
|
88
|
+
const output = await generateOverlay(config);
|
|
89
|
+
|
|
90
|
+
expect(output).toContain("No spec file provided");
|
|
91
|
+
expect(output).not.toContain("{{SPEC_PATH}}");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("includes 'Read your task spec' instruction when spec provided", async () => {
|
|
95
|
+
const config = makeConfig({ specPath: ".overstory/specs/my-task.md" });
|
|
96
|
+
const output = await generateOverlay(config);
|
|
97
|
+
|
|
98
|
+
expect(output).toContain("Read your task spec at the path above");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("does not include 'Read your task spec' instruction when specPath is null", async () => {
|
|
102
|
+
const config = makeConfig({ specPath: null });
|
|
103
|
+
const output = await generateOverlay(config);
|
|
104
|
+
|
|
105
|
+
expect(output).not.toContain("Read your task spec at the path above");
|
|
106
|
+
expect(output).toContain("No task spec was provided");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("shows 'coordinator' when parentAgent is null", async () => {
|
|
110
|
+
const config = makeConfig({ parentAgent: null });
|
|
111
|
+
const output = await generateOverlay(config);
|
|
112
|
+
|
|
113
|
+
expect(output).toContain("coordinator");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("file scope is formatted as markdown bullets", async () => {
|
|
117
|
+
const config = makeConfig({
|
|
118
|
+
fileScope: ["src/foo.ts", "src/bar.ts"],
|
|
119
|
+
});
|
|
120
|
+
const output = await generateOverlay(config);
|
|
121
|
+
|
|
122
|
+
expect(output).toContain("- `src/foo.ts`");
|
|
123
|
+
expect(output).toContain("- `src/bar.ts`");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("empty file scope shows fallback text", async () => {
|
|
127
|
+
const config = makeConfig({ fileScope: [] });
|
|
128
|
+
const output = await generateOverlay(config);
|
|
129
|
+
|
|
130
|
+
expect(output).toContain("No file scope restrictions");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("mulch domains formatted as prime command", async () => {
|
|
134
|
+
const config = makeConfig({ mulchDomains: ["typescript", "testing"] });
|
|
135
|
+
const output = await generateOverlay(config);
|
|
136
|
+
|
|
137
|
+
expect(output).toContain("mulch prime typescript testing");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("empty mulch domains shows fallback text", async () => {
|
|
141
|
+
const config = makeConfig({ mulchDomains: [] });
|
|
142
|
+
const output = await generateOverlay(config);
|
|
143
|
+
|
|
144
|
+
expect(output).toContain("No specific expertise domains configured");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("canSpawn false says 'You may NOT spawn sub-workers'", async () => {
|
|
148
|
+
const config = makeConfig({ canSpawn: false });
|
|
149
|
+
const output = await generateOverlay(config);
|
|
150
|
+
|
|
151
|
+
expect(output).toContain("You may NOT spawn sub-workers");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("canSpawn true includes sling example", async () => {
|
|
155
|
+
const config = makeConfig({
|
|
156
|
+
canSpawn: true,
|
|
157
|
+
agentName: "lead-alpha",
|
|
158
|
+
depth: 1,
|
|
159
|
+
});
|
|
160
|
+
const output = await generateOverlay(config);
|
|
161
|
+
|
|
162
|
+
expect(output).toContain("overstory sling");
|
|
163
|
+
expect(output).toContain("--parent lead-alpha");
|
|
164
|
+
expect(output).toContain("--depth 2");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("no unreplaced placeholders remain in output", async () => {
|
|
168
|
+
const config = makeConfig();
|
|
169
|
+
const output = await generateOverlay(config);
|
|
170
|
+
|
|
171
|
+
expect(output).not.toContain("{{");
|
|
172
|
+
expect(output).not.toContain("}}");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("includes pre-loaded expertise when mulchExpertise is provided", async () => {
|
|
176
|
+
const config = makeConfig({
|
|
177
|
+
mulchExpertise: "## architecture\n- Pattern: use singleton for config loader",
|
|
178
|
+
});
|
|
179
|
+
const output = await generateOverlay(config);
|
|
180
|
+
|
|
181
|
+
expect(output).toContain("### Pre-loaded Expertise");
|
|
182
|
+
expect(output).toContain("automatically loaded at spawn time");
|
|
183
|
+
expect(output).toContain("## architecture");
|
|
184
|
+
expect(output).toContain("Pattern: use singleton for config loader");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("omits expertise section when mulchExpertise is undefined", async () => {
|
|
188
|
+
const config = makeConfig({ mulchExpertise: undefined });
|
|
189
|
+
const output = await generateOverlay(config);
|
|
190
|
+
|
|
191
|
+
expect(output).not.toContain("### Pre-loaded Expertise");
|
|
192
|
+
expect(output).not.toContain("automatically loaded at spawn time");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("omits expertise section when mulchExpertise is empty string", async () => {
|
|
196
|
+
const config = makeConfig({ mulchExpertise: "" });
|
|
197
|
+
const output = await generateOverlay(config);
|
|
198
|
+
|
|
199
|
+
expect(output).not.toContain("### Pre-loaded Expertise");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("omits expertise section when mulchExpertise is whitespace only", async () => {
|
|
203
|
+
const config = makeConfig({ mulchExpertise: " \n\t \n " });
|
|
204
|
+
const output = await generateOverlay(config);
|
|
205
|
+
|
|
206
|
+
expect(output).not.toContain("### Pre-loaded Expertise");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("builder capability includes full quality gates section", async () => {
|
|
210
|
+
const config = makeConfig({ capability: "builder" });
|
|
211
|
+
const output = await generateOverlay(config);
|
|
212
|
+
|
|
213
|
+
expect(output).toContain("Quality Gates");
|
|
214
|
+
expect(output).toContain("bun test");
|
|
215
|
+
expect(output).toContain("bun run lint");
|
|
216
|
+
expect(output).toContain("Commit");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("lead capability includes full quality gates section", async () => {
|
|
220
|
+
const config = makeConfig({ capability: "lead" });
|
|
221
|
+
const output = await generateOverlay(config);
|
|
222
|
+
|
|
223
|
+
expect(output).toContain("Quality Gates");
|
|
224
|
+
expect(output).toContain("bun test");
|
|
225
|
+
expect(output).toContain("bun run lint");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("merger capability includes full quality gates section", async () => {
|
|
229
|
+
const config = makeConfig({ capability: "merger" });
|
|
230
|
+
const output = await generateOverlay(config);
|
|
231
|
+
|
|
232
|
+
expect(output).toContain("Quality Gates");
|
|
233
|
+
expect(output).toContain("bun test");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("scout capability gets read-only completion section instead of quality gates", async () => {
|
|
237
|
+
const config = makeConfig({ capability: "scout", agentName: "my-scout" });
|
|
238
|
+
const output = await generateOverlay(config);
|
|
239
|
+
|
|
240
|
+
expect(output).toContain("Completion");
|
|
241
|
+
expect(output).toContain("read-only agent");
|
|
242
|
+
expect(output).toContain("Do NOT commit");
|
|
243
|
+
expect(output).not.toContain("Quality Gates");
|
|
244
|
+
expect(output).not.toContain("bun test");
|
|
245
|
+
expect(output).not.toContain("bun run lint");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("reviewer capability gets read-only completion section instead of quality gates", async () => {
|
|
249
|
+
const config = makeConfig({ capability: "reviewer", agentName: "my-reviewer" });
|
|
250
|
+
const output = await generateOverlay(config);
|
|
251
|
+
|
|
252
|
+
expect(output).toContain("Completion");
|
|
253
|
+
expect(output).toContain("read-only agent");
|
|
254
|
+
expect(output).toContain("Do NOT commit");
|
|
255
|
+
expect(output).not.toContain("Quality Gates");
|
|
256
|
+
expect(output).not.toContain("bun test");
|
|
257
|
+
expect(output).not.toContain("bun run lint");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("scout completion section includes bd close and mail send", async () => {
|
|
261
|
+
const config = makeConfig({
|
|
262
|
+
capability: "scout",
|
|
263
|
+
agentName: "recon-1",
|
|
264
|
+
beadId: "overstory-task1",
|
|
265
|
+
parentAgent: "lead-alpha",
|
|
266
|
+
});
|
|
267
|
+
const output = await generateOverlay(config);
|
|
268
|
+
|
|
269
|
+
expect(output).toContain("bd close overstory-task1");
|
|
270
|
+
expect(output).toContain("overstory mail send --to lead-alpha");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("reviewer completion section uses coordinator when no parent", async () => {
|
|
274
|
+
const config = makeConfig({
|
|
275
|
+
capability: "reviewer",
|
|
276
|
+
parentAgent: null,
|
|
277
|
+
});
|
|
278
|
+
const output = await generateOverlay(config);
|
|
279
|
+
|
|
280
|
+
expect(output).toContain("--to coordinator");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("output includes communication section with agent address", async () => {
|
|
284
|
+
const config = makeConfig({ agentName: "worker-42" });
|
|
285
|
+
const output = await generateOverlay(config);
|
|
286
|
+
|
|
287
|
+
expect(output).toContain("overstory mail check --agent worker-42");
|
|
288
|
+
expect(output).toContain("overstory mail send --to");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("output includes base agent definition content (Layer 1)", async () => {
|
|
292
|
+
const config = makeConfig();
|
|
293
|
+
const output = await generateOverlay(config);
|
|
294
|
+
|
|
295
|
+
expect(output).toContain("# Builder Agent");
|
|
296
|
+
expect(output).toContain("Propulsion Principle");
|
|
297
|
+
expect(output).toContain("FILE_SCOPE_VIOLATION");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("base definition appears before task assignment section", async () => {
|
|
301
|
+
const config = makeConfig();
|
|
302
|
+
const output = await generateOverlay(config);
|
|
303
|
+
|
|
304
|
+
const baseDefIndex = output.indexOf("# Builder Agent");
|
|
305
|
+
const assignmentIndex = output.indexOf("## Your Assignment");
|
|
306
|
+
expect(baseDefIndex).toBeGreaterThan(-1);
|
|
307
|
+
expect(assignmentIndex).toBeGreaterThan(-1);
|
|
308
|
+
expect(baseDefIndex).toBeLessThan(assignmentIndex);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("output contains worktree path in assignment section", async () => {
|
|
312
|
+
const config = makeConfig({
|
|
313
|
+
worktreePath: "/project/.overstory/worktrees/my-builder",
|
|
314
|
+
});
|
|
315
|
+
const output = await generateOverlay(config);
|
|
316
|
+
|
|
317
|
+
expect(output).toContain("/project/.overstory/worktrees/my-builder");
|
|
318
|
+
expect(output).toContain("**Worktree:**");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("output contains Working Directory section with worktree path", async () => {
|
|
322
|
+
const config = makeConfig({
|
|
323
|
+
worktreePath: "/tmp/worktrees/builder-1",
|
|
324
|
+
});
|
|
325
|
+
const output = await generateOverlay(config);
|
|
326
|
+
|
|
327
|
+
expect(output).toContain("## Working Directory");
|
|
328
|
+
expect(output).toContain("Your worktree root is: `/tmp/worktrees/builder-1`");
|
|
329
|
+
expect(output).toContain("PATH_BOUNDARY_VIOLATION");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("file scope section references worktree root", async () => {
|
|
333
|
+
const config = makeConfig({
|
|
334
|
+
worktreePath: "/tmp/worktrees/builder-scope",
|
|
335
|
+
});
|
|
336
|
+
const output = await generateOverlay(config);
|
|
337
|
+
|
|
338
|
+
expect(output).toContain(
|
|
339
|
+
"These paths are relative to your worktree root: `/tmp/worktrees/builder-scope`",
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("builder constraints include worktree isolation", async () => {
|
|
344
|
+
const config = makeConfig({
|
|
345
|
+
capability: "builder",
|
|
346
|
+
worktreePath: "/tmp/worktrees/builder-constraints",
|
|
347
|
+
});
|
|
348
|
+
const output = await generateOverlay(config);
|
|
349
|
+
|
|
350
|
+
expect(output).toContain("WORKTREE ISOLATION");
|
|
351
|
+
expect(output).toContain("/tmp/worktrees/builder-constraints");
|
|
352
|
+
expect(output).toContain("NEVER write to the canonical repo root");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("no unreplaced WORKTREE_PATH placeholders", async () => {
|
|
356
|
+
const config = makeConfig();
|
|
357
|
+
const output = await generateOverlay(config);
|
|
358
|
+
|
|
359
|
+
expect(output).not.toContain("{{WORKTREE_PATH}}");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("builder with custom qualityGates uses them instead of defaults", async () => {
|
|
363
|
+
const gates: QualityGate[] = [
|
|
364
|
+
{ name: "Test", command: "pytest", description: "all tests pass" },
|
|
365
|
+
{ name: "Lint", command: "ruff check .", description: "no lint errors" },
|
|
366
|
+
];
|
|
367
|
+
const config = makeConfig({ capability: "builder", qualityGates: gates });
|
|
368
|
+
const output = await generateOverlay(config);
|
|
369
|
+
|
|
370
|
+
expect(output).toContain("pytest");
|
|
371
|
+
expect(output).toContain("ruff check .");
|
|
372
|
+
expect(output).not.toContain("bun test");
|
|
373
|
+
expect(output).not.toContain("bun run lint");
|
|
374
|
+
expect(output).not.toContain("bun run typecheck");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("builder with undefined qualityGates falls back to defaults", async () => {
|
|
378
|
+
const config = makeConfig({ capability: "builder", qualityGates: undefined });
|
|
379
|
+
const output = await generateOverlay(config);
|
|
380
|
+
|
|
381
|
+
expect(output).toContain("bun test");
|
|
382
|
+
expect(output).toContain("bun run lint");
|
|
383
|
+
expect(output).toContain("bun run typecheck");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("builder with empty qualityGates array falls back to defaults", async () => {
|
|
387
|
+
const config = makeConfig({ capability: "builder", qualityGates: [] });
|
|
388
|
+
const output = await generateOverlay(config);
|
|
389
|
+
|
|
390
|
+
expect(output).toContain("bun test");
|
|
391
|
+
expect(output).toContain("bun run lint");
|
|
392
|
+
expect(output).toContain("bun run typecheck");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("custom qualityGates are numbered correctly", async () => {
|
|
396
|
+
const gates: QualityGate[] = [
|
|
397
|
+
{ name: "Build", command: "cargo build", description: "compilation succeeds" },
|
|
398
|
+
{ name: "Test", command: "cargo test", description: "all tests pass" },
|
|
399
|
+
];
|
|
400
|
+
const config = makeConfig({ capability: "builder", qualityGates: gates });
|
|
401
|
+
const output = await generateOverlay(config);
|
|
402
|
+
|
|
403
|
+
expect(output).toContain("1. **Build:**");
|
|
404
|
+
expect(output).toContain("2. **Test:**");
|
|
405
|
+
// Commit should be item 3
|
|
406
|
+
expect(output).toContain("3. **Commit:**");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("scout capability ignores qualityGates (stays read-only)", async () => {
|
|
410
|
+
const gates: QualityGate[] = [
|
|
411
|
+
{ name: "Test", command: "pytest", description: "all tests pass" },
|
|
412
|
+
];
|
|
413
|
+
const config = makeConfig({ capability: "scout", qualityGates: gates });
|
|
414
|
+
const output = await generateOverlay(config);
|
|
415
|
+
|
|
416
|
+
expect(output).toContain("read-only agent");
|
|
417
|
+
expect(output).not.toContain("pytest");
|
|
418
|
+
expect(output).not.toContain("Quality Gates");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("default trackerCli renders as bd in quality gates", async () => {
|
|
422
|
+
const config = makeConfig({ capability: "builder", beadId: "overstory-task1" });
|
|
423
|
+
const output = await generateOverlay(config);
|
|
424
|
+
|
|
425
|
+
expect(output).toContain("bd close overstory-task1");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("custom trackerCli replaces bd in quality gates", async () => {
|
|
429
|
+
const config = makeConfig({
|
|
430
|
+
capability: "builder",
|
|
431
|
+
trackerCli: "sd",
|
|
432
|
+
beadId: "overstory-test1",
|
|
433
|
+
});
|
|
434
|
+
const output = await generateOverlay(config);
|
|
435
|
+
|
|
436
|
+
expect(output).toContain("sd close overstory-test1");
|
|
437
|
+
expect(output).not.toContain("bd close");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("custom trackerCli replaces bd in constraints", async () => {
|
|
441
|
+
const config = makeConfig({
|
|
442
|
+
capability: "builder",
|
|
443
|
+
trackerCli: "sd",
|
|
444
|
+
});
|
|
445
|
+
const output = await generateOverlay(config);
|
|
446
|
+
|
|
447
|
+
expect(output).toContain("`sd close`");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("custom trackerCli replaces bd in read-only completion section", async () => {
|
|
451
|
+
const config = makeConfig({
|
|
452
|
+
capability: "scout",
|
|
453
|
+
trackerCli: "sd",
|
|
454
|
+
beadId: "overstory-test2",
|
|
455
|
+
});
|
|
456
|
+
const output = await generateOverlay(config);
|
|
457
|
+
|
|
458
|
+
expect(output).toContain("sd close overstory-test2");
|
|
459
|
+
expect(output).not.toContain("bd close");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("TRACKER_CLI in base definition is replaced", async () => {
|
|
463
|
+
const config = makeConfig({
|
|
464
|
+
trackerCli: "sd",
|
|
465
|
+
baseDefinition: "Run `{{TRACKER_CLI}} show` to check status.",
|
|
466
|
+
});
|
|
467
|
+
const output = await generateOverlay(config);
|
|
468
|
+
|
|
469
|
+
expect(output).toContain("Run `sd show` to check status.");
|
|
470
|
+
expect(output).not.toContain("{{TRACKER_CLI}}");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("TRACKER_NAME in base definition is replaced", async () => {
|
|
474
|
+
const config = makeConfig({
|
|
475
|
+
trackerName: "seeds",
|
|
476
|
+
baseDefinition: "Close your {{TRACKER_NAME}} issue when done.",
|
|
477
|
+
});
|
|
478
|
+
const output = await generateOverlay(config);
|
|
479
|
+
|
|
480
|
+
expect(output).toContain("Close your seeds issue when done.");
|
|
481
|
+
expect(output).not.toContain("{{TRACKER_NAME}}");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("defaults backward-compatible: no trackerCli/trackerName produces bd/beads", async () => {
|
|
485
|
+
const config = makeConfig({ capability: "builder", beadId: "overstory-back" });
|
|
486
|
+
const output = await generateOverlay(config);
|
|
487
|
+
|
|
488
|
+
expect(output).toContain("bd close overstory-back");
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe("writeOverlay", () => {
|
|
493
|
+
let tempDir: string;
|
|
494
|
+
|
|
495
|
+
beforeEach(async () => {
|
|
496
|
+
tempDir = await mkdtemp(join(tmpdir(), "overstory-overlay-test-"));
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
afterEach(async () => {
|
|
500
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("creates .claude/CLAUDE.md in worktree directory", async () => {
|
|
504
|
+
const worktreePath = join(tempDir, "worktree");
|
|
505
|
+
const config = makeConfig();
|
|
506
|
+
|
|
507
|
+
await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
|
|
508
|
+
|
|
509
|
+
const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
|
|
510
|
+
const file = Bun.file(outputPath);
|
|
511
|
+
const exists = await file.exists();
|
|
512
|
+
expect(exists).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("written file contains the overlay content", async () => {
|
|
516
|
+
const worktreePath = join(tempDir, "worktree");
|
|
517
|
+
const config = makeConfig({ agentName: "file-writer-test" });
|
|
518
|
+
|
|
519
|
+
await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
|
|
520
|
+
|
|
521
|
+
const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
|
|
522
|
+
const content = await Bun.file(outputPath).text();
|
|
523
|
+
expect(content).toContain("file-writer-test");
|
|
524
|
+
expect(content).toContain(config.beadId);
|
|
525
|
+
expect(content).toContain(config.branchName);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("creates .claude directory even if worktree already exists", async () => {
|
|
529
|
+
const worktreePath = join(tempDir, "existing-worktree");
|
|
530
|
+
const { mkdir } = await import("node:fs/promises");
|
|
531
|
+
await mkdir(worktreePath, { recursive: true });
|
|
532
|
+
|
|
533
|
+
const config = makeConfig();
|
|
534
|
+
await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
|
|
535
|
+
|
|
536
|
+
const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
|
|
537
|
+
const exists = await Bun.file(outputPath).exists();
|
|
538
|
+
expect(exists).toBe(true);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("overwrites existing CLAUDE.md if it already exists", async () => {
|
|
542
|
+
const worktreePath = join(tempDir, "worktree");
|
|
543
|
+
const claudeDir = join(worktreePath, ".claude");
|
|
544
|
+
const { mkdir } = await import("node:fs/promises");
|
|
545
|
+
await mkdir(claudeDir, { recursive: true });
|
|
546
|
+
await Bun.write(join(claudeDir, "CLAUDE.md"), "old content");
|
|
547
|
+
|
|
548
|
+
const config = makeConfig({ agentName: "new-agent" });
|
|
549
|
+
await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
|
|
550
|
+
|
|
551
|
+
const content = await Bun.file(join(claudeDir, "CLAUDE.md")).text();
|
|
552
|
+
expect(content).toContain("new-agent");
|
|
553
|
+
expect(content).not.toContain("old content");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("writeOverlay content matches generateOverlay output", async () => {
|
|
557
|
+
const worktreePath = join(tempDir, "worktree");
|
|
558
|
+
const config = makeConfig();
|
|
559
|
+
|
|
560
|
+
const generated = await generateOverlay(config);
|
|
561
|
+
await writeOverlay(worktreePath, config, "/nonexistent-canonical-root");
|
|
562
|
+
|
|
563
|
+
const written = await Bun.file(join(worktreePath, ".claude", "CLAUDE.md")).text();
|
|
564
|
+
expect(written).toBe(generated);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("throws AgentError when worktreePath is the canonical project root", async () => {
|
|
568
|
+
const fakeProjectRoot = join(tempDir, "project-root");
|
|
569
|
+
await mkdir(fakeProjectRoot, { recursive: true });
|
|
570
|
+
|
|
571
|
+
const config = makeConfig({ agentName: "rogue-agent" });
|
|
572
|
+
|
|
573
|
+
expect(async () => {
|
|
574
|
+
await writeOverlay(fakeProjectRoot, config, fakeProjectRoot);
|
|
575
|
+
}).toThrow(AgentError);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("error message mentions canonical project root when guard triggers", async () => {
|
|
579
|
+
const fakeProjectRoot = join(tempDir, "project-root-msg");
|
|
580
|
+
await mkdir(fakeProjectRoot, { recursive: true });
|
|
581
|
+
|
|
582
|
+
const config = makeConfig({ agentName: "rogue-agent" });
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
await writeOverlay(fakeProjectRoot, config, fakeProjectRoot);
|
|
586
|
+
expect.unreachable("should have thrown");
|
|
587
|
+
} catch (err) {
|
|
588
|
+
expect(err).toBeInstanceOf(AgentError);
|
|
589
|
+
const agentErr = err as AgentError;
|
|
590
|
+
expect(agentErr.message).toContain("canonical project root");
|
|
591
|
+
expect(agentErr.message).toContain(fakeProjectRoot);
|
|
592
|
+
expect(agentErr.agentName).toBe("rogue-agent");
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("does NOT throw when worktreePath is a proper worktree subdirectory", async () => {
|
|
597
|
+
const fakeProjectRoot = join(tempDir, "project-with-worktrees");
|
|
598
|
+
await mkdir(join(fakeProjectRoot, ".overstory", "worktrees", "my-agent"), { recursive: true });
|
|
599
|
+
|
|
600
|
+
const worktreePath = join(fakeProjectRoot, ".overstory", "worktrees", "my-agent");
|
|
601
|
+
const config = makeConfig();
|
|
602
|
+
|
|
603
|
+
// This should succeed — the worktree is not the canonical root
|
|
604
|
+
await writeOverlay(worktreePath, config, fakeProjectRoot);
|
|
605
|
+
|
|
606
|
+
const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
|
|
607
|
+
const exists = await Bun.file(outputPath).exists();
|
|
608
|
+
expect(exists).toBe(true);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("does not write CLAUDE.md when guard rejects the path", async () => {
|
|
612
|
+
const fakeProjectRoot = join(tempDir, "project-no-write");
|
|
613
|
+
await mkdir(fakeProjectRoot, { recursive: true });
|
|
614
|
+
|
|
615
|
+
const config = makeConfig();
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
await writeOverlay(fakeProjectRoot, config, fakeProjectRoot);
|
|
619
|
+
} catch {
|
|
620
|
+
// Expected
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Verify CLAUDE.md was NOT written
|
|
624
|
+
const claudeMdPath = join(fakeProjectRoot, ".claude", "CLAUDE.md");
|
|
625
|
+
const exists = await Bun.file(claudeMdPath).exists();
|
|
626
|
+
expect(exists).toBe(false);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("succeeds for worktree with .overstory/config.yaml (dogfooding scenario)", async () => {
|
|
630
|
+
// When dogfooding on overstory's own repo, .overstory/config.yaml is tracked
|
|
631
|
+
// in git. Every worktree checkout includes it. The old file-existence heuristic
|
|
632
|
+
// would incorrectly reject these worktrees. The path-comparison guard must allow
|
|
633
|
+
// writes because the worktree path differs from the canonical root (overstory-p4st).
|
|
634
|
+
const fakeProjectRoot = join(tempDir, "overstory-dogfood");
|
|
635
|
+
const worktreePath = join(fakeProjectRoot, ".overstory", "worktrees", "dogfood-agent");
|
|
636
|
+
await mkdir(join(worktreePath, ".overstory"), { recursive: true });
|
|
637
|
+
// Simulate tracked .overstory/config.yaml appearing in the worktree checkout
|
|
638
|
+
await Bun.write(
|
|
639
|
+
join(worktreePath, ".overstory", "config.yaml"),
|
|
640
|
+
"project:\n name: overstory\n",
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
const config = makeConfig({ agentName: "dogfood-agent" });
|
|
644
|
+
|
|
645
|
+
// Must succeed — worktreePath !== fakeProjectRoot even though config.yaml exists
|
|
646
|
+
await writeOverlay(worktreePath, config, fakeProjectRoot);
|
|
647
|
+
|
|
648
|
+
const outputPath = join(worktreePath, ".claude", "CLAUDE.md");
|
|
649
|
+
const exists = await Bun.file(outputPath).exists();
|
|
650
|
+
expect(exists).toBe(true);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
describe("isCanonicalRoot", () => {
|
|
655
|
+
test("returns true when dir matches canonicalRoot", () => {
|
|
656
|
+
expect(isCanonicalRoot("/projects/my-app", "/projects/my-app")).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("returns true when paths resolve to the same location", () => {
|
|
660
|
+
expect(isCanonicalRoot("/projects/my-app/./", "/projects/my-app")).toBe(true);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("returns false when dir differs from canonicalRoot", () => {
|
|
664
|
+
expect(
|
|
665
|
+
isCanonicalRoot("/projects/my-app/.overstory/worktrees/agent-1", "/projects/my-app"),
|
|
666
|
+
).toBe(false);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("returns false for worktree even when it contains .overstory/config.yaml (dogfooding)", () => {
|
|
670
|
+
// This is the core dogfooding scenario: the worktree has .overstory/config.yaml
|
|
671
|
+
// because it's tracked in git, but the path is different from the canonical root.
|
|
672
|
+
const canonicalRoot = "/projects/overstory";
|
|
673
|
+
const worktreePath = "/projects/overstory/.overstory/worktrees/dogfood-agent";
|
|
674
|
+
expect(isCanonicalRoot(worktreePath, canonicalRoot)).toBe(false);
|
|
675
|
+
});
|
|
676
|
+
});
|