@cueloop/daemon 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +2 -2
- package/src/api.test.ts +14 -0
- package/src/api.ts +20 -0
- package/src/client.ts +3 -0
- package/src/review.test.ts +17 -0
- package/src/review.ts +37 -3
- package/src/server.ts +4 -0
- package/src/validate.test.ts +11 -0
- package/src/validate.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/daemon",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.16",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"./review": "./src/review.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
12
|
+
"@cueloop/schema": "0.1.0-alpha.16",
|
|
13
13
|
"valibot": "^1.4.2"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
package/src/api.test.ts
CHANGED
|
@@ -84,6 +84,20 @@ describe("session lifecycle", () => {
|
|
|
84
84
|
expect(core.sessionGet(s.id).workingCopy).toBeUndefined();
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
test("viewed paths merge, dedupe, clear on empty, and survive a daemon restart", () => {
|
|
88
|
+
const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
89
|
+
core.sessionSetViewed(s.id, ["src/a.ts", "src/b.ts", "src/a.ts"]);
|
|
90
|
+
expect(core.sessionGet(s.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts"]);
|
|
91
|
+
// merge-additive: a stale client sending only its own new mark loses nothing
|
|
92
|
+
core.sessionSetViewed(s.id, ["src/c.ts"]);
|
|
93
|
+
expect(core.sessionGet(s.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
|
|
94
|
+
// a resumed review reads its progress back after a daemon restart
|
|
95
|
+
const reborn = new DaemonCore(home);
|
|
96
|
+
expect(reborn.sessionGet(s.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
|
|
97
|
+
core.sessionSetViewed(s.id, []);
|
|
98
|
+
expect(core.sessionGet(s.id).viewedPaths).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
87
101
|
test("revision reopens the session and resets working copy + verdict", () => {
|
|
88
102
|
const s = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
89
103
|
core.sessionSetWorkingCopy(s.id, PLAN.content + "\nedit");
|
package/src/api.ts
CHANGED
|
@@ -150,6 +150,26 @@ export class DaemonCore {
|
|
|
150
150
|
return s;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The guided walk's viewed marks. The full list replaces the stored one
|
|
155
|
+
* (last write wins - one reviewer walks at a time); an empty list clears
|
|
156
|
+
* the field so untouched records stay lean. Duplicates collapse here so
|
|
157
|
+
* the record never accumulates repeats from racing clients.
|
|
158
|
+
*/
|
|
159
|
+
/**
|
|
160
|
+
* Merge-additive: walking only ever adds marks, so concurrent or stale
|
|
161
|
+
* clients converge instead of overwriting each other. An empty array is the
|
|
162
|
+
* explicit reset.
|
|
163
|
+
*/
|
|
164
|
+
sessionSetViewed(id: string, viewedPaths: string[]): ReviewSession {
|
|
165
|
+
const s = this.mutable(id);
|
|
166
|
+
if (viewedPaths.length === 0) delete s.viewedPaths;
|
|
167
|
+
else s.viewedPaths = [...new Set([...(s.viewedPaths ?? []), ...viewedPaths])];
|
|
168
|
+
this.store.upsert(s);
|
|
169
|
+
this.emit("session.updated", id);
|
|
170
|
+
return s;
|
|
171
|
+
}
|
|
172
|
+
|
|
153
173
|
sessionResolve(id: string, verdictKind: VerdictKind, summary: string): ReviewSession {
|
|
154
174
|
const s = this.mutable(id);
|
|
155
175
|
const verdict: Verdict = {
|
package/src/client.ts
CHANGED
|
@@ -159,6 +159,9 @@ export class DaemonClient {
|
|
|
159
159
|
sessionSetWorkingCopy(id: string, workingCopy: string | undefined): Promise<ReviewSession> {
|
|
160
160
|
return this.request("session.setWorkingCopy", { id, workingCopy });
|
|
161
161
|
}
|
|
162
|
+
sessionSetViewed(id: string, viewedPaths: string[]): Promise<ReviewSession> {
|
|
163
|
+
return this.request("session.setViewed", { id, viewedPaths });
|
|
164
|
+
}
|
|
162
165
|
sessionResolve(id: string, verdictKind: VerdictKind, summary: string): Promise<ReviewSession> {
|
|
163
166
|
return this.request("session.resolve", { id, verdictKind, summary });
|
|
164
167
|
}
|
package/src/review.test.ts
CHANGED
|
@@ -99,6 +99,23 @@ describe("openReview", () => {
|
|
|
99
99
|
const other = await openReview(client, { type: "plan", content: PLAN, cwd: home, agentSessionId: "agent-2" });
|
|
100
100
|
expect(other.id).not.toBe(first.id);
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
test("per-file notes land as note annotations anchored at the file path", async () => {
|
|
104
|
+
const review = await openReview(client, {
|
|
105
|
+
type: "diff",
|
|
106
|
+
content: "d",
|
|
107
|
+
cwd: home,
|
|
108
|
+
notes: [
|
|
109
|
+
{ path: "src/a.ts", body: "Renames b and adds c." },
|
|
110
|
+
{ path: "src/b.ts", body: "Swaps the line." },
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
const notes = review.session.annotations;
|
|
114
|
+
expect(notes.length).toBe(2);
|
|
115
|
+
expect(notes.every((annotation) => annotation.kind === "note")).toBe(true);
|
|
116
|
+
expect(notes.map((annotation) => annotation.anchor.quote)).toEqual(["src/a.ts", "src/b.ts"]);
|
|
117
|
+
expect(notes[0]!.body).toBe("Renames b and adds c.");
|
|
118
|
+
});
|
|
102
119
|
});
|
|
103
120
|
|
|
104
121
|
describe("awaitVerdict: one long-poll (the hook shape)", () => {
|
package/src/review.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* serializing the decision in its host's contract.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { newAnnotationId, type ArtifactType, type ReviewSession, type WorkspaceKey } from "@cueloop/schema";
|
|
11
11
|
import { verdictResponse } from "./api";
|
|
12
12
|
import type { DaemonClient } from "./client";
|
|
13
13
|
|
|
@@ -53,6 +53,31 @@ export interface OpenReviewOptions {
|
|
|
53
53
|
herdrPane?: string;
|
|
54
54
|
/** Defaults to the plan's first markdown heading; diffs get no derived title. */
|
|
55
55
|
title?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Per-file agent notes for diff sessions: the submitting agent's own
|
|
58
|
+
* explanation of each file's change, in dead prose. Stored as annotations
|
|
59
|
+
* with kind "note" anchored at the file path, so they render as regular
|
|
60
|
+
* rail cards and feed the guided walk's agent-note block.
|
|
61
|
+
*/
|
|
62
|
+
notes?: ReviewNote[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The note contract: one note per changed file, anchored by the file path. */
|
|
66
|
+
export interface ReviewNote {
|
|
67
|
+
path: string;
|
|
68
|
+
body: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Anchor a note at its file: quote = the path, no context selectors. */
|
|
72
|
+
async function attachNotes(client: DaemonClient, sessionId: string, notes: ReviewNote[]): Promise<void> {
|
|
73
|
+
for (const note of notes) {
|
|
74
|
+
await client.sessionAnnotate(sessionId, {
|
|
75
|
+
id: newAnnotationId(),
|
|
76
|
+
kind: "note",
|
|
77
|
+
anchor: { quote: note.path, prefix: "", suffix: "" },
|
|
78
|
+
body: note.body,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
export interface AwaitVerdictOptions {
|
|
@@ -154,10 +179,15 @@ export async function openReview(client: DaemonClient, opts: OpenReviewOptions):
|
|
|
154
179
|
(s) => s.artifact.meta.agentSessionId === opts.agentSessionId,
|
|
155
180
|
);
|
|
156
181
|
if (existing !== undefined) {
|
|
157
|
-
|
|
182
|
+
let revised = await client.sessionSubmitRevision(existing.id, opts.content);
|
|
183
|
+
if (opts.notes?.length) {
|
|
184
|
+
await attachNotes(client, revised.id, opts.notes);
|
|
185
|
+
revised = await client.sessionGet(revised.id);
|
|
186
|
+
}
|
|
187
|
+
return new ReviewHandle(client, revised);
|
|
158
188
|
}
|
|
159
189
|
}
|
|
160
|
-
|
|
190
|
+
let session = await client.sessionCreate(workspace, {
|
|
161
191
|
type: opts.type,
|
|
162
192
|
content: opts.content,
|
|
163
193
|
meta: {
|
|
@@ -170,5 +200,9 @@ export async function openReview(client: DaemonClient, opts: OpenReviewOptions):
|
|
|
170
200
|
cwd,
|
|
171
201
|
},
|
|
172
202
|
});
|
|
203
|
+
if (opts.notes?.length) {
|
|
204
|
+
await attachNotes(client, session.id, opts.notes);
|
|
205
|
+
session = await client.sessionGet(session.id);
|
|
206
|
+
}
|
|
173
207
|
return new ReviewHandle(client, session);
|
|
174
208
|
}
|
package/src/server.ts
CHANGED
|
@@ -241,6 +241,10 @@ export class DaemonServer {
|
|
|
241
241
|
const p = parseParams("session.setWorkingCopy", req.params);
|
|
242
242
|
return core.sessionSetWorkingCopy(p.id, p.workingCopy);
|
|
243
243
|
}
|
|
244
|
+
case "session.setViewed": {
|
|
245
|
+
const p = parseParams("session.setViewed", req.params);
|
|
246
|
+
return core.sessionSetViewed(p.id, p.viewedPaths);
|
|
247
|
+
}
|
|
244
248
|
case "session.resolve": {
|
|
245
249
|
const p = parseParams("session.resolve", req.params);
|
|
246
250
|
return core.sessionResolve(p.id, p.verdictKind, p.summary);
|
package/src/validate.test.ts
CHANGED
|
@@ -89,6 +89,16 @@ describe("parseParams", () => {
|
|
|
89
89
|
expect(p.annotation.anchor.prefix).toBe("");
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
test("setViewed takes a full path list and rejects non-string entries", () => {
|
|
93
|
+
expect(parseParams("session.setViewed", { id: "s", viewedPaths: ["src/a.ts", "src/b.ts"] }).viewedPaths).toEqual([
|
|
94
|
+
"src/a.ts",
|
|
95
|
+
"src/b.ts",
|
|
96
|
+
]);
|
|
97
|
+
expect(parseParams("session.setViewed", { id: "s", viewedPaths: [] }).viewedPaths).toEqual([]);
|
|
98
|
+
expect(() => parseParams("session.setViewed", { id: "s", viewedPaths: [1] })).toThrow(/viewedPaths/);
|
|
99
|
+
expect(() => parseParams("session.setViewed", { id: "s" })).toThrow(/viewedPaths/);
|
|
100
|
+
});
|
|
101
|
+
|
|
92
102
|
test("null params are treated as empty", () => {
|
|
93
103
|
expect(parseParams("daemon.ping", null)).toEqual({});
|
|
94
104
|
});
|
|
@@ -163,6 +173,7 @@ describe("wire pins", () => {
|
|
|
163
173
|
revisions: [fullRevision],
|
|
164
174
|
annotations: [fullAnnotation],
|
|
165
175
|
workingCopy: "# P edited",
|
|
176
|
+
viewedPaths: ["src/a.ts"],
|
|
166
177
|
verdict: fullVerdict,
|
|
167
178
|
status: "pending",
|
|
168
179
|
createdAt: "now",
|
package/src/validate.ts
CHANGED
|
@@ -95,6 +95,7 @@ export const Params = {
|
|
|
95
95
|
"session.annotate": v.object({ id: SessionId, annotation: AnnotationSchema }),
|
|
96
96
|
"session.removeAnnotation": v.object({ id: SessionId, annotationId: NonEmpty }),
|
|
97
97
|
"session.setWorkingCopy": v.object({ id: SessionId, workingCopy: v.optional(v.string()) }),
|
|
98
|
+
"session.setViewed": v.object({ id: SessionId, viewedPaths: v.array(v.string()) }),
|
|
98
99
|
"session.resolve": v.object({
|
|
99
100
|
id: SessionId,
|
|
100
101
|
verdictKind: v.picklist(["comment", "approve", "request_changes"]),
|
|
@@ -145,6 +146,7 @@ export const SessionRecordSchema = v.object({
|
|
|
145
146
|
revisions: v.array(RevisionSchema),
|
|
146
147
|
annotations: v.array(v.object({ ...AnnotationSchema.entries, createdAt: v.string() } satisfies EntriesOf<Annotation>)),
|
|
147
148
|
workingCopy: v.optional(v.string()),
|
|
149
|
+
viewedPaths: v.optional(v.array(v.string())),
|
|
148
150
|
verdict: v.nullable(VerdictSchema),
|
|
149
151
|
status: v.picklist(["pending", "resolved"]),
|
|
150
152
|
createdAt: v.string(),
|