@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/schema",
3
- "version": "0.1.0-alpha.79",
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": {
@@ -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
- verdictKind: "request_changes",
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: request changes");
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
- verdictKind: "request_changes",
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
- verdictKind: "request_changes",
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
- verdictKind: "request_changes",
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
- verdictKind: "request_changes",
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
- verdictKind: "comment",
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
- verdictKind: "comment",
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
- verdictKind: "comment",
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
- verdictKind: "comment",
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
- verdictKind: "approve",
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
- verdictKind: "approve",
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
- verdictKind: "request_changes",
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
- verdictKind: "request_changes",
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
- verdictKind: "request_changes",
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
- verdictKind: "request_changes",
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
- verdictKind: "comment",
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 VerdictKind,
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
- verdictKind: VerdictKind;
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.verdictKind.replace("_", " "));
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
- verdictKind: VerdictKind,
250
+ outcome: MessageOutcome,
251
251
  summary: string,
252
252
  actionBodies?: Record<string, string>,
253
253
  ): string {
254
254
  return renderFeedback({
255
- verdictKind,
255
+ outcome,
256
256
  summary,
257
257
  artifactContent: session.artifact.content,
258
258
  workingCopy: session.workingCopy,
@@ -18,10 +18,16 @@ import {
18
18
  validateHistory,
19
19
  type SessionHistory,
20
20
  } from "./history";
21
- import type { Annotation, Verdict } from "./types";
21
+ import type { Annotation, Message } from "./types";
22
22
 
23
23
  const AT = "2026-09-01T10:00:00.000Z";
24
- const VERDICT: Verdict = { kind: "approve", summary: "", feedback: "", resolvedAt: AT };
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
- verdict: null,
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 verdicts", () => {
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: "verdict", verdict: VERDICT, createdAt: AT }).history;
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.verdicts).toEqual([VERDICT]);
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
- "verdict",
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 verdicts, starts at the head", () => {
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: "verdict", verdict: VERDICT, createdAt: AT }).history;
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 verdict entry, so it does not travel
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, verdict, rev2 by the agent, a reviewer edit, a comment on top
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: "verdict", verdict: VERDICT, createdAt: AT }).history;
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 verdict", () => {
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
- verdict: VERDICT,
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", "verdict:"]);
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.verdicts).toEqual([VERDICT]);
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
- verdict: null,
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
- verdict: null,
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
- verdict: null,
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 verdict history - derives from the active path.
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, Verdict } from "./types";
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: "verdict"; verdict: Verdict })
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
- /** Verdicts on the path, oldest first. */
165
- verdicts: Verdict[];
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 verdicts: Verdict[] = [];
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 === "verdict") verdicts.push(entry.verdict);
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
- verdicts,
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 verdicts and the reviewer's edits, with the tip
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 === "verdict" || (entry.type === "revision" && entry.by === "reviewer"))
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 verdict (when resolved) after the last revision.
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" | "verdict" | "createdAt">,
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.verdict) {
406
+ if (session.message) {
407
407
  push({
408
- id: `${session.id}_verdict`,
409
- type: "verdict",
410
- verdict: session.verdict,
411
- createdAt: session.verdict.resolvedAt,
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);
@@ -10,7 +10,7 @@ function emptySession(): Thread {
10
10
  artifact: { type: "plan", content: "# Plan\n", meta: {} },
11
11
  revisions: [],
12
12
  annotations: [],
13
- verdict: null,
13
+ message: null,
14
14
  status: "pending",
15
15
  createdAt: "2026-01-01T00:00:00.000Z",
16
16
  };
@@ -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
- verdict: null,
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
- verdict: null,
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
- verdict: null,
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
- verdict: null,
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
- verdict: null,
189
+ message: null,
190
190
  status: "pending",
191
191
  createdAt: AT,
192
192
  };
@@ -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
- verdict: null,
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
- verdictAllows,
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("verdictAllows", () => {
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(verdictAllows("approve")).toBe(true);
39
- expect(verdictAllows("comment")).toBe(false);
40
- expect(verdictAllows("request_changes")).toBe(false);
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
- * resolves this one noun. This module is pure data shapes - no IO, no
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 verdict can be posted back. */
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
- export type VerdictKind = "comment" | "approve" | "request_changes";
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
- export interface Verdict {
204
- kind: VerdictKind;
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 one structured feedback document sent to the agent. */
207
- feedback: string;
208
- resolvedAt: string;
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
- verdict: Verdict | null;
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
- /** comment and request_changes both map to deny in agent-native contracts. */
320
- export function verdictAllows(kind: VerdictKind): boolean {
321
- return kind === "approve";
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
  /**