@cueloop/schema 0.1.0-alpha.79 → 0.1.0-alpha.80
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 +1 -1
- package/src/feedback.test.ts +17 -17
- package/src/feedback.ts +5 -5
- package/src/history.test.ts +25 -19
- package/src/history.ts +17 -17
- package/src/participants.test.ts +1 -1
- package/src/path-view.test.ts +5 -5
- package/src/share-links.test.ts +1 -1
- package/src/types.test.ts +14 -5
- package/src/types.ts +55 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/schema",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.80",
|
|
4
4
|
"description": "The Thread primitive: types, markdown block model, quote-primary anchors, feedback serialization",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
package/src/feedback.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ describe("renderFeedback", () => {
|
|
|
29
29
|
test("plan edits section carries the one unified diff, directive framing first", () => {
|
|
30
30
|
// Act
|
|
31
31
|
const feedback = renderFeedback({
|
|
32
|
-
|
|
32
|
+
outcome: "changes_requested",
|
|
33
33
|
summary: "Tighten the storage section.",
|
|
34
34
|
artifactContent: PLAN,
|
|
35
35
|
workingCopy: PLAN.replace("one JSON document", "one JSON record"),
|
|
@@ -38,7 +38,7 @@ describe("renderFeedback", () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// Assert
|
|
41
|
-
expect(feedback).toContain("# Review:
|
|
41
|
+
expect(feedback).toContain("# Review: changes requested");
|
|
42
42
|
expect(feedback).toContain("Tighten the storage section.");
|
|
43
43
|
expect(feedback).toContain("## Plan edits");
|
|
44
44
|
expect(feedback).toContain("Apply this exact diff first");
|
|
@@ -50,7 +50,7 @@ describe("renderFeedback", () => {
|
|
|
50
50
|
test("reply edits section is labelled for the reply artifact, not a plan", () => {
|
|
51
51
|
// Act
|
|
52
52
|
const feedback = renderFeedback({
|
|
53
|
-
|
|
53
|
+
outcome: "changes_requested",
|
|
54
54
|
summary: "",
|
|
55
55
|
artifactContent: PLAN,
|
|
56
56
|
workingCopy: PLAN.replace("one JSON document", "one JSON record"),
|
|
@@ -68,7 +68,7 @@ describe("renderFeedback", () => {
|
|
|
68
68
|
test("a reply review with no artifact path references reply.md, never plan.md", () => {
|
|
69
69
|
// Act
|
|
70
70
|
const feedback = renderFeedback({
|
|
71
|
-
|
|
71
|
+
outcome: "changes_requested",
|
|
72
72
|
summary: "",
|
|
73
73
|
artifactContent: "# Findings\n\nThe cache is fine.\n",
|
|
74
74
|
artifactType: "reply",
|
|
@@ -101,7 +101,7 @@ describe("renderFeedback", () => {
|
|
|
101
101
|
|
|
102
102
|
// Act
|
|
103
103
|
const feedback = renderFeedback({
|
|
104
|
-
|
|
104
|
+
outcome: "changes_requested",
|
|
105
105
|
summary: "",
|
|
106
106
|
artifactContent: submittedPatch,
|
|
107
107
|
workingCopy: curatedPatch,
|
|
@@ -120,7 +120,7 @@ describe("renderFeedback", () => {
|
|
|
120
120
|
test("a diff working copy that rejected everything says so", () => {
|
|
121
121
|
// Act
|
|
122
122
|
const feedback = renderFeedback({
|
|
123
|
-
|
|
123
|
+
outcome: "changes_requested",
|
|
124
124
|
summary: "",
|
|
125
125
|
artifactContent: "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-a\n+b\n",
|
|
126
126
|
workingCopy: "",
|
|
@@ -136,7 +136,7 @@ describe("renderFeedback", () => {
|
|
|
136
136
|
test("annotations are located by quote and section", () => {
|
|
137
137
|
// Act
|
|
138
138
|
const feedback = renderFeedback({
|
|
139
|
-
|
|
139
|
+
outcome: "changes_requested",
|
|
140
140
|
summary: "",
|
|
141
141
|
artifactContent: PLAN,
|
|
142
142
|
annotations: [
|
|
@@ -159,7 +159,7 @@ describe("renderFeedback", () => {
|
|
|
159
159
|
|
|
160
160
|
// Act
|
|
161
161
|
const feedback = renderFeedback({
|
|
162
|
-
|
|
162
|
+
outcome: "changes_requested",
|
|
163
163
|
summary: "",
|
|
164
164
|
artifactContent: PLAN,
|
|
165
165
|
annotations: [
|
|
@@ -199,7 +199,7 @@ describe("renderFeedback", () => {
|
|
|
199
199
|
|
|
200
200
|
// Act
|
|
201
201
|
const feedback = renderFeedback({
|
|
202
|
-
|
|
202
|
+
outcome: "changes_requested",
|
|
203
203
|
summary: "",
|
|
204
204
|
artifactContent: PLAN,
|
|
205
205
|
annotations: [
|
|
@@ -223,7 +223,7 @@ describe("renderFeedback", () => {
|
|
|
223
223
|
test("orphaned anchors are flagged, never dropped", () => {
|
|
224
224
|
// Act
|
|
225
225
|
const feedback = renderFeedback({
|
|
226
|
-
|
|
226
|
+
outcome: "changes_requested",
|
|
227
227
|
summary: "",
|
|
228
228
|
artifactContent: PLAN,
|
|
229
229
|
annotations: [
|
|
@@ -239,7 +239,7 @@ describe("renderFeedback", () => {
|
|
|
239
239
|
test("empty review says so explicitly", () => {
|
|
240
240
|
// Act
|
|
241
241
|
const feedback = renderFeedback({
|
|
242
|
-
|
|
242
|
+
outcome: "approved",
|
|
243
243
|
summary: "",
|
|
244
244
|
artifactContent: PLAN,
|
|
245
245
|
annotations: [],
|
|
@@ -259,7 +259,7 @@ describe("renderFeedback", () => {
|
|
|
259
259
|
|
|
260
260
|
// Act
|
|
261
261
|
const noteOnly = renderFeedback({
|
|
262
|
-
|
|
262
|
+
outcome: "approved",
|
|
263
263
|
summary: "",
|
|
264
264
|
artifactContent: PLAN,
|
|
265
265
|
annotations: [note],
|
|
@@ -269,7 +269,7 @@ describe("renderFeedback", () => {
|
|
|
269
269
|
expect(noteOnly).toContain("_No edits or annotations._");
|
|
270
270
|
expect(noteOnly).not.toContain("The agent's own explanation of the change.");
|
|
271
271
|
const mixed = renderFeedback({
|
|
272
|
-
|
|
272
|
+
outcome: "changes_requested",
|
|
273
273
|
summary: "",
|
|
274
274
|
artifactContent: PLAN,
|
|
275
275
|
annotations: [
|
|
@@ -291,7 +291,7 @@ describe("renderFeedback", () => {
|
|
|
291
291
|
|
|
292
292
|
// Act
|
|
293
293
|
const feedback = renderFeedback({
|
|
294
|
-
|
|
294
|
+
outcome: "changes_requested",
|
|
295
295
|
summary: "",
|
|
296
296
|
artifactContent: PLAN,
|
|
297
297
|
workingCopy: working,
|
|
@@ -309,7 +309,7 @@ describe("renderFeedback", () => {
|
|
|
309
309
|
test("a prototype annotation locates by its selector and is never orphan-flagged", () => {
|
|
310
310
|
// Act
|
|
311
311
|
const feedback = renderFeedback({
|
|
312
|
-
|
|
312
|
+
outcome: "changes_requested",
|
|
313
313
|
summary: "",
|
|
314
314
|
artifactType: "prototype",
|
|
315
315
|
artifactContent: "<main><div class='card'>Pricing</div></main>",
|
|
@@ -333,7 +333,7 @@ describe("renderFeedback", () => {
|
|
|
333
333
|
describe("renderFeedback target grouping", () => {
|
|
334
334
|
test("file notes group under their own path section, apart from the artifact", () => {
|
|
335
335
|
const feedback = renderFeedback({
|
|
336
|
-
|
|
336
|
+
outcome: "changes_requested",
|
|
337
337
|
summary: "",
|
|
338
338
|
artifactContent: PLAN,
|
|
339
339
|
artifactPath: "docs/plan.md",
|
|
@@ -371,7 +371,7 @@ describe("renderFeedback target grouping", () => {
|
|
|
371
371
|
|
|
372
372
|
test("a quick-action reference expands to its body; an unknown skill reference passes through", () => {
|
|
373
373
|
const feedback = renderFeedback({
|
|
374
|
-
|
|
374
|
+
outcome: "changes_requested",
|
|
375
375
|
summary: "",
|
|
376
376
|
artifactContent: PLAN,
|
|
377
377
|
annotations: [
|
package/src/feedback.ts
CHANGED
|
@@ -12,14 +12,14 @@ import {
|
|
|
12
12
|
type Annotation,
|
|
13
13
|
type ArtifactType,
|
|
14
14
|
type Thread,
|
|
15
|
-
type
|
|
15
|
+
type MessageOutcome,
|
|
16
16
|
} from "./types";
|
|
17
17
|
import { parseBlocks, sectionOf, type Block } from "./markdown";
|
|
18
18
|
import { resolveAnchor } from "./anchor";
|
|
19
19
|
import { unifiedDiffText } from "./diff";
|
|
20
20
|
|
|
21
21
|
export interface FeedbackInput {
|
|
22
|
-
|
|
22
|
+
outcome: MessageOutcome;
|
|
23
23
|
summary: string;
|
|
24
24
|
/** The submitted artifact content (latest revision). */
|
|
25
25
|
artifactContent: string;
|
|
@@ -80,7 +80,7 @@ export function renderFeedback(input: FeedbackInput): string {
|
|
|
80
80
|
);
|
|
81
81
|
const lines: string[] = [];
|
|
82
82
|
|
|
83
|
-
lines.push("# Review: " + input.
|
|
83
|
+
lines.push("# Review: " + input.outcome.replaceAll("_", " "));
|
|
84
84
|
lines.push("");
|
|
85
85
|
if (input.summary.trim()) {
|
|
86
86
|
lines.push(input.summary.trim());
|
|
@@ -247,12 +247,12 @@ function annotationSectionLines(
|
|
|
247
247
|
|
|
248
248
|
export function feedbackForSession(
|
|
249
249
|
session: Thread,
|
|
250
|
-
|
|
250
|
+
outcome: MessageOutcome,
|
|
251
251
|
summary: string,
|
|
252
252
|
actionBodies?: Record<string, string>,
|
|
253
253
|
): string {
|
|
254
254
|
return renderFeedback({
|
|
255
|
-
|
|
255
|
+
outcome,
|
|
256
256
|
summary,
|
|
257
257
|
artifactContent: session.artifact.content,
|
|
258
258
|
workingCopy: session.workingCopy,
|
package/src/history.test.ts
CHANGED
|
@@ -18,10 +18,16 @@ import {
|
|
|
18
18
|
validateHistory,
|
|
19
19
|
type SessionHistory,
|
|
20
20
|
} from "./history";
|
|
21
|
-
import type { Annotation,
|
|
21
|
+
import type { Annotation, Message } from "./types";
|
|
22
22
|
|
|
23
23
|
const AT = "2026-09-01T10:00:00.000Z";
|
|
24
|
-
const
|
|
24
|
+
const MESSAGE: Message = {
|
|
25
|
+
id: "msg_1",
|
|
26
|
+
outcome: "approved",
|
|
27
|
+
summary: "",
|
|
28
|
+
body: "",
|
|
29
|
+
sentAt: AT,
|
|
30
|
+
};
|
|
25
31
|
|
|
26
32
|
function annotation(id: string, createdAt: string): Annotation {
|
|
27
33
|
return {
|
|
@@ -39,13 +45,13 @@ function root(): SessionHistory {
|
|
|
39
45
|
id: "ses_1",
|
|
40
46
|
revisions: [{ revision: 1, content: "Plan v1", submittedAt: AT }],
|
|
41
47
|
annotations: [],
|
|
42
|
-
|
|
48
|
+
message: null,
|
|
43
49
|
createdAt: AT,
|
|
44
50
|
});
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
describe("appendEntry and derivePath", () => {
|
|
48
|
-
test("entries chain on the current tip and the path derives the head, comments, and
|
|
54
|
+
test("entries chain on the current tip and the path derives the head, comments, and messages", () => {
|
|
49
55
|
// Arrange
|
|
50
56
|
let history = root();
|
|
51
57
|
|
|
@@ -57,7 +63,7 @@ describe("appendEntry and derivePath", () => {
|
|
|
57
63
|
annotationId: "a1",
|
|
58
64
|
createdAt: AT,
|
|
59
65
|
}).history;
|
|
60
|
-
history = appendEntry(history, { type: "
|
|
66
|
+
history = appendEntry(history, { type: "message", message: MESSAGE, createdAt: AT }).history;
|
|
61
67
|
history = appendEntry(history, {
|
|
62
68
|
type: "revision",
|
|
63
69
|
by: "agent",
|
|
@@ -70,14 +76,14 @@ describe("appendEntry and derivePath", () => {
|
|
|
70
76
|
|
|
71
77
|
expect(derived.head.content).toBe("Plan v2");
|
|
72
78
|
expect(derived.annotationIds).toEqual(["a2"]);
|
|
73
|
-
expect(derived.
|
|
79
|
+
expect(derived.messages).toEqual([MESSAGE]);
|
|
74
80
|
expect(derived.rounds).toBe(2);
|
|
75
81
|
expect(pathOf(history).map((entry) => entry.type)).toEqual([
|
|
76
82
|
"revision",
|
|
77
83
|
"comment",
|
|
78
84
|
"comment",
|
|
79
85
|
"comment-removed",
|
|
80
|
-
"
|
|
86
|
+
"message",
|
|
81
87
|
"revision",
|
|
82
88
|
]);
|
|
83
89
|
});
|
|
@@ -209,12 +215,12 @@ describe("validateHistory and cycles", () => {
|
|
|
209
215
|
});
|
|
210
216
|
|
|
211
217
|
describe("forkHistory", () => {
|
|
212
|
-
test("copies the path with its comments and labels, drops
|
|
218
|
+
test("copies the path with its comments and labels, drops messages, starts at the head", () => {
|
|
213
219
|
// Arrange
|
|
214
220
|
let history = root();
|
|
215
221
|
|
|
216
222
|
history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
|
|
217
|
-
history = appendEntry(history, { type: "
|
|
223
|
+
history = appendEntry(history, { type: "message", message: MESSAGE, createdAt: AT }).history;
|
|
218
224
|
history = labelTip(history, "approved v1");
|
|
219
225
|
const approvedTip = tipOf(history);
|
|
220
226
|
|
|
@@ -229,16 +235,16 @@ describe("forkHistory", () => {
|
|
|
229
235
|
expect(fork.entries.map((entry) => entry.type)).toEqual(["revision", "comment"]);
|
|
230
236
|
expect(fork.tips).toEqual({ [MAIN_BRANCH]: fork.entries[1]!.id });
|
|
231
237
|
expect(derivePath(fork).annotationIds).toEqual(["a1"]);
|
|
232
|
-
// the label sat on the dropped
|
|
238
|
+
// the label sat on the dropped message entry, so it does not travel
|
|
233
239
|
expect(fork.labels).toEqual({});
|
|
234
240
|
expect(history.labels[approvedTip]).toBe("approved v1");
|
|
235
241
|
});
|
|
236
242
|
|
|
237
243
|
test("entries after a dropped one are chained to the kept entry before it", () => {
|
|
238
|
-
// Arrange: rev1,
|
|
244
|
+
// Arrange: rev1, message, rev2 by the agent, a reviewer edit, a comment on top
|
|
239
245
|
let history = root();
|
|
240
246
|
|
|
241
|
-
history = appendEntry(history, { type: "
|
|
247
|
+
history = appendEntry(history, { type: "message", message: MESSAGE, createdAt: AT }).history;
|
|
242
248
|
history = appendEntry(history, {
|
|
243
249
|
type: "revision",
|
|
244
250
|
by: "agent",
|
|
@@ -289,7 +295,7 @@ describe("recaptureMainHead", () => {
|
|
|
289
295
|
});
|
|
290
296
|
|
|
291
297
|
describe("historyFromLinear", () => {
|
|
292
|
-
test("chains revisions on main, files comments after the revision they were made on, ends with the
|
|
298
|
+
test("chains revisions on main, files comments after the revision they were made on, ends with the message", () => {
|
|
293
299
|
// Act
|
|
294
300
|
const history = historyFromLinear({
|
|
295
301
|
id: "ses_9",
|
|
@@ -301,7 +307,7 @@ describe("historyFromLinear", () => {
|
|
|
301
307
|
annotation("late", "2026-09-01T13:00:00.000Z"),
|
|
302
308
|
annotation("early", "2026-09-01T11:00:00.000Z"),
|
|
303
309
|
],
|
|
304
|
-
|
|
310
|
+
message: MESSAGE,
|
|
305
311
|
createdAt: "2026-09-01T09:00:00.000Z",
|
|
306
312
|
});
|
|
307
313
|
|
|
@@ -310,14 +316,14 @@ describe("historyFromLinear", () => {
|
|
|
310
316
|
history.entries.map(
|
|
311
317
|
(entry) => `${entry.type}:${"annotationId" in entry ? entry.annotationId : ""}`,
|
|
312
318
|
),
|
|
313
|
-
).toEqual(["revision:", "comment:early", "revision:", "comment:late", "
|
|
319
|
+
).toEqual(["revision:", "comment:early", "revision:", "comment:late", "message:"]);
|
|
314
320
|
expect(history.entries[0]!.id).toBe("ses_9_rev1");
|
|
315
321
|
expect(history.branch).toBe(MAIN_BRANCH);
|
|
316
322
|
const derived = derivePath(history);
|
|
317
323
|
|
|
318
324
|
expect(derived.head.content).toBe("v2");
|
|
319
325
|
expect(derived.annotationIds).toEqual(["early", "late"]);
|
|
320
|
-
expect(derived.
|
|
326
|
+
expect(derived.messages).toEqual([MESSAGE]);
|
|
321
327
|
expect(derived.rounds).toBe(2);
|
|
322
328
|
});
|
|
323
329
|
|
|
@@ -327,7 +333,7 @@ describe("historyFromLinear", () => {
|
|
|
327
333
|
id: "ses_2",
|
|
328
334
|
revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
|
|
329
335
|
annotations: [annotation("a", AT)],
|
|
330
|
-
|
|
336
|
+
message: null,
|
|
331
337
|
createdAt: AT,
|
|
332
338
|
};
|
|
333
339
|
|
|
@@ -343,7 +349,7 @@ describe("followBranch", () => {
|
|
|
343
349
|
id: "ses_1",
|
|
344
350
|
revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
|
|
345
351
|
annotations: [],
|
|
346
|
-
|
|
352
|
+
message: null,
|
|
347
353
|
createdAt: AT,
|
|
348
354
|
});
|
|
349
355
|
|
|
@@ -373,7 +379,7 @@ describe("removalEntries", () => {
|
|
|
373
379
|
id: "ses_1",
|
|
374
380
|
revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
|
|
375
381
|
annotations: [],
|
|
376
|
-
|
|
382
|
+
message: null,
|
|
377
383
|
createdAt: AT,
|
|
378
384
|
});
|
|
379
385
|
|
package/src/history.ts
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* from the current branch's tip back to the root. Nothing is ever deleted:
|
|
5
5
|
* navigating moves a tip, a comment removal is its own entry, a fork copies a
|
|
6
6
|
* path into a new history. Every view of the session - the artifact text,
|
|
7
|
-
* the open comments, the
|
|
7
|
+
* the open comments, the message history - derives from the active path.
|
|
8
8
|
*
|
|
9
9
|
* Pure: no store, no daemon. The daemon owns persistence; the client and the
|
|
10
10
|
* feedback document read what this module derives.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { Annotation, Thread,
|
|
13
|
+
import type { Annotation, Thread, Message } from "./types";
|
|
14
14
|
|
|
15
15
|
export type EntryAuthor = "agent" | "reviewer";
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ export type SessionEntry =
|
|
|
25
25
|
| (EntryBase & { type: "revision"; by: EntryAuthor; content: string })
|
|
26
26
|
| (EntryBase & { type: "comment"; annotationId: string })
|
|
27
27
|
| (EntryBase & { type: "comment-removed"; annotationId: string })
|
|
28
|
-
| (EntryBase & { type: "
|
|
28
|
+
| (EntryBase & { type: "message"; message: Message })
|
|
29
29
|
| (EntryBase & { type: "branch-summary"; text: string; abandoned: string[] });
|
|
30
30
|
|
|
31
31
|
export type EntryType = SessionEntry["type"];
|
|
@@ -161,8 +161,8 @@ export interface DerivedPath {
|
|
|
161
161
|
* is addressed by a revision is the annotation's own state, not the path's.
|
|
162
162
|
*/
|
|
163
163
|
annotationIds: string[];
|
|
164
|
-
/**
|
|
165
|
-
|
|
164
|
+
/** Messages on the path, oldest first. */
|
|
165
|
+
messages: Message[];
|
|
166
166
|
summaries: Array<SessionEntry & { type: "branch-summary" }>;
|
|
167
167
|
/** Agent revisions on the path: one per round. */
|
|
168
168
|
rounds: number;
|
|
@@ -176,20 +176,20 @@ export function derivePath(history: SessionHistory, fromId?: string): DerivedPat
|
|
|
176
176
|
|
|
177
177
|
if (!head) throw new HistoryError("not-a-revision", "a path without a revision has no head");
|
|
178
178
|
const open = new Set<string>();
|
|
179
|
-
const
|
|
179
|
+
const messages: Message[] = [];
|
|
180
180
|
const summaries: Array<SessionEntry & { type: "branch-summary" }> = [];
|
|
181
181
|
|
|
182
182
|
for (const entry of path) {
|
|
183
183
|
if (entry.type === "comment") open.add(entry.annotationId);
|
|
184
184
|
if (entry.type === "comment-removed") open.delete(entry.annotationId);
|
|
185
|
-
if (entry.type === "
|
|
185
|
+
if (entry.type === "message") messages.push(entry.message);
|
|
186
186
|
if (entry.type === "branch-summary") summaries.push(entry);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
return {
|
|
190
190
|
head,
|
|
191
191
|
annotationIds: [...open],
|
|
192
|
-
|
|
192
|
+
messages,
|
|
193
193
|
summaries,
|
|
194
194
|
rounds: revisions.filter((entry) => entry.by === "agent").length,
|
|
195
195
|
};
|
|
@@ -301,7 +301,7 @@ export function navigateTo(
|
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
303
|
* A fork's history: the current path copied as one branch, keeping comments
|
|
304
|
-
* and labels on it, dropping
|
|
304
|
+
* and labels on it, dropping messages and the reviewer's edits, with the tip
|
|
305
305
|
* at the copied head. The kept entries are chained anew so no entry points at
|
|
306
306
|
* one that was dropped.
|
|
307
307
|
*/
|
|
@@ -310,7 +310,7 @@ export function forkHistory(history: SessionHistory): SessionHistory {
|
|
|
310
310
|
const labels: Record<string, string> = {};
|
|
311
311
|
|
|
312
312
|
for (const entry of pathOf(history)) {
|
|
313
|
-
if (entry.type === "
|
|
313
|
+
if (entry.type === "message" || (entry.type === "revision" && entry.by === "reviewer"))
|
|
314
314
|
continue;
|
|
315
315
|
kept.push({ ...entry, parentId: kept.at(-1)?.id ?? null });
|
|
316
316
|
const label = history.labels[entry.id];
|
|
@@ -357,11 +357,11 @@ export function removalEntries(
|
|
|
357
357
|
/**
|
|
358
358
|
* A linear session as a one-branch tree: its revisions chained on `main` as
|
|
359
359
|
* agent revisions, its comments attached after the revision that was current
|
|
360
|
-
* when they were made, its
|
|
360
|
+
* when they were made, its message (when resolved) after the last revision.
|
|
361
361
|
* Deterministic: the same record migrates to the same ids.
|
|
362
362
|
*/
|
|
363
363
|
export function historyFromLinear(
|
|
364
|
-
session: Pick<Thread, "id" | "revisions" | "annotations" | "
|
|
364
|
+
session: Pick<Thread, "id" | "revisions" | "annotations" | "message" | "createdAt">,
|
|
365
365
|
): SessionHistory {
|
|
366
366
|
const entries: SessionEntry[] = [];
|
|
367
367
|
let parentId: string | null = null;
|
|
@@ -403,12 +403,12 @@ export function historyFromLinear(
|
|
|
403
403
|
nextComment++;
|
|
404
404
|
}
|
|
405
405
|
});
|
|
406
|
-
if (session.
|
|
406
|
+
if (session.message) {
|
|
407
407
|
push({
|
|
408
|
-
id: `${session.id}
|
|
409
|
-
type: "
|
|
410
|
-
|
|
411
|
-
createdAt: session.
|
|
408
|
+
id: `${session.id}_message`,
|
|
409
|
+
type: "message",
|
|
410
|
+
message: session.message,
|
|
411
|
+
createdAt: session.message.sentAt,
|
|
412
412
|
});
|
|
413
413
|
}
|
|
414
414
|
const tip = entries.at(-1);
|
package/src/participants.test.ts
CHANGED
package/src/path-view.test.ts
CHANGED
|
@@ -28,7 +28,7 @@ function threaded(): SessionHistory {
|
|
|
28
28
|
id: "ses_1",
|
|
29
29
|
revisions: [{ revision: 1, content: "Plan v1", submittedAt: AT }],
|
|
30
30
|
annotations: [],
|
|
31
|
-
|
|
31
|
+
message: null,
|
|
32
32
|
createdAt: AT,
|
|
33
33
|
});
|
|
34
34
|
|
|
@@ -110,7 +110,7 @@ describe("applyPathView", () => {
|
|
|
110
110
|
annotations: [annotation("a2")],
|
|
111
111
|
shelvedAnnotations: [annotation("a1")],
|
|
112
112
|
workingCopy: "edited",
|
|
113
|
-
|
|
113
|
+
message: null,
|
|
114
114
|
status: "pending",
|
|
115
115
|
createdAt: AT,
|
|
116
116
|
};
|
|
@@ -149,7 +149,7 @@ describe("viewFollowing", () => {
|
|
|
149
149
|
shelvedAnnotations: [annotation("a2")],
|
|
150
150
|
workingCopy: "Plan v1 edited",
|
|
151
151
|
history,
|
|
152
|
-
|
|
152
|
+
message: null,
|
|
153
153
|
status: "pending",
|
|
154
154
|
createdAt: AT,
|
|
155
155
|
};
|
|
@@ -174,7 +174,7 @@ describe("viewFollowing", () => {
|
|
|
174
174
|
id: "ses_1",
|
|
175
175
|
revisions: [{ revision: 1, content: "Plan v1", submittedAt: AT }],
|
|
176
176
|
annotations: [],
|
|
177
|
-
|
|
177
|
+
message: null,
|
|
178
178
|
createdAt: AT,
|
|
179
179
|
});
|
|
180
180
|
const session: Thread = {
|
|
@@ -186,7 +186,7 @@ describe("viewFollowing", () => {
|
|
|
186
186
|
annotations: [],
|
|
187
187
|
shareBranch: "gone",
|
|
188
188
|
history,
|
|
189
|
-
|
|
189
|
+
message: null,
|
|
190
190
|
status: "pending",
|
|
191
191
|
createdAt: AT,
|
|
192
192
|
};
|
package/src/share-links.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ function thread(overrides: Partial<Thread> = {}): Thread {
|
|
|
10
10
|
artifact: { type: "plan", content: "# Plan\n", meta: {} },
|
|
11
11
|
revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00.000Z" }],
|
|
12
12
|
annotations: [],
|
|
13
|
-
|
|
13
|
+
message: null,
|
|
14
14
|
status: "pending",
|
|
15
15
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
16
16
|
...overrides,
|
package/src/types.test.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
annotationTarget,
|
|
4
4
|
ARTIFACT_TYPES,
|
|
5
|
+
WORKFLOW_KINDS,
|
|
5
6
|
isArtifactType,
|
|
6
7
|
isMarkdownArtifact,
|
|
7
8
|
newAnnotationId,
|
|
8
|
-
|
|
9
|
+
messageAllows,
|
|
9
10
|
} from "./types";
|
|
10
11
|
|
|
11
12
|
describe("annotationTarget", () => {
|
|
@@ -33,11 +34,19 @@ describe("ARTIFACT_TYPES", () => {
|
|
|
33
34
|
});
|
|
34
35
|
});
|
|
35
36
|
|
|
36
|
-
describe("
|
|
37
|
+
describe("WORKFLOW_KINDS", () => {
|
|
38
|
+
test("review and refine are workflows, not artifact types", () => {
|
|
39
|
+
expect(WORKFLOW_KINDS).toEqual(["plan", "reply", "prototype", "diff", "review", "refine"]);
|
|
40
|
+
expect(isArtifactType("review")).toBe(false);
|
|
41
|
+
expect(isArtifactType("refine")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("messageAllows", () => {
|
|
37
46
|
test("only approve maps to allow", () => {
|
|
38
|
-
expect(
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
47
|
+
expect(messageAllows("approved")).toBe(true);
|
|
48
|
+
expect(messageAllows("changes_requested")).toBe(false);
|
|
49
|
+
expect(messageAllows("changes_requested")).toBe(false);
|
|
41
50
|
});
|
|
42
51
|
});
|
|
43
52
|
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Thread primitive. Everything in cueloop renders, annotates, or
|
|
3
|
-
*
|
|
3
|
+
* extends this one noun. This module is pure data shapes - no IO, no
|
|
4
4
|
* dependencies beyond the history shapes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -33,6 +33,11 @@ export const ARTIFACT_TYPES = ["plan", "diff", "prototype", "reply"] as const;
|
|
|
33
33
|
|
|
34
34
|
export type ArtifactType = (typeof ARTIFACT_TYPES)[number];
|
|
35
35
|
|
|
36
|
+
/** Harness workflows; review and refine are compositions, never artifact types. */
|
|
37
|
+
export const WORKFLOW_KINDS = ["plan", "reply", "prototype", "diff", "review", "refine"] as const;
|
|
38
|
+
|
|
39
|
+
export type WorkflowKind = (typeof WORKFLOW_KINDS)[number];
|
|
40
|
+
|
|
36
41
|
/** Trust-boundary guard: is this string one of the artifact primitives? */
|
|
37
42
|
export function isArtifactType(value: string): value is ArtifactType {
|
|
38
43
|
return ARTIFACT_TYPES.some((candidate) => candidate === value);
|
|
@@ -51,6 +56,8 @@ export function isMarkdownArtifact(type: ArtifactType): boolean {
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export interface ArtifactMeta {
|
|
59
|
+
/** The workflow that submitted this artifact, distinct from its artifact type. */
|
|
60
|
+
workflow?: WorkflowKind;
|
|
54
61
|
cwd?: string;
|
|
55
62
|
agent?: string;
|
|
56
63
|
/** Agent-native session id, for resume/fork context. */
|
|
@@ -59,7 +66,7 @@ export interface ArtifactMeta {
|
|
|
59
66
|
planPath?: string;
|
|
60
67
|
/** Path to the prototype's entry HTML file on disk. */
|
|
61
68
|
prototypePath?: string;
|
|
62
|
-
/** Pull request reference the diff came from, so the
|
|
69
|
+
/** Pull request reference the diff came from, so the message can be posted back. */
|
|
63
70
|
pr?: string;
|
|
64
71
|
/** herdr pane the submitting agent runs in - the review returns focus there. */
|
|
65
72
|
herdrPane?: string;
|
|
@@ -198,14 +205,43 @@ export function isAddressed(annotation: Annotation): boolean {
|
|
|
198
205
|
return annotation.resolution !== undefined;
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
|
|
208
|
+
/** A harness session bound to a Thread. Harness identity stays outside the Thread itself. */
|
|
209
|
+
export interface HarnessBinding {
|
|
210
|
+
id: string;
|
|
211
|
+
threadId: string;
|
|
212
|
+
harness: string;
|
|
213
|
+
harnessSessionId: string;
|
|
214
|
+
createdAt: string;
|
|
215
|
+
/** Approval whose one unchanged plan resubmission this harness has consumed. */
|
|
216
|
+
approvedRetryMessageId?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Durable routing state for one Message sent to one harness binding. */
|
|
220
|
+
export interface Delivery {
|
|
221
|
+
id: string;
|
|
222
|
+
messageId: string;
|
|
223
|
+
bindingId: string;
|
|
224
|
+
status: "pending" | "acknowledged";
|
|
225
|
+
createdAt: string;
|
|
226
|
+
acknowledgedAt?: string;
|
|
227
|
+
}
|
|
202
228
|
|
|
203
|
-
|
|
204
|
-
|
|
229
|
+
/** What a harness adapter pulls: routing state plus its immutable Message payload. */
|
|
230
|
+
export interface PendingDelivery {
|
|
231
|
+
delivery: Delivery;
|
|
232
|
+
message: Message;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export type MessageOutcome = "approved" | "changes_requested";
|
|
236
|
+
|
|
237
|
+
export interface Message {
|
|
238
|
+
/** Stable identity used to deduplicate at-least-once harness delivery. */
|
|
239
|
+
id: string;
|
|
240
|
+
outcome: MessageOutcome;
|
|
205
241
|
summary: string;
|
|
206
|
-
/** The
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
/** The structured review document sent to the harness. */
|
|
243
|
+
body: string;
|
|
244
|
+
sentAt: string;
|
|
209
245
|
}
|
|
210
246
|
|
|
211
247
|
export interface Revision {
|
|
@@ -287,7 +323,7 @@ export interface Thread {
|
|
|
287
323
|
* progress. Undefined = the walk never started.
|
|
288
324
|
*/
|
|
289
325
|
viewedPaths?: string[];
|
|
290
|
-
|
|
326
|
+
message: Message | null;
|
|
291
327
|
status: SessionStatus;
|
|
292
328
|
createdAt: string;
|
|
293
329
|
/**
|
|
@@ -316,9 +352,16 @@ export interface Thread {
|
|
|
316
352
|
participants?: Identity[];
|
|
317
353
|
}
|
|
318
354
|
|
|
319
|
-
/**
|
|
320
|
-
export function
|
|
321
|
-
return
|
|
355
|
+
/** Whether a Message releases a harness gate. */
|
|
356
|
+
export function messageAllows(outcome: MessageOutcome): boolean {
|
|
357
|
+
return outcome === "approved";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let messageSeq = 0;
|
|
361
|
+
|
|
362
|
+
/** A process-unique, time-sortable Message id for delivery deduplication. */
|
|
363
|
+
export function newMessageId(): string {
|
|
364
|
+
return `msg_${Date.now().toString(36)}${(messageSeq++).toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
322
365
|
}
|
|
323
366
|
|
|
324
367
|
/**
|