@cueloop/daemon 0.1.0-alpha.6 → 0.1.0-alpha.61

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/package.json CHANGED
@@ -1,34 +1,42 @@
1
1
  {
2
2
  "name": "@cueloop/daemon",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.61",
4
+ "description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits",
5
+ "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/mmurakaru/cueloop/issues"
8
+ },
4
9
  "license": "Apache-2.0",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/mmurakaru/cueloop.git",
13
+ "directory": "packages/daemon"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "README.md"
18
+ ],
5
19
  "type": "module",
6
20
  "exports": {
7
21
  ".": "./src/index.ts",
8
- "./client": "./src/client.ts"
9
- },
10
- "dependencies": {
11
- "@cueloop/schema": "0.1.0-alpha.6",
12
- "valibot": "^1.4.2"
22
+ "./client": "./src/client.ts",
23
+ "./review": "./src/review.ts",
24
+ "./working-tree": "./src/working-tree.ts",
25
+ "./store": "./src/store.ts",
26
+ "./paths": "./src/paths.ts",
27
+ "./retention": "./src/retention.ts",
28
+ "./herdr-pane": "./src/herdr-pane.ts",
29
+ "./herdr-split": "./src/herdr-split.ts",
30
+ "./share-blob": "./src/share-blob.ts"
13
31
  },
14
32
  "publishConfig": {
15
33
  "access": "public"
16
34
  },
17
- "files": [
18
- "src",
19
- "README.md"
20
- ],
35
+ "dependencies": {
36
+ "@cueloop/schema": "0.1.0-alpha.61",
37
+ "valibot": "^1.4.2"
38
+ },
21
39
  "engines": {
22
40
  "bun": ">=1.3.0"
23
- },
24
- "repository": {
25
- "type": "git",
26
- "url": "git+https://github.com/mmurakaru/cueloop.git",
27
- "directory": "packages/daemon"
28
- },
29
- "homepage": "https://github.com/mmurakaru/cueloop#readme",
30
- "bugs": {
31
- "url": "https://github.com/mmurakaru/cueloop/issues"
32
- },
33
- "description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits"
34
- }
41
+ }
42
+ }
package/src/api.test.ts CHANGED
@@ -24,71 +24,174 @@ afterEach(() => rmSync(home, { recursive: true, force: true }));
24
24
 
