@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,603 @@
|
|
|
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 { AgentIdentity } from "../types.ts";
|
|
7
|
+
import { createIdentity, loadIdentity, updateIdentity } from "./identity.ts";
|
|
8
|
+
|
|
9
|
+
describe("identity", () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tempDir = await mkdtemp(join(tmpdir(), "overstory-identity-test-"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("createIdentity", () => {
|
|
21
|
+
test("creates identity file with minimal data", async () => {
|
|
22
|
+
const identity: AgentIdentity = {
|
|
23
|
+
name: "test-agent",
|
|
24
|
+
capability: "builder",
|
|
25
|
+
created: "2024-01-01T00:00:00Z",
|
|
26
|
+
sessionsCompleted: 0,
|
|
27
|
+
expertiseDomains: [],
|
|
28
|
+
recentTasks: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await createIdentity(tempDir, identity);
|
|
32
|
+
|
|
33
|
+
const filePath = join(tempDir, "test-agent", "identity.yaml");
|
|
34
|
+
const file = Bun.file(filePath);
|
|
35
|
+
expect(await file.exists()).toBe(true);
|
|
36
|
+
|
|
37
|
+
const content = await file.text();
|
|
38
|
+
expect(content).toContain("name: test-agent");
|
|
39
|
+
expect(content).toContain("capability: builder");
|
|
40
|
+
expect(content).toContain('created: "2024-01-01T00:00:00Z"');
|
|
41
|
+
expect(content).toContain("sessionsCompleted: 0");
|
|
42
|
+
expect(content).toContain("expertiseDomains: []");
|
|
43
|
+
expect(content).toContain("recentTasks: []");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("creates identity with expertise domains", async () => {
|
|
47
|
+
const identity: AgentIdentity = {
|
|
48
|
+
name: "test-agent",
|
|
49
|
+
capability: "builder",
|
|
50
|
+
created: "2024-01-01T00:00:00Z",
|
|
51
|
+
sessionsCompleted: 5,
|
|
52
|
+
expertiseDomains: ["typescript", "testing", "architecture"],
|
|
53
|
+
recentTasks: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await createIdentity(tempDir, identity);
|
|
57
|
+
|
|
58
|
+
const filePath = join(tempDir, "test-agent", "identity.yaml");
|
|
59
|
+
const content = await Bun.file(filePath).text();
|
|
60
|
+
expect(content).toContain("expertiseDomains:");
|
|
61
|
+
expect(content).toContain("\t- typescript");
|
|
62
|
+
expect(content).toContain("\t- testing");
|
|
63
|
+
expect(content).toContain("\t- architecture");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("creates identity with recent tasks", async () => {
|
|
67
|
+
const identity: AgentIdentity = {
|
|
68
|
+
name: "test-agent",
|
|
69
|
+
capability: "builder",
|
|
70
|
+
created: "2024-01-01T00:00:00Z",
|
|
71
|
+
sessionsCompleted: 3,
|
|
72
|
+
expertiseDomains: [],
|
|
73
|
+
recentTasks: [
|
|
74
|
+
{
|
|
75
|
+
beadId: "beads-001",
|
|
76
|
+
summary: "Fixed authentication bug",
|
|
77
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
beadId: "beads-002",
|
|
81
|
+
summary: "Added user profile page",
|
|
82
|
+
completedAt: "2024-01-16T14:30:00Z",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await createIdentity(tempDir, identity);
|
|
88
|
+
|
|
89
|
+
const filePath = join(tempDir, "test-agent", "identity.yaml");
|
|
90
|
+
const content = await Bun.file(filePath).text();
|
|
91
|
+
expect(content).toContain("recentTasks:");
|
|
92
|
+
expect(content).toContain("\t- beadId: beads-001");
|
|
93
|
+
expect(content).toContain("\t\tsummary: Fixed authentication bug");
|
|
94
|
+
expect(content).toContain('\t\tcompletedAt: "2024-01-15T12:00:00Z"');
|
|
95
|
+
expect(content).toContain("\t- beadId: beads-002");
|
|
96
|
+
expect(content).toContain("\t\tsummary: Added user profile page");
|
|
97
|
+
expect(content).toContain('\t\tcompletedAt: "2024-01-16T14:30:00Z"');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("quotes strings with special characters", async () => {
|
|
101
|
+
const identity: AgentIdentity = {
|
|
102
|
+
name: "test-agent",
|
|
103
|
+
capability: "builder",
|
|
104
|
+
created: "2024-01-01T00:00:00Z",
|
|
105
|
+
sessionsCompleted: 0,
|
|
106
|
+
expertiseDomains: ["domain: with colon", "domain#with hash", " leading space"],
|
|
107
|
+
recentTasks: [
|
|
108
|
+
{
|
|
109
|
+
beadId: "beads-001",
|
|
110
|
+
summary: 'Fixed bug: "memory leak"',
|
|
111
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await createIdentity(tempDir, identity);
|
|
117
|
+
|
|
118
|
+
const filePath = join(tempDir, "test-agent", "identity.yaml");
|
|
119
|
+
const content = await Bun.file(filePath).text();
|
|
120
|
+
expect(content).toContain('"domain: with colon"');
|
|
121
|
+
expect(content).toContain('"domain#with hash"');
|
|
122
|
+
expect(content).toContain('" leading space"');
|
|
123
|
+
expect(content).toContain('Fixed bug: \\"memory leak\\"');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("creates directory if it does not exist", async () => {
|
|
127
|
+
const identity: AgentIdentity = {
|
|
128
|
+
name: "new-agent",
|
|
129
|
+
capability: "scout",
|
|
130
|
+
created: "2024-01-01T00:00:00Z",
|
|
131
|
+
sessionsCompleted: 0,
|
|
132
|
+
expertiseDomains: [],
|
|
133
|
+
recentTasks: [],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const agentDir = join(tempDir, "new-agent");
|
|
137
|
+
expect(await Bun.file(agentDir).exists()).toBe(false);
|
|
138
|
+
|
|
139
|
+
await createIdentity(tempDir, identity);
|
|
140
|
+
|
|
141
|
+
const filePath = join(agentDir, "identity.yaml");
|
|
142
|
+
expect(await Bun.file(filePath).exists()).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("overwrites existing identity file", async () => {
|
|
146
|
+
const identity1: AgentIdentity = {
|
|
147
|
+
name: "test-agent",
|
|
148
|
+
capability: "builder",
|
|
149
|
+
created: "2024-01-01T00:00:00Z",
|
|
150
|
+
sessionsCompleted: 5,
|
|
151
|
+
expertiseDomains: ["old-domain"],
|
|
152
|
+
recentTasks: [],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await createIdentity(tempDir, identity1);
|
|
156
|
+
|
|
157
|
+
const identity2: AgentIdentity = {
|
|
158
|
+
...identity1,
|
|
159
|
+
sessionsCompleted: 10,
|
|
160
|
+
expertiseDomains: ["new-domain"],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
await createIdentity(tempDir, identity2);
|
|
164
|
+
|
|
165
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
166
|
+
expect(loaded?.sessionsCompleted).toBe(10);
|
|
167
|
+
expect(loaded?.expertiseDomains).toEqual(["new-domain"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("throws AgentError for invalid directory", async () => {
|
|
171
|
+
const identity: AgentIdentity = {
|
|
172
|
+
name: "test-agent",
|
|
173
|
+
capability: "builder",
|
|
174
|
+
created: "2024-01-01T00:00:00Z",
|
|
175
|
+
sessionsCompleted: 0,
|
|
176
|
+
expertiseDomains: [],
|
|
177
|
+
recentTasks: [],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Create a file where the directory should be
|
|
181
|
+
const blockedPath = join(tempDir, "test-agent");
|
|
182
|
+
await Bun.write(blockedPath, "blocking file");
|
|
183
|
+
|
|
184
|
+
await expect(createIdentity(tempDir, identity)).rejects.toThrow(AgentError);
|
|
185
|
+
await expect(createIdentity(tempDir, identity)).rejects.toThrow(
|
|
186
|
+
"Failed to create identity directory",
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("loadIdentity", () => {
|
|
192
|
+
test("loads existing identity correctly", async () => {
|
|
193
|
+
const identity: AgentIdentity = {
|
|
194
|
+
name: "test-agent",
|
|
195
|
+
capability: "builder",
|
|
196
|
+
created: "2024-01-01T00:00:00Z",
|
|
197
|
+
sessionsCompleted: 7,
|
|
198
|
+
expertiseDomains: ["typescript", "testing"],
|
|
199
|
+
recentTasks: [
|
|
200
|
+
{
|
|
201
|
+
beadId: "beads-001",
|
|
202
|
+
summary: "Fixed bug",
|
|
203
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
await createIdentity(tempDir, identity);
|
|
209
|
+
|
|
210
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
211
|
+
|
|
212
|
+
expect(loaded).toBeDefined();
|
|
213
|
+
expect(loaded?.name).toBe("test-agent");
|
|
214
|
+
expect(loaded?.capability).toBe("builder");
|
|
215
|
+
expect(loaded?.created).toBe("2024-01-01T00:00:00Z");
|
|
216
|
+
expect(loaded?.sessionsCompleted).toBe(7);
|
|
217
|
+
expect(loaded?.expertiseDomains).toEqual(["typescript", "testing"]);
|
|
218
|
+
expect(loaded?.recentTasks).toHaveLength(1);
|
|
219
|
+
expect(loaded?.recentTasks[0]?.beadId).toBe("beads-001");
|
|
220
|
+
expect(loaded?.recentTasks[0]?.summary).toBe("Fixed bug");
|
|
221
|
+
expect(loaded?.recentTasks[0]?.completedAt).toBe("2024-01-15T12:00:00Z");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("returns null for non-existent identity", async () => {
|
|
225
|
+
const loaded = await loadIdentity(tempDir, "nonexistent");
|
|
226
|
+
expect(loaded).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("loads identity with empty arrays", async () => {
|
|
230
|
+
const identity: AgentIdentity = {
|
|
231
|
+
name: "test-agent",
|
|
232
|
+
capability: "builder",
|
|
233
|
+
created: "2024-01-01T00:00:00Z",
|
|
234
|
+
sessionsCompleted: 0,
|
|
235
|
+
expertiseDomains: [],
|
|
236
|
+
recentTasks: [],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
await createIdentity(tempDir, identity);
|
|
240
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
241
|
+
|
|
242
|
+
expect(loaded?.expertiseDomains).toEqual([]);
|
|
243
|
+
expect(loaded?.recentTasks).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("loads identity with multiple recent tasks", async () => {
|
|
247
|
+
const identity: AgentIdentity = {
|
|
248
|
+
name: "test-agent",
|
|
249
|
+
capability: "builder",
|
|
250
|
+
created: "2024-01-01T00:00:00Z",
|
|
251
|
+
sessionsCompleted: 3,
|
|
252
|
+
expertiseDomains: [],
|
|
253
|
+
recentTasks: [
|
|
254
|
+
{
|
|
255
|
+
beadId: "beads-001",
|
|
256
|
+
summary: "Task 1",
|
|
257
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
beadId: "beads-002",
|
|
261
|
+
summary: "Task 2",
|
|
262
|
+
completedAt: "2024-01-16T12:00:00Z",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
beadId: "beads-003",
|
|
266
|
+
summary: "Task 3",
|
|
267
|
+
completedAt: "2024-01-17T12:00:00Z",
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
await createIdentity(tempDir, identity);
|
|
273
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
274
|
+
|
|
275
|
+
expect(loaded?.recentTasks).toHaveLength(3);
|
|
276
|
+
expect(loaded?.recentTasks[0]?.beadId).toBe("beads-001");
|
|
277
|
+
expect(loaded?.recentTasks[1]?.beadId).toBe("beads-002");
|
|
278
|
+
expect(loaded?.recentTasks[2]?.beadId).toBe("beads-003");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("handles quoted strings with special characters", async () => {
|
|
282
|
+
const identity: AgentIdentity = {
|
|
283
|
+
name: "test-agent",
|
|
284
|
+
capability: "builder",
|
|
285
|
+
created: "2024-01-01T00:00:00Z",
|
|
286
|
+
sessionsCompleted: 0,
|
|
287
|
+
expertiseDomains: ["domain: with colon", "domain#with hash"],
|
|
288
|
+
recentTasks: [
|
|
289
|
+
{
|
|
290
|
+
beadId: "beads-001",
|
|
291
|
+
summary: 'Fixed bug: "memory leak"',
|
|
292
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
await createIdentity(tempDir, identity);
|
|
298
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
299
|
+
|
|
300
|
+
expect(loaded?.expertiseDomains).toContain("domain: with colon");
|
|
301
|
+
expect(loaded?.expertiseDomains).toContain("domain#with hash");
|
|
302
|
+
expect(loaded?.recentTasks[0]?.summary).toBe('Fixed bug: "memory leak"');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("handles escaped characters in strings", async () => {
|
|
306
|
+
const identity: AgentIdentity = {
|
|
307
|
+
name: "test-agent",
|
|
308
|
+
capability: "builder",
|
|
309
|
+
created: "2024-01-01T00:00:00Z",
|
|
310
|
+
sessionsCompleted: 0,
|
|
311
|
+
expertiseDomains: [],
|
|
312
|
+
recentTasks: [
|
|
313
|
+
{
|
|
314
|
+
beadId: "beads-001",
|
|
315
|
+
summary: "Path: C:\\Users\\test\\file.txt",
|
|
316
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
await createIdentity(tempDir, identity);
|
|
322
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
323
|
+
|
|
324
|
+
expect(loaded?.recentTasks[0]?.summary).toBe("Path: C:\\Users\\test\\file.txt");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("handles malformed YAML gracefully", async () => {
|
|
328
|
+
const agentDir = join(tempDir, "test-agent");
|
|
329
|
+
await mkdir(agentDir, { recursive: true });
|
|
330
|
+
const filePath = join(agentDir, "identity.yaml");
|
|
331
|
+
|
|
332
|
+
// Write invalid YAML (unbalanced quotes)
|
|
333
|
+
await Bun.write(
|
|
334
|
+
filePath,
|
|
335
|
+
'name: "test-agent\ncapability: builder\ncreated: 2024-01-01T00:00:00Z\n',
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
339
|
+
|
|
340
|
+
// Parser handles unbalanced quotes by treating the rest as quoted content
|
|
341
|
+
expect(loaded?.name).toBe('"test-agent');
|
|
342
|
+
expect(loaded?.capability).toBe("builder");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("handles YAML with comments", async () => {
|
|
346
|
+
const agentDir = join(tempDir, "test-agent");
|
|
347
|
+
await mkdir(agentDir, { recursive: true });
|
|
348
|
+
const filePath = join(agentDir, "identity.yaml");
|
|
349
|
+
|
|
350
|
+
await Bun.write(
|
|
351
|
+
filePath,
|
|
352
|
+
`# Agent identity file
|
|
353
|
+
name: test-agent
|
|
354
|
+
capability: builder
|
|
355
|
+
created: 2024-01-01T00:00:00Z
|
|
356
|
+
sessionsCompleted: 5
|
|
357
|
+
# Expertise domains
|
|
358
|
+
expertiseDomains:
|
|
359
|
+
- typescript
|
|
360
|
+
- testing
|
|
361
|
+
recentTasks: []
|
|
362
|
+
`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
366
|
+
|
|
367
|
+
expect(loaded?.name).toBe("test-agent");
|
|
368
|
+
expect(loaded?.capability).toBe("builder");
|
|
369
|
+
expect(loaded?.sessionsCompleted).toBe(5);
|
|
370
|
+
expect(loaded?.expertiseDomains).toEqual(["typescript", "testing"]);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("updateIdentity", () => {
|
|
375
|
+
test("increments sessionsCompleted", async () => {
|
|
376
|
+
const identity: AgentIdentity = {
|
|
377
|
+
name: "test-agent",
|
|
378
|
+
capability: "builder",
|
|
379
|
+
created: "2024-01-01T00:00:00Z",
|
|
380
|
+
sessionsCompleted: 5,
|
|
381
|
+
expertiseDomains: [],
|
|
382
|
+
recentTasks: [],
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
await createIdentity(tempDir, identity);
|
|
386
|
+
const updated = await updateIdentity(tempDir, "test-agent", {
|
|
387
|
+
sessionsCompleted: 3,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(updated.sessionsCompleted).toBe(8);
|
|
391
|
+
|
|
392
|
+
// Verify persistence
|
|
393
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
394
|
+
expect(loaded?.sessionsCompleted).toBe(8);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("merges expertise domains with deduplication", async () => {
|
|
398
|
+
const identity: AgentIdentity = {
|
|
399
|
+
name: "test-agent",
|
|
400
|
+
capability: "builder",
|
|
401
|
+
created: "2024-01-01T00:00:00Z",
|
|
402
|
+
sessionsCompleted: 0,
|
|
403
|
+
expertiseDomains: ["typescript", "testing"],
|
|
404
|
+
recentTasks: [],
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
await createIdentity(tempDir, identity);
|
|
408
|
+
const updated = await updateIdentity(tempDir, "test-agent", {
|
|
409
|
+
expertiseDomains: ["testing", "architecture", "git"],
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(updated.expertiseDomains).toHaveLength(4);
|
|
413
|
+
expect(updated.expertiseDomains).toContain("typescript");
|
|
414
|
+
expect(updated.expertiseDomains).toContain("testing");
|
|
415
|
+
expect(updated.expertiseDomains).toContain("architecture");
|
|
416
|
+
expect(updated.expertiseDomains).toContain("git");
|
|
417
|
+
|
|
418
|
+
// Count "testing" only once
|
|
419
|
+
const testingCount = updated.expertiseDomains.filter((d) => d === "testing").length;
|
|
420
|
+
expect(testingCount).toBe(1);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("appends completed task with current timestamp", async () => {
|
|
424
|
+
const identity: AgentIdentity = {
|
|
425
|
+
name: "test-agent",
|
|
426
|
+
capability: "builder",
|
|
427
|
+
created: "2024-01-01T00:00:00Z",
|
|
428
|
+
sessionsCompleted: 0,
|
|
429
|
+
expertiseDomains: [],
|
|
430
|
+
recentTasks: [],
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
await createIdentity(tempDir, identity);
|
|
434
|
+
|
|
435
|
+
const beforeUpdate = Date.now();
|
|
436
|
+
const updated = await updateIdentity(tempDir, "test-agent", {
|
|
437
|
+
completedTask: {
|
|
438
|
+
beadId: "beads-001",
|
|
439
|
+
summary: "Fixed authentication bug",
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
const afterUpdate = Date.now();
|
|
443
|
+
|
|
444
|
+
expect(updated.recentTasks).toHaveLength(1);
|
|
445
|
+
expect(updated.recentTasks[0]?.beadId).toBe("beads-001");
|
|
446
|
+
expect(updated.recentTasks[0]?.summary).toBe("Fixed authentication bug");
|
|
447
|
+
|
|
448
|
+
// Verify timestamp is within the update window
|
|
449
|
+
const timestamp = new Date(updated.recentTasks[0]?.completedAt ?? "").getTime();
|
|
450
|
+
expect(timestamp).toBeGreaterThanOrEqual(beforeUpdate);
|
|
451
|
+
expect(timestamp).toBeLessThanOrEqual(afterUpdate);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("caps recentTasks at 20 entries, dropping oldest", async () => {
|
|
455
|
+
// Create identity with 19 tasks
|
|
456
|
+
const existingTasks = Array.from({ length: 19 }, (_, i) => ({
|
|
457
|
+
beadId: `beads-${i.toString().padStart(3, "0")}`,
|
|
458
|
+
summary: `Task ${i}`,
|
|
459
|
+
completedAt: `2024-01-${(i + 1).toString().padStart(2, "0")}T12:00:00Z`,
|
|
460
|
+
}));
|
|
461
|
+
|
|
462
|
+
const identity: AgentIdentity = {
|
|
463
|
+
name: "test-agent",
|
|
464
|
+
capability: "builder",
|
|
465
|
+
created: "2024-01-01T00:00:00Z",
|
|
466
|
+
sessionsCompleted: 0,
|
|
467
|
+
expertiseDomains: [],
|
|
468
|
+
recentTasks: existingTasks,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
await createIdentity(tempDir, identity);
|
|
472
|
+
|
|
473
|
+
// Add two more tasks (total would be 21)
|
|
474
|
+
let updated = await updateIdentity(tempDir, "test-agent", {
|
|
475
|
+
completedTask: { beadId: "beads-019", summary: "Task 19" },
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(updated.recentTasks).toHaveLength(20);
|
|
479
|
+
expect(updated.recentTasks[0]?.beadId).toBe("beads-000");
|
|
480
|
+
|
|
481
|
+
updated = await updateIdentity(tempDir, "test-agent", {
|
|
482
|
+
completedTask: { beadId: "beads-020", summary: "Task 20" },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(updated.recentTasks).toHaveLength(20);
|
|
486
|
+
// Oldest task (beads-000) should be dropped
|
|
487
|
+
expect(updated.recentTasks[0]?.beadId).toBe("beads-001");
|
|
488
|
+
expect(updated.recentTasks[19]?.beadId).toBe("beads-020");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("applies multiple updates simultaneously", async () => {
|
|
492
|
+
const identity: AgentIdentity = {
|
|
493
|
+
name: "test-agent",
|
|
494
|
+
capability: "builder",
|
|
495
|
+
created: "2024-01-01T00:00:00Z",
|
|
496
|
+
sessionsCompleted: 5,
|
|
497
|
+
expertiseDomains: ["typescript"],
|
|
498
|
+
recentTasks: [],
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
await createIdentity(tempDir, identity);
|
|
502
|
+
const updated = await updateIdentity(tempDir, "test-agent", {
|
|
503
|
+
sessionsCompleted: 2,
|
|
504
|
+
expertiseDomains: ["testing", "architecture"],
|
|
505
|
+
completedTask: {
|
|
506
|
+
beadId: "beads-001",
|
|
507
|
+
summary: "Completed task",
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
expect(updated.sessionsCompleted).toBe(7);
|
|
512
|
+
expect(updated.expertiseDomains).toHaveLength(3);
|
|
513
|
+
expect(updated.expertiseDomains).toContain("typescript");
|
|
514
|
+
expect(updated.expertiseDomains).toContain("testing");
|
|
515
|
+
expect(updated.expertiseDomains).toContain("architecture");
|
|
516
|
+
expect(updated.recentTasks).toHaveLength(1);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("throws AgentError for non-existent identity", async () => {
|
|
520
|
+
await expect(
|
|
521
|
+
updateIdentity(tempDir, "nonexistent", { sessionsCompleted: 1 }),
|
|
522
|
+
).rejects.toThrow(AgentError);
|
|
523
|
+
await expect(
|
|
524
|
+
updateIdentity(tempDir, "nonexistent", { sessionsCompleted: 1 }),
|
|
525
|
+
).rejects.toThrow("not found");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test("handles empty update object", async () => {
|
|
529
|
+
const identity: AgentIdentity = {
|
|
530
|
+
name: "test-agent",
|
|
531
|
+
capability: "builder",
|
|
532
|
+
created: "2024-01-01T00:00:00Z",
|
|
533
|
+
sessionsCompleted: 5,
|
|
534
|
+
expertiseDomains: ["typescript"],
|
|
535
|
+
recentTasks: [],
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
await createIdentity(tempDir, identity);
|
|
539
|
+
const updated = await updateIdentity(tempDir, "test-agent", {});
|
|
540
|
+
|
|
541
|
+
expect(updated.sessionsCompleted).toBe(5);
|
|
542
|
+
expect(updated.expertiseDomains).toEqual(["typescript"]);
|
|
543
|
+
expect(updated.recentTasks).toEqual([]);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("round-trip serialization", () => {
|
|
548
|
+
test("preserves data through create and load cycle", async () => {
|
|
549
|
+
const original: AgentIdentity = {
|
|
550
|
+
name: "test-agent",
|
|
551
|
+
capability: "builder",
|
|
552
|
+
created: "2024-01-01T00:00:00Z",
|
|
553
|
+
sessionsCompleted: 42,
|
|
554
|
+
expertiseDomains: ["typescript", "testing", "architecture"],
|
|
555
|
+
recentTasks: [
|
|
556
|
+
{
|
|
557
|
+
beadId: "beads-001",
|
|
558
|
+
summary: "Implemented feature X",
|
|
559
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
beadId: "beads-002",
|
|
563
|
+
summary: "Fixed bug in module Y",
|
|
564
|
+
completedAt: "2024-01-16T14:30:00Z",
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
await createIdentity(tempDir, original);
|
|
570
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
571
|
+
|
|
572
|
+
expect(loaded).toEqual(original);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("preserves special characters through round-trip", async () => {
|
|
576
|
+
const original: AgentIdentity = {
|
|
577
|
+
name: "test-agent",
|
|
578
|
+
capability: "builder",
|
|
579
|
+
created: "2024-01-01T00:00:00Z",
|
|
580
|
+
sessionsCompleted: 0,
|
|
581
|
+
expertiseDomains: [
|
|
582
|
+
"domain: with colon",
|
|
583
|
+
"domain#with hash",
|
|
584
|
+
"domain with spaces",
|
|
585
|
+
"true",
|
|
586
|
+
"123",
|
|
587
|
+
],
|
|
588
|
+
recentTasks: [
|
|
589
|
+
{
|
|
590
|
+
beadId: "beads-001",
|
|
591
|
+
summary: 'Summary with "quotes" and: colons',
|
|
592
|
+
completedAt: "2024-01-15T12:00:00Z",
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
await createIdentity(tempDir, original);
|
|
598
|
+
const loaded = await loadIdentity(tempDir, "test-agent");
|
|
599
|
+
|
|
600
|
+
expect(loaded).toEqual(original);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
});
|