25
25
  describe("session lifecycle", () => {
26
26
  test("create → get → list", () => {
27
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
28
- expect(s.status).toBe("pending");
29
- expect(s.revisions.length).toBe(1);
30
- expect(core.sessionGet(s.id).id).toBe(s.id);
27
+ // Act
28
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
29
+
30
+ // Assert
31
+ expect(session.status).toBe("pending");
32
+ expect(session.revisions.length).toBe(1);
33
+ expect(core.sessionGet(session.id).id).toBe(session.id);
31
34
  expect(core.sessionList({ status: "pending" }).length).toBe(1);
32
35
  });
33
36
 
34
37
  test("annotate upserts by id; remove deletes", () => {
35
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
36
- core.sessionAnnotate(s.id, {
38
+ // Arrange
39
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
40
+
41
+ // Act
42
+ core.sessionAnnotate(session.id, {
37
43
  id: "a1",
38
44
  kind: "comment",
39
45
  anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
40
46
  body: "How carefully?",
41
47
  });
42
- core.sessionAnnotate(s.id, {
48
+ core.sessionAnnotate(session.id, {
43
49
  id: "a1",
44
50
  kind: "comment",
45
51
  anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
46
52
  body: "Define carefully.",
47
53
  });
48
- expect(core.sessionGet(s.id).annotations.length).toBe(1);
49
- expect(core.sessionGet(s.id).annotations[0]!.body).toBe("Define carefully.");
50
- core.sessionRemoveAnnotation(s.id, "a1");
51
- expect(core.sessionGet(s.id).annotations.length).toBe(0);
54
+
55
+ // Assert
56
+ expect(core.sessionGet(session.id).annotations.length).toBe(1);
57
+ expect(core.sessionGet(session.id).annotations[0]!.body).toBe("Define carefully.");
58
+
59
+ // Act
60
+ core.sessionRemoveAnnotation(session.id, "a1");
61
+
62
+ // Assert
63
+ expect(core.sessionGet(session.id).annotations.length).toBe(0);
64
+ });
65
+
66
+ test("annotate with an author registers them in the participant registry", () => {
67
+ // Arrange
68
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
69
+
70
+ // Act
71
+ core.sessionAnnotate(
72
+ session.id,
73
+ {
74
+ id: "a1",
75
+ kind: "comment",
76
+ anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
77
+ body: "A collaborator's note.",
78
+ author: "SHA256:ana",
79
+ },
80
+ "Ana",
81
+ );
82
+
83
+ // Assert
84
+ const stored = core.sessionGet(session.id);
85
+
86
+ expect(stored.annotations[0]!.author).toBe("SHA256:ana");
87
+ expect(stored.participants).toEqual([{ id: "SHA256:ana", provider: "ssh", name: "Ana" }]);
52
88
  });
53
89
 
54
90
  test("resolve produces feedback.md and maps to the agent contract", () => {
55
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
56
- core.sessionAnnotate(s.id, {
91
+ // Arrange
92
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
93
+
94
+ core.sessionAnnotate(session.id, {
57
95
  id: "a1",
58
96
  kind: "comment",
59
97
  anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
60
98
  body: "Spell out the steps.",
61
99
  });
62
- const resolved = core.sessionResolve(s.id, "request_changes", "Needs detail.");
100
+
101
+ // Act
102
+ const resolved = core.sessionResolve(session.id, "request_changes", "Needs detail.");
103
+
104
+ // Assert
63
105
  expect(resolved.verdict!.feedback).toContain("# Review: request changes");
64
106
  expect(resolved.verdict!.feedback).toContain("> carefully");
65
- expect(verdictResponse(resolved)).toEqual({ allow: false, feedback: resolved.verdict!.feedback });
107
+ expect(verdictResponse(resolved)).toEqual({
108
+ allow: false,
109
+ feedback: resolved.verdict!.feedback,
110
+ });
111
+
112
+ // Arrange
66
113
  const approved = core.sessionCreate({ workspace: WS, artifact: PLAN });
114
+
115
+ // Act
67
116
  core.sessionResolve(approved.id, "approve", "LGTM");
117
+
118
+ // Assert
68
119
  expect(verdictResponse(core.sessionGet(approved.id)).allow).toBe(true);
69
120
  });
70
121
 
71
122
  test("mutating a resolved session throws", () => {
72
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
73
- core.sessionResolve(s.id, "approve", "");
123
+ // Arrange
124
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
125
+
126
+ core.sessionResolve(session.id, "approve", "");
127
+
128
+ // Assert
74
129
  expect(() =>
75
- core.sessionAnnotate(s.id, { id: "a1", kind: "comment", anchor: { quote: "x", prefix: "", suffix: "" }, body: "b" }),
130
+ core.sessionAnnotate(session.id, {
131
+ id: "a1",
132
+ kind: "comment",
133
+ anchor: { quote: "x", prefix: "", suffix: "" },
134
+ body: "b",
135
+ }),
76
136
  ).toThrow("already resolved");
77
137
  });
78
138
 
79
139
  test("working copy stores edits and clears on revert or no-op", () => {
80
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
81
- core.sessionSetWorkingCopy(s.id, PLAN.content.replace("carefully", "very carefully"));
82
- expect(core.sessionGet(s.id).workingCopy).toContain("very carefully");
83
- core.sessionSetWorkingCopy(s.id, PLAN.content);
84
- expect(core.sessionGet(s.id).workingCopy).toBeUndefined();
140
+ // Arrange
141
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
142
+
143
+ // Act
144
+ core.sessionSetWorkingCopy(session.id, PLAN.content.replace("carefully", "very carefully"));
145
+
146
+ // Assert
147
+ expect(core.sessionGet(session.id).workingCopy).toContain("very carefully");
148
+
149
+ // Act
150
+ core.sessionSetWorkingCopy(session.id, PLAN.content);
151
+
152
+ // Assert
153
+ expect(core.sessionGet(session.id).workingCopy).toBeUndefined();
154
+ });
155
+
156
+ test("viewed paths merge, dedupe, clear on empty, and survive a daemon restart", () => {
157
+ // Arrange
158
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
159
+
160
+ // Act
161
+ core.sessionSetViewed(session.id, ["src/a.ts", "src/b.ts", "src/a.ts"]);
162
+
163
+ // Assert
164
+ expect(core.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts"]);
165
+
166
+ // Act
167
+ // merge-additive: a stale client sending only its own new mark loses nothing
168
+ core.sessionSetViewed(session.id, ["src/c.ts"]);
169
+
170
+ // Assert
171
+ expect(core.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
172
+ // a resumed review reads its progress back after a daemon restart
173
+ const reborn = new DaemonCore(home);
174
+
175
+ expect(reborn.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
176
+
177
+ // Act
178
+ core.sessionSetViewed(session.id, []);
179
+
180
+ // Assert
181
+ expect(core.sessionGet(session.id).viewedPaths).toBeUndefined();
85
182
  });
86
183
 
87
184
  test("revision reopens the session and resets working copy + verdict", () => {
88
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
89
- core.sessionSetWorkingCopy(s.id, PLAN.content + "\nedit");
90
- core.sessionResolve(s.id, "request_changes", "redo");
91
- const revised = core.sessionSubmitRevision(s.id, "# Plan v2\n");
185
+ // Arrange
186
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
187
+
188
+ core.sessionSetWorkingCopy(session.id, PLAN.content + "\nedit");
189
+ core.sessionResolve(session.id, "request_changes", "redo");
190
+
191
+ // Act
192
+ const revised = core.sessionSubmitRevision(session.id, "# Plan v2\n");
193
+
194
+ // Assert
92
195
  expect(revised.status).toBe("pending");
93
196
  expect(revised.verdict).toBeNull();
94
197
  expect(revised.workingCopy).toBeUndefined();
@@ -97,47 +200,172 @@ describe("session lifecycle", () => {
97
200
  });
98
201
  });
99
202
 
203
+ describe("revision marks addressed annotations", () => {
204
+ const annotate = (sessionId: string, id: string, quote: string, body = "note") =>
205
+ core.sessionAnnotate(sessionId, {
206
+ id,
207
+ kind: "comment",
208
+ anchor: { quote, prefix: "", suffix: "" },
209
+ body,
210
+ });
211
+
212
+ test("ids the agent reports are marked addressed by that revision", () => {
213
+ // Arrange
214
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
215
+
216
+ annotate(session.id, "a1", "carefully");
217
+ annotate(session.id, "a2", "Context");
218
+
219
+ // Act: the revision keeps both quoted texts, so only the reported id resolves
220
+ const revised = core.sessionSubmitRevision(session.id, PLAN.content + "\nMore.\n", ["a1"]);
221
+
222
+ // Assert
223
+ const [first, second] = revised.annotations;
224
+
225
+ expect(first!.resolution).toEqual({ revision: 2, source: "agent" });
226
+ expect(second!.resolution).toBeUndefined();
227
+ });
228
+
229
+ test("a plan annotation whose quoted text vanished is drift-addressed; a surviving quote stays open", () => {
230
+ // Arrange
231
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
232
+
233
+ annotate(session.id, "gone", "carefully");
234
+ annotate(session.id, "kept", "Context");
235
+
236
+ // Act: the revision rewrites the "carefully" sentence but keeps the Context heading
237
+ const revised = core.sessionSubmitRevision(
238
+ session.id,
239
+ "# Plan\n\n## Context\n\nDo the thing with tests.\n",
240
+ );
241
+
242
+ // Assert
243
+ expect(revised.annotations.find((a) => a.id === "gone")!.resolution).toEqual({
244
+ revision: 2,
245
+ source: "drift",
246
+ });
247
+ expect(revised.annotations.find((a) => a.id === "kept")!.resolution).toBeUndefined();
248
+ });
249
+
250
+ test("an unknown reported id is ignored, and an already-addressed annotation keeps its first resolution", () => {
251
+ // Arrange
252
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
253
+
254
+ annotate(session.id, "a1", "carefully");
255
+ core.sessionSubmitRevision(session.id, PLAN.content, ["a1"]);
256
+
257
+ // Act: a second revision reports a stale id and re-reports the settled one
258
+ const revised = core.sessionSubmitRevision(session.id, PLAN.content, ["a1", "a_never_existed"]);
259
+
260
+ // Assert
261
+ expect(revised.annotations[0]!.resolution).toEqual({ revision: 2, source: "agent" });
262
+ expect(revised.revisions.length).toBe(3);
263
+ });
264
+
265
+ test("a diff revision never drift-addresses - a new patch says nothing about the feedback", () => {
266
+ // Arrange
267
+ const diffArtifact: Artifact = { type: "diff", content: "+++ b/a.ts\n+new line\n", meta: {} };
268
+ const session = core.sessionCreate({ workspace: WS, artifact: diffArtifact });
269
+
270
+ annotate(session.id, "d1", "new line");
271
+
272
+ // Act: the revised patch no longer contains the quoted text
273
+ const revised = core.sessionSubmitRevision(session.id, "+++ b/b.ts\n+other\n");
274
+
275
+ // Assert
276
+ expect(revised.annotations[0]!.resolution).toBeUndefined();
277
+ });
278
+
279
+ test("the next feedback document omits addressed annotations and teaches the addressed-ids call", () => {
280
+ // Arrange
281
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
282
+
283
+ annotate(session.id, "settled", "carefully");
284
+ annotate(session.id, "open", "Context", "needs a diagram");
285
+ core.sessionSubmitRevision(session.id, PLAN.content, ["settled"]);
286
+
287
+ // Act
288
+ const resolved = core.sessionResolve(session.id, "request_changes", "one left");
289
+
290
+ // Assert
291
+ expect(resolved.verdict!.feedback).not.toContain("settled");
292
+ expect(resolved.verdict!.feedback).toContain("annotation id: `open`");
293
+ expect(resolved.verdict!.feedback).toContain(`submit-revision ${session.id}`);
294
+ expect(resolved.verdict!.feedback).toContain("--addressed");
295
+ });
296
+ });
297
+
100
298
  describe("the wait contract: verdicts outlive waits", () => {
101
299
  test("wait resolves when the verdict arrives", async () => {
102
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
103
- const wait = core.sessionWait(s.id, 5_000);
104
- core.sessionResolve(s.id, "approve", "");
300
+ // Arrange
301
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
302
+ const wait = core.sessionWait(session.id, 5_000);
303
+
304
+ // Act
305
+ core.sessionResolve(session.id, "approve", "");
105
306
  const resolved = await wait;
307
+
308
+ // Assert
106
309
  expect(resolved!.verdict!.kind).toBe("approve");
107
310
  });
108
311
 
109
312
  test("wait times out to null; a later wait collects the stored verdict", async () => {
110
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
111
- const first = await core.sessionWait(s.id, 30);
313
+ // Arrange
314
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
315
+
316
+ // Act
317
+ const first = await core.sessionWait(session.id, 30);
318
+
319
+ // Assert
112
320
  expect(first).toBeNull();
113
- core.sessionResolve(s.id, "request_changes", "later");
114
- const second = await core.sessionWait(s.id, 30);
321
+
322
+ // Act
323
+ core.sessionResolve(session.id, "request_changes", "later");
324
+ const second = await core.sessionWait(session.id, 30);
325
+
326
+ // Assert
115
327
  expect(second!.verdict!.summary).toBe("later");
116
328
  });
117
329
 
118
330
  test("multiple waiters all resolve", async () => {
119
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
120
- const waits = [core.sessionWait(s.id, 5_000), core.sessionWait(s.id, 5_000)];
121
- core.sessionResolve(s.id, "approve", "");
331
+ // Arrange
332
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
333
+ const waits = [core.sessionWait(session.id, 5_000), core.sessionWait(session.id, 5_000)];
334
+
335
+ // Act
336
+ core.sessionResolve(session.id, "approve", "");
122
337
  const results = await Promise.all(waits);
123
- expect(results.every((r) => r?.status === "resolved")).toBe(true);
338
+
339
+ // Assert
340
+ expect(results.every((result) => result?.status === "resolved")).toBe(true);
124
341
  });
125
342
  });
126
343
 
127
344
  describe("persistence and recovery", () => {
128
345
  test("sessions survive a daemon restart", () => {
129
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
130
- core.sessionResolve(s.id, "approve", "done");
346
+ // Arrange
347
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
348
+
349
+ core.sessionResolve(session.id, "approve", "done");
350
+
351
+ // Act
131
352
  const reborn = new DaemonCore(home);
132
- const recovered = reborn.sessionGet(s.id);
353
+ const recovered = reborn.sessionGet(session.id);
354
+
355
+ // Assert
133
356
  expect(recovered.verdict!.summary).toBe("done");
134
357
  });
135
358
 
136
359
  test("corrupt records are skipped, not fatal, and never deleted", async () => {
360
+ // Arrange
137
361
  core.sessionCreate({ workspace: WS, artifact: PLAN });
138
362
  await Bun.write(join(home, "sessions", "broken.json"), "{ not json");
139
363
  const store = new SessionStore(home);
364
+
365
+ // Act
140
366
  const report = store.recover();
367
+
368
+ // Assert
141
369
  expect(report.recovered.length).toBe(1);
142
370
  expect(report.skipped.length).toBe(1);
143
371
  expect(await Bun.file(join(home, "sessions", "broken.json")).exists()).toBe(true);
@@ -146,11 +374,29 @@ describe("persistence and recovery", () => {
146
374
 
147
375
  describe("events", () => {
148
376
  test("lifecycle events fire in order", () => {
377
+ // Arrange
149
378
  const seen: string[] = [];
150
- core.onEvent((e) => seen.push(e.event));
151
- const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
152
- core.sessionAnnotate(s.id, { id: "a1", kind: "comment", anchor: { quote: "thing", prefix: "", suffix: "" }, body: "b" });
153
- core.sessionResolve(s.id, "approve", "");
154
- expect(seen).toEqual(["session.created", "inbox.changed", "session.updated", "session.resolved", "inbox.changed"]);
379
+
380
+ core.onEvent((event) => seen.push(event.event));
381
+
382
+ // Act
383
+ const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
384
+
385
+ core.sessionAnnotate(session.id, {
386
+ id: "a1",
387
+ kind: "comment",
388
+ anchor: { quote: "thing", prefix: "", suffix: "" },
389
+ body: "b",
390
+ });
391
+ core.sessionResolve(session.id, "approve", "");
392
+
393
+ // Assert
394
+ expect(seen).toEqual([
395
+ "session.created",
396
+ "inbox.changed",
397
+ "session.updated",
398
+ "session.resolved",
399
+ "inbox.changed",
400
+ ]);
155
401
  });
156
402
  });