cueloop 0.1.0-alpha.59 → 0.1.0-alpha.60

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",
3
- "version": "0.1.0-alpha.59",
3
+ "version": "0.1.0-alpha.60",
4
4
  "description": "Terminal review surface for coding agents: annotate plans and diffs, return a verdict, send structured feedback back",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -30,10 +30,10 @@
30
30
  "build:binary": "bun build --compile --external cpu-features ./src/main.ts --outfile dist/cueloop"
31
31
  },
32
32
  "dependencies": {
33
- "@cueloop/adapters": "0.1.0-alpha.59",
34
- "@cueloop/client": "0.1.0-alpha.59",
35
- "@cueloop/daemon": "0.1.0-alpha.59",
36
- "@cueloop/schema": "0.1.0-alpha.59"
33
+ "@cueloop/adapters": "0.1.0-alpha.60",
34
+ "@cueloop/client": "0.1.0-alpha.60",
35
+ "@cueloop/daemon": "0.1.0-alpha.60",
36
+ "@cueloop/schema": "0.1.0-alpha.60"
37
37
  },
38
38
  "engines": {
39
39
  "bun": ">=1.3.0"
@@ -13,8 +13,10 @@ import { parseArgs, stringFlag } from "./args";
13
13
  export async function actionsCommand(argv: string[]): Promise<number> {
14
14
  const { positional, flags } = parseArgs(argv);
15
15
  const verb = positional[0] ?? "list";
16
+
16
17
  if (verb !== "list") {
17
18
  console.error("usage: cueloop actions list [--session <id>]");
19
+
18
20
  return 2;
19
21
  }
20
22
  const role = stringFlag(flags, "role") === "agent" ? "agent" : undefined;
@@ -25,7 +27,9 @@ export async function actionsCommand(argv: string[]): Promise<number> {
25
27
  prompt: action.prompt,
26
28
  metadata: action.metadata,
27
29
  }));
30
+
28
31
  console.log(JSON.stringify(numbered, null, 2));
32
+
29
33
  return 0;
30
34
  }
31
35
 
@@ -36,6 +40,7 @@ async function repoRootForSession(
36
40
  ): Promise<string> {
37
41
  if (sessionId === undefined) return process.cwd();
38
42
  const client = await DaemonClient.connect({ autostart: true, role });
43
+
39
44
  try {
40
45
  return (await client.sessionGet(sessionId)).workspace.repoRoot;
41
46
  } finally {
package/src/args.ts CHANGED
@@ -8,18 +8,22 @@ export interface ParsedArgs {
8
8
  export function parseArgs(argv: string[]): ParsedArgs {
9
9
  const positional: string[] = [];
10
10
  const flags: Record<string, string | boolean> = {};
11
+
11
12
  for (let i = 0; i < argv.length; i++) {
12
13
  const argument = argv[i]!;
14
+
13
15
  if (!argument.startsWith("--")) {
14
16
  positional.push(argument);
15
17
  continue;
16
18
  }
17
19
  const equalsIndex = argument.indexOf("=");
20
+
18
21
  if (equalsIndex !== -1) {
19
22
  flags[argument.slice(2, equalsIndex)] = argument.slice(equalsIndex + 1);
20
23
  } else {
21
24
  const key = argument.slice(2);
22
25
  const next = argv[i + 1];
26
+
23
27
  if (next !== undefined && !next.startsWith("--")) {
24
28
  flags[key] = next;
25
29
  i++;
@@ -28,6 +32,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
28
32
  }
29
33
  }
30
34
  }
35
+
31
36
  return { positional, flags };
32
37
  }
33
38
 
@@ -36,5 +41,6 @@ export function stringFlag(
36
41
  key: string,
37
42
  ): string | undefined {
38
43
  const value = flags[key];
44
+
39
45
  return typeof value === "string" ? value : undefined;
40
46
  }
package/src/main.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * cueloop entry points: `cueloop` opens the TUI on the inbox, the verb-first
4
- * openers `cueloop plan|diff|review` open the latest pending review of that
5
- * type (or address one by id/title), `cueloop diff`/`cueloop review <pr>` also
4
+ * openers `cueloop plan|reply|diff|review` open the latest pending review of
5
+ * that type (or address one by id/title), `cueloop diff`/`cueloop review <pr>` also
6
6
  * keep their create paths, `cueloop session *` mirrors the daemon API for
7
7
  * agents and scripts, `cueloop serve` shares a session over ssh (read-only
8
8
  * observers), `cueloop daemon` runs the daemon in the foreground.
@@ -15,6 +15,7 @@ import {
15
15
  isPlanReview,
16
16
  isPrototypeReview,
17
17
  isPrReview,
18
+ isReplyReview,
18
19
  isSessionId,
19
20
  openTargetMessage,
20
21
  resolveOpenTarget,
@@ -33,12 +34,15 @@ async function daemonCommand(): Promise<number> {
33
34
  const { DaemonServer } = await import("@cueloop/daemon");
34
35
  const server = new DaemonServer({ idleExitMs: 0 });
35
36
  const path = server.start();
37
+
36
38
  if (path === null) {
37
39
  console.error("a cueloop daemon already owns this home - nothing to do");
40
+
38
41
  return 1;
39
42
  }
40
43
  console.log(`cueloop daemon (foreground) on ${path}`);
41
44
  await new Promise(() => {}); // run until signalled
45
+
42
46
  return 0;
43
47
  }
44
48
 
@@ -51,6 +55,7 @@ async function serveEntry(rest: string[]): Promise<number> {
51
55
  host: stringFlag(flags, "host"),
52
56
  sessionId: positional[0],
53
57
  });
58
+
54
59
  console.log(
55
60
  [
56
61
  "",
@@ -63,9 +68,11 @@ async function serveEntry(rest: string[]): Promise<number> {
63
68
  ].join("\n"),
64
69
  );
65
70
  const stop = () => void handle.close().finally(() => process.exit(0));
71
+
66
72
  process.on("SIGINT", stop);
67
73
  process.on("SIGTERM", stop);
68
74
  await new Promise(() => {}); // serve until signalled
75
+
69
76
  return 0;
70
77
  }
71
78
 
@@ -77,7 +84,9 @@ async function shareEntry(rest: string[]): Promise<number> {
77
84
  host: stringFlag(flags, "host"),
78
85
  port: port !== undefined ? Number(port) : undefined,
79
86
  };
87
+
80
88
  if (positional[0] === "pull") return sharePullCommand({ ...target, sessionId: positional[1] });
89
+
81
90
  return shareCommand({ ...target, sessionId: positional[0] });
82
91
  }
83
92
 
@@ -87,6 +96,7 @@ const commandHandlers: Record<string, CommandHandler> = {
87
96
  session: (rest) => sessionCommand(rest),
88
97
  daemon: () => daemonCommand(),
89
98
  plan: (rest) => planCommand(rest),
99
+ reply: (rest) => replyCommand(rest),
90
100
  diff: (rest) => diffCommand(rest),
91
101
  prototype: (rest) => prototypeCommand(rest),
92
102
  serve: (rest) => serveEntry(rest),
@@ -104,17 +114,21 @@ const helpAliases = new Set(["-h", "--help", "help"]);
104
114
  async function main(): Promise<number> {
105
115
  if (cmd === undefined) return runTui();
106
116
  const handler = commandHandlers[cmd];
117
+
107
118
  if (handler !== undefined) return handler(argv.slice(1));
108
119
  if (versionAliases.has(cmd)) {
109
120
  console.log(CLI_VERSION);
121
+
110
122
  return 0;
111
123
  }
112
124
  if (helpAliases.has(cmd)) {
113
125
  printHelp();
126
+
114
127
  return 0;
115
128
  }
116
129
  if (cmd.startsWith("ses_")) return runTui(cmd);
117
130
  printHelp();
131
+
118
132
  return 2;
119
133
  }
120
134
 
@@ -143,18 +157,22 @@ async function openReviewOfKind(
143
157
  ): Promise<number> {
144
158
  const client = await DaemonClient.connect({ autostart: true });
145
159
  let sessions: ReviewSession[];
160
+
146
161
  try {
147
162
  sessions = await client.sessionList();
148
163
  } finally {
149
164
  client.close();
150
165
  }
151
166
  const target = resolveOpenTarget(sessions, { match, selector });
167
+
152
168
  if (target.kind === "session") return runTui(target.sessionId);
153
169
  if (target.kind === "no-pending" && emptyMessage !== undefined) {
154
170
  console.error(emptyMessage);
171
+
155
172
  return 1;
156
173
  }
157
174
  console.error(openTargetMessage(label, target));
175
+
158
176
  return 1;
159
177
  }
160
178
 
@@ -163,6 +181,15 @@ async function planCommand(argv: string[]): Promise<number> {
163
181
  return openReviewOfKind(isPlanReview, "plan", openSelector(parseArgs(argv)));
164
182
  }
165
183
 
184
+ /**
185
+ * `cueloop reply [id|title]` - open the latest pending reply review, or address
186
+ * one. A reply artifact is the agent's previous message, submitted for review
187
+ * by the /cueloop:reply skill; the opener is scope-only, like plan.
188
+ */
189
+ async function replyCommand(argv: string[]): Promise<number> {
190
+ return openReviewOfKind(isReplyReview, "reply", openSelector(parseArgs(argv)));
191
+ }
192
+
166
193
  /**
167
194
  * `cueloop prototype <file.html>` creates a review of a rendered HTML file;
168
195
  * a selector or `--open`/`--latest` (or a bare call with no file) opens the
@@ -174,6 +201,7 @@ async function prototypeCommand(argv: string[]): Promise<number> {
174
201
  const wantsOpen = "open" in parsed.flags || "latest" in parsed.flags;
175
202
  const looksLikeFile =
176
203
  selector !== undefined && !isSessionId(selector) && selector.endsWith(".html");
204
+
177
205
  if (wantsOpen || !looksLikeFile)
178
206
  return openReviewOfKind(isPrototypeReview, "prototype", selector);
179
207
 
@@ -181,8 +209,10 @@ async function prototypeCommand(argv: string[]): Promise<number> {
181
209
  const html = await Bun.file(path)
182
210
  .text()
183
211
  .catch(() => undefined);
212
+
184
213
  if (html === undefined) {
185
214
  console.error(`prototype: cannot read ${path}`);
215
+
186
216
  return 1;
187
217
  }
188
218
  const client = await DaemonClient.connect({ autostart: true });
@@ -192,7 +222,9 @@ async function prototypeCommand(argv: string[]): Promise<number> {
192
222
  prototypePath: path,
193
223
  title: basename(path),
194
224
  });
225
+
195
226
  client.close();
227
+
196
228
  return runTui(review.id);
197
229
  }
198
230
 
@@ -208,10 +240,12 @@ async function diffCommand(argv: string[]): Promise<number> {
208
240
  const parsed = parseArgs(argv);
209
241
  const selector = openSelector(parsed);
210
242
  const wantsOpen = selector !== undefined || "open" in parsed.flags || "latest" in parsed.flags;
243
+
211
244
  if (wantsOpen) return openReviewOfKind(isDiffReview, "diff", selector);
212
245
 
213
246
  const workspace = await resolveWorkspace();
214
247
  const diff = await workingTreeDiff();
248
+
215
249
  if (!diff.patch.trim()) {
216
250
  return openReviewOfKind(
217
251
  isDiffReview,
@@ -228,7 +262,9 @@ async function diffCommand(argv: string[]): Promise<number> {
228
262
  workspace,
229
263
  title: `working tree @ ${workspace.branch}`,
230
264
  });
265
+
231
266
  client.close();
267
+
232
268
  return runTui(review.id);
233
269
  }
234
270
 
@@ -244,10 +280,13 @@ async function reviewEntry(argv: string[]): Promise<number> {
244
280
  const selector = openSelector(parsed);
245
281
  const looksLikeSessionId = selector !== undefined && isSessionId(selector);
246
282
  const wantsCreate = !explicitOpen && !looksLikeSessionId && selector !== undefined;
283
+
247
284
  if (wantsCreate) {
248
285
  const { reviewCommand } = await import("./pr");
286
+
249
287
  return reviewCommand(argv);
250
288
  }
289
+
251
290
  return openReviewOfKind(
252
291
  isPrReview,
253
292
  "PR",
@@ -257,6 +296,7 @@ async function reviewEntry(argv: string[]): Promise<number> {
257
296
 
258
297
  async function runTui(sessionId?: string): Promise<number> {
259
298
  const { runClient } = await import("@cueloop/client");
299
+
260
300
  return runClient({ sessionId });
261
301
  }
262
302
 
@@ -270,6 +310,7 @@ function printHelp(): void {
270
310
  "common commands:",
271
311
  " cueloop open the inbox",
272
312
  " cueloop plan [id|title] open the latest pending plan review (or one by id/title)",
313
+ " cueloop reply [id|title] open the latest pending reply review (the agent's previous message)",
273
314
  " cueloop diff [id|title] review your working tree (untracked files included);",
274
315
  " with a clean tree, open the latest pending diff review",
275
316
  " cueloop review <pr> review a pull request (--no-tui prints the session)",
@@ -7,6 +7,7 @@ import {
7
7
  isPlanReview,
8
8
  isPrototypeReview,
9
9
  isPrReview,
10
+ isReplyReview,
10
11
  isSessionId,
11
12
  openTargetMessage,
12
13
  resolveOpenTarget,
@@ -66,6 +67,13 @@ describe("verb scope predicates are disjoint", () => {
66
67
  expect(isPrototypeReview(session({ id: "ses_d", type: "diff" }))).toBe(false);
67
68
  });
68
69
 
70
+ test("isReplyReview matches only reply artifacts, and never a plan", () => {
71
+ expect(isReplyReview(session({ id: "ses_r", type: "reply" }))).toBe(true);
72
+ expect(isReplyReview(session({ id: "ses_p", type: "plan" }))).toBe(false);
73
+ expect(isReplyReview(session({ id: "ses_d", type: "diff" }))).toBe(false);
74
+ expect(isPlanReview(session({ id: "ses_r", type: "reply" }))).toBe(false);
75
+ });
76
+
69
77
  test("a PR review resolves under exactly one scope - never both diff and review", () => {
70
78
  // Arrange
71
79
  const prReview = session({ id: "ses_pr", type: "diff", pr: "42" });
@@ -28,6 +28,9 @@ import type { ReviewSession } from "@cueloop/schema";
28
28
  export function isPlanReview(session: ReviewSession): boolean {
29
29
  return session.artifact.type === "plan";
30
30
  }
31
+ export function isReplyReview(session: ReviewSession): boolean {
32
+ return session.artifact.type === "reply";
33
+ }
31
34
  export function isPrototypeReview(session: ReviewSession): boolean {
32
35
  return session.artifact.type === "prototype";
33
36
  }
@@ -68,6 +71,7 @@ export function resolveOpenTarget(sessions: ReviewSession[], query: OpenTargetQu
68
71
  const latestPending = scoped
69
72
  .filter((candidate) => candidate.status === "pending")
70
73
  .sort(newestFirst)[0];
74
+
71
75
  return latestPending
72
76
  ? { kind: "session", sessionId: latestPending.id }
73
77
  : { kind: "no-pending" };
@@ -75,6 +79,7 @@ export function resolveOpenTarget(sessions: ReviewSession[], query: OpenTargetQu
75
79
 
76
80
  const selector = query.selector;
77
81
  const byId = scoped.find((candidate) => candidate.id === selector);
82
+
78
83
  if (byId) return { kind: "session", sessionId: byId.id };
79
84
 
80
85
  const loweredSelector = selector.toLowerCase();
@@ -83,13 +88,16 @@ export function resolveOpenTarget(sessions: ReviewSession[], query: OpenTargetQu
83
88
  const exactTitle = titled
84
89
  .filter((candidate) => candidate.artifact.meta.title!.toLowerCase() === loweredSelector)
85
90
  .sort(newestFirst)[0];
91
+
86
92
  if (exactTitle) return { kind: "session", sessionId: exactTitle.id };
87
93
 
88
94
  const substringMatches = titled
89
95
  .filter((candidate) => candidate.artifact.meta.title!.toLowerCase().includes(loweredSelector))
90
96
  .sort(newestFirst);
97
+
91
98
  if (substringMatches.length === 1) return { kind: "session", sessionId: substringMatches[0]!.id };
92
99
  if (substringMatches.length === 0) return { kind: "no-match", selector };
100
+
93
101
  return {
94
102
  kind: "ambiguous",
95
103
  selector,
package/src/pr.ts CHANGED
@@ -29,6 +29,7 @@ async function gh(args: string[]): Promise<GhResult> {
29
29
  new Response(proc.stderr).text(),
30
30
  proc.exited,
31
31
  ]);
32
+
32
33
  return { code, stdout, stderr };
33
34
  }
34
35
 
@@ -42,17 +43,22 @@ const VERDICT_FLAG: Record<VerdictKind, string> = {
42
43
  export async function reviewCommand(argv: string[]): Promise<number> {
43
44
  const { positional, flags } = parseArgs(argv);
44
45
  const pr = positional[0];
46
+
45
47
  if (!pr) {
46
48
  console.error("usage: cueloop review <pr> [--no-tui]");
49
+
47
50
  return 2;
48
51
  }
49
52
  const diff = await gh(["pr", "diff", pr]);
53
+
50
54
  if (diff.code !== 0) {
51
55
  console.error(diff.stderr.trim() || `gh pr diff ${pr} failed (exit ${diff.code})`);
56
+
52
57
  return 1;
53
58
  }
54
59
  if (!diff.stdout.trim()) {
55
60
  console.error(`PR ${pr} has an empty diff - nothing to review`);
61
+
56
62
  return 1;
57
63
  }
58
64
  const client = await DaemonClient.connect({ autostart: true });
@@ -65,21 +71,27 @@ export async function reviewCommand(argv: string[]): Promise<number> {
65
71
  pr,
66
72
  });
67
73
  const session = review.session;
74
+
68
75
  client.close();
69
76
 
70
77
  if (flags["no-tui"]) {
71
78
  console.log(JSON.stringify(session, null, 2));
79
+
72
80
  return 0;
73
81
  }
74
82
 
75
83
  const { runClient } = await import("@cueloop/client");
84
+
76
85
  await runClient({ sessionId: session.id });
77
86
 
78
87
  const after = await getSession(session.id);
88
+
79
89
  if (after.status !== "resolved" || !after.verdict) {
80
90
  console.log(`session ${session.id} is unresolved - nothing was posted to PR ${pr}`);
91
+
81
92
  return 0;
82
93
  }
94
+
83
95
  return postVerdict(after, pr);
84
96
  }
85
97
 
@@ -87,20 +99,26 @@ export async function reviewPostCommand(argv: string[]): Promise<number> {
87
99
  const { positional } = parseArgs(argv);
88
100
  const sessionId = positional[0];
89
101
  const pr = positional[1];
102
+
90
103
  if (!sessionId || !pr) {
91
104
  console.error("usage: cueloop review-post <session-id> <pr>");
105
+
92
106
  return 2;
93
107
  }
94
108
  const session = await getSession(sessionId);
109
+
95
110
  if (session.status !== "resolved" || !session.verdict) {
96
111
  console.error(`session ${sessionId} is unresolved - nothing was posted to PR ${pr}`);
112
+
97
113
  return 1;
98
114
  }
115
+
99
116
  return postVerdict(session, pr);
100
117
  }
101
118
 
102
119
  async function getSession(id: string): Promise<ReviewSession> {
103
120
  const client = await DaemonClient.connect({ autostart: true });
121
+
104
122
  try {
105
123
  return await client.sessionGet(id);
106
124
  } finally {
@@ -119,10 +137,13 @@ async function postVerdict(session: ReviewSession, pr: string): Promise<number>
119
137
  "--body",
120
138
  verdict.feedback,
121
139
  ]);
140
+
122
141
  if (result.code !== 0) {
123
142
  console.error(result.stderr.trim() || `gh pr review ${pr} failed (exit ${result.code})`);
143
+
124
144
  return 1;
125
145
  }
126
146
  console.log(`posted ${verdict.kind} review to PR ${pr} (session ${session.id})`);
147
+
127
148
  return 0;
128
149
  }
@@ -32,6 +32,7 @@ function session(
32
32
  overrides: Partial<ReviewSession> & { type?: ReviewSession["artifact"]["type"] } = {},
33
33
  ): ReviewSession {
34
34
  const { type, ...rest } = overrides;
35
+
35
36
  return {
36
37
  schemaVersion: "1",
37
38
  id,
@@ -87,6 +88,7 @@ describe("refineCommand", () => {
87
88
  // Arrange
88
89
  const home = tempHome();
89
90
  const store = new SessionStore(home);
91
+
90
92
  store.upsert(session("ses_a", { verdict: verdict("approve"), status: "resolved" }));
91
93
  store.upsert(
92
94
  session("ses_b", {
@@ -119,6 +121,7 @@ describe("refineCommand", () => {
119
121
  // Arrange
120
122
  const home = tempHome();
121
123
  const store = new SessionStore(home);
124
+
122
125
  store.upsert(
123
126
  session("ses_x", {
124
127
  verdict: verdict("approve"),
@@ -148,6 +151,7 @@ describe("refineCommand", () => {
148
151
 
149
152
  // Assert
150
153
  const secondReport = readFileSync(reportPath, "utf8");
154
+
151
155
  expect(secondReport).toContain("1 sessions analyzed (1 total).");
152
156
  expect(secondReport).toContain("two");
153
157
  });
@@ -28,6 +28,7 @@ export async function refineCommand(argv: string[]): Promise<number> {
28
28
  const nowMs = Date.now();
29
29
 
30
30
  const store = new SessionStore(home);
31
+
31
32
  store.recover();
32
33
  const all = store.list();
33
34
 
@@ -40,10 +41,12 @@ export async function refineCommand(argv: string[]): Promise<number> {
40
41
  const markdown = buildRefineReport(analyzed, all.length, new Date(nowMs).toISOString());
41
42
 
42
43
  const directory = reportsDir(home);
44
+
43
45
  mkdirSync(directory, { recursive: true });
44
46
  pruneExpiredReports(directory, resolveCleanupPeriodDays(), nowMs);
45
47
  const latestPath = join(directory, LATEST_REPORT_FILENAME);
46
48
  const timestampedPath = join(directory, timestampedReportFilename(nowMs));
49
+
47
50
  writeFileSync(latestPath, markdown);
48
51
  writeFileSync(timestampedPath, markdown);
49
52
 
@@ -64,6 +67,7 @@ export async function refineCommand(argv: string[]): Promise<number> {
64
67
  2,
65
68
  ),
66
69
  );
70
+
67
71
  return 0;
68
72
  }
69
73
 
@@ -104,6 +108,7 @@ export function buildRefineReport(
104
108
  verdictLabel(session),
105
109
  isoWeek(annotation.createdAt),
106
110
  ].join(" · ");
111
+
107
112
  lines.push(
108
113
  `- "${truncate(annotation.anchor.quote, QUOTE_LIMIT)}": ${truncate(annotation.body, BODY_LIMIT)} · ${meta}`,
109
114
  );
@@ -140,21 +145,26 @@ function hasReviewSignal(session: ReviewSession): boolean {
140
145
 
141
146
  function flattenReviewAnnotations(sessions: ReviewSession[]): AnnotatedEntry[] {
142
147
  const entries: AnnotatedEntry[] = [];
148
+
143
149
  for (const session of sessions) {
144
150
  for (const annotation of session.annotations) {
145
151
  if (!isAgentNote(annotation)) entries.push({ annotation, session });
146
152
  }
147
153
  }
154
+
148
155
  return entries;
149
156
  }
150
157
 
151
158
  function groupByKind(entries: AnnotatedEntry[]): [string, AnnotatedEntry[]][] {
152
159
  const groups = new Map<string, AnnotatedEntry[]>();
160
+
153
161
  for (const entry of entries) {
154
162
  const group = groups.get(entry.annotation.kind) ?? [];
163
+
155
164
  group.push(entry);
156
165
  groups.set(entry.annotation.kind, group);
157
166
  }
167
+
158
168
  return [...groups.entries()].toSorted(
159
169
  (left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0]),
160
170
  );
@@ -174,15 +184,19 @@ function byKey<T>(items: T[], keyOf: (item: T) => string): [string, number][] {
174
184
 
175
185
  function tally<T>(items: T[], keyOf: (item: T) => string): Map<string, number> {
176
186
  const counts = new Map<string, number>();
187
+
177
188
  for (const item of items) {
178
189
  const key = keyOf(item);
190
+
179
191
  counts.set(key, (counts.get(key) ?? 0) + 1);
180
192
  }
193
+
181
194
  return counts;
182
195
  }
183
196
 
184
197
  function primitiveLabel(session: ReviewSession): string {
185
198
  if (session.artifact.type === "diff" && session.artifact.meta.pr) return "pull request";
199
+
186
200
  return session.artifact.type;
187
201
  }
188
202
 
@@ -201,22 +215,27 @@ function verdictLabel(session: ReviewSession): string {
201
215
 
202
216
  function isoWeek(iso: string): string {
203
217
  const date = new Date(iso);
218
+
204
219
  if (Number.isNaN(date.getTime())) return "undated";
205
220
  const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
206
221
  const weekday = utc.getUTCDay() || 7;
222
+
207
223
  utc.setUTCDate(utc.getUTCDate() + 4 - weekday);
208
224
  const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
209
225
  const week = Math.ceil(((utc.getTime() - yearStart.getTime()) / (24 * 60 * 60 * 1000) + 1) / 7);
226
+
210
227
  return `${utc.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
211
228
  }
212
229
 
213
230
  function truncate(text: string, max: number): string {
214
231
  const collapsed = text.replace(/\s+/g, " ").trim();
232
+
215
233
  return collapsed.length > max ? `${collapsed.slice(0, max - 1)}…` : collapsed;
216
234
  }
217
235
 
218
236
  function parseLimit(raw: string | undefined): number {
219
237
  const value = Number(raw);
238
+
220
239
  return Number.isInteger(value) && value > 0 ? value : DEFAULT_SESSION_LIMIT;
221
240
  }
222
241
 
@@ -238,5 +257,6 @@ function readState(home: string): Map<string, string> {
238
257
 
239
258
  function writeState(home: string, analyzed: Map<string, string>): void {
240
259
  const state = { analyzed: Object.fromEntries(analyzed) };
260
+
241
261
  writeFileSync(statePath(home), JSON.stringify(state, null, 2));
242
262
  }
@@ -42,20 +42,25 @@ async function sessionCreate({ client, flags }: SessionContext): Promise<number>
42
42
  title: stringFlag(flags, "title"),
43
43
  notes,
44
44
  });
45
+
45
46
  // herdr auto-open: render the review in a tab (no-op outside herdr).
46
47
  await openHerdrPaneForReview(review.session, client);
47
48
  out(review.session);
49
+
48
50
  return 0;
49
51
  }
50
52
 
51
53
  async function sessionGetCommand({ client, positional }: SessionContext): Promise<number> {
52
54
  out(await client.sessionGet(required(positional[1], "session id")));
55
+
53
56
  return 0;
54
57
  }
55
58
 
56
59
  async function sessionListCommand({ client, flags }: SessionContext): Promise<number> {
57
60
  const status = stringFlag(flags, "status") as "pending" | "resolved" | undefined;
61
+
58
62
  out(await client.sessionList(status ? { status } : undefined));
63
+
59
64
  return 0;
60
65
  }
61
66
 
@@ -63,12 +68,16 @@ async function sessionWaitCommand({ client, positional, flags }: SessionContext)
63
68
  const id = required(positional[1], "session id");
64
69
  const timeoutMs = Number(stringFlag(flags, "timeout-ms") ?? "60000");
65
70
  const session = await client.sessionWait(id, timeoutMs);
71
+
66
72
  if (session === null) {
67
73
  out({ status: "pending" });
74
+
68
75
  return 0;
69
76
  }
70
77
  const { allow, feedback } = verdictResponse(session);
78
+
71
79
  out({ status: "resolved", allow, verdict: session.verdict!.kind, feedback });
80
+
72
81
  return 0;
73
82
  }
74
83
 
@@ -81,6 +90,7 @@ async function sessionAnnotateCommand({
81
90
  const quote = required(stringFlag(flags, "quote"), "--quote");
82
91
  const author = stringFlag(flags, "author");
83
92
  const body = await annotateBody(flags, client, id);
93
+
84
94
  out(
85
95
  await client.sessionAnnotate(
86
96
  id,
@@ -98,6 +108,7 @@ async function sessionAnnotateCommand({
98
108
  stringFlag(flags, "author-name"),
99
109
  ),
100
110
  );
111
+
101
112
  return 0;
102
113
  }
103
114
 
@@ -108,7 +119,9 @@ async function sessionResolveCommand({
108
119
  }: SessionContext): Promise<number> {
109
120
  const id = required(positional[1], "session id");
110
121
  const kind = required(stringFlag(flags, "verdict"), "--verdict") as VerdictKind;
122
+
111
123
  out(await client.sessionResolve(id, kind, stringFlag(flags, "summary") ?? ""));
124
+
112
125
  return 0;
113
126
  }
114
127
 
@@ -125,7 +138,9 @@ async function sessionSubmitRevisionCommand({
125
138
  .split(",")
126
139
  .map((annotationId) => annotationId.trim())
127
140
  .filter(Boolean);
141
+
128
142
  out(await client.sessionSubmitRevision(id, content, addressed));
143
+
129
144
  return 0;
130
145
  }
131
146
 
@@ -146,14 +161,18 @@ export async function sessionCommand(argv: string[]): Promise<number> {
146
161
  // review-side agent passes it so the daemon rejects any escalation attempt.
147
162
  const role = stringFlag(flags, "role") === "agent" ? "agent" : undefined;
148
163
  const client = await DaemonClient.connect({ autostart: true, role });
164
+
149
165
  try {
150
166
  const handler = verb !== undefined ? sessionVerbHandlers[verb] : undefined;
167
+
151
168
  if (handler === undefined) {
152
169
  console.error(
153
170
  "usage: cueloop session <create|get|list|wait|annotate|resolve|submit-revision> [flags]",
154
171
  );
172
+
155
173
  return 2;
156
174
  }
175
+
157
176
  return await handler({ client, positional, flags });
158
177
  } finally {
159
178
  client.close();
@@ -162,6 +181,7 @@ export async function sessionCommand(argv: string[]): Promise<number> {
162
181
 
163
182
  function required<T>(value: T | undefined, description: string): T {
164
183
  if (value === undefined) throw new Error(`missing ${description}`);
184
+
165
185
  return value;
166
186
  }
167
187
 
@@ -177,12 +197,16 @@ async function annotateBody(
177
197
  id: string,
178
198
  ): Promise<string> {
179
199
  const explicit = stringFlag(flags, "body");
200
+
180
201
  if (explicit !== undefined) return explicit;
181
202
  const actionRef = stringFlag(flags, "action");
203
+
182
204
  if (actionRef === undefined) return "";
183
205
  const session = await client.sessionGet(id);
184
206
  const actions = loadConfig({ repoRoot: session.workspace.repoRoot }).actions;
185
207
  const action = resolveQuickAction(actions, actionRef);
208
+
186
209
  if (!action) throw new Error(`no quick action ${actionRef} - see: cueloop actions list`);
210
+
187
211
  return quickActionBody(action);
188
212
  }
@@ -26,6 +26,7 @@ function annotationFixture(id: string, author?: string): Annotation {
26
26
  body: "note",
27
27
  createdAt: "2026-01-01T00:00:00.000Z",
28
28
  };
29
+
29
30
  return author ? { ...base, author } : base;
30
31
  }
31
32
 
@@ -36,14 +37,18 @@ function fakeClient(sessions: ReviewSession[]): SessionClient {
36
37
  sessionList: async () => sessions,
37
38
  sessionSetShareId: mock(async (id: string, shareId: string) => {
38
39
  const session = sessions.find((candidate) => candidate.id === id)!;
40
+
39
41
  session.shareId = shareId;
42
+
40
43
  return session;
41
44
  }),
42
45
  sessionMergeShared: mock(async (id: string, incoming: { annotations: Annotation[] }) => {
43
46
  const session = sessions.find((candidate) => candidate.id === id)!;
44
47
  const known = new Set(session.annotations.map((annotation) => annotation.id));
48
+
45
49
  for (const annotation of incoming.annotations)
46
50
  if (!known.has(annotation.id)) session.annotations.push(annotation);
51
+
47
52
  return session;
48
53
  }),
49
54
  } as unknown as SessionClient;
@@ -51,6 +56,7 @@ function fakeClient(sessions: ReviewSession[]): SessionClient {
51
56
 
52
57
  function depsSpy(overrides: Partial<ShareDeps> = {}): ShareDeps & { lines: string[] } {
53
58
  const lines: string[] = [];
59
+
54
60
  return {
55
61
  publish: mock(async () => ({ line: "ssh p_abc123xy@cueloop.dev", copied: true })),
56
62
  out: (message: string) => void lines.push(message),
@@ -141,6 +147,7 @@ describe(shareSession, () => {
141
147
 
142
148
  function pullDepsSpy(remote: ReviewSession): PullDeps & { lines: string[] } {
143
149
  const lines: string[] = [];
150
+
144
151
  return {
145
152
  pull: mock(async () => remote),
146
153
  out: (message: string) => void lines.push(message),
@@ -43,6 +43,7 @@ export async function shareCommand(
43
43
  deps: ShareDeps = defaultDeps,
44
44
  ): Promise<number> {
45
45
  const client = await DaemonClient.connect({ home: params.home, autostart: true });
46
+
46
47
  try {
47
48
  return await shareSession(client, params, deps);
48
49
  } finally {
@@ -56,6 +57,7 @@ export async function sharePullCommand(
56
57
  deps: PullDeps = defaultPullDeps,
57
58
  ): Promise<number> {
58
59
  const client = await DaemonClient.connect({ home: params.home, autostart: true });
60
+
59
61
  try {
60
62
  return await pullSession(client, params, deps);
61
63
  } finally {
@@ -70,14 +72,18 @@ export async function shareSession(
70
72
  deps: ShareDeps,
71
73
  ): Promise<number> {
72
74
  const session = await pickSession(client, params.sessionId);
75
+
73
76
  if (!session) {
74
77
  deps.out("no plan to share - open a review first");
78
+
75
79
  return 1;
76
80
  }
77
81
  const { line, copied } = await deps.publish(session, { host: params.host, port: params.port });
78
82
  const shareId = shareIdFromLine(line);
83
+
79
84
  if (shareId) await client.sessionSetShareId(session.id, shareId);
80
85
  deps.out(copied ? `share link copied - ${line}` : line);
86
+
81
87
  return 0;
82
88
  }
83
89
 
@@ -92,8 +98,10 @@ export async function pullSession(
92
98
  deps: PullDeps,
93
99
  ): Promise<number> {
94
100
  const session = await pickSharedSession(client, params.sessionId);
101
+
95
102
  if (!session) {
96
103
  deps.out("no shared plan to pull - run `cueloop share` first");
104
+
97
105
  return 1;
98
106
  }
99
107
  const remote = await deps.pull(session.shareId!, { host: params.host, port: params.port });
@@ -103,9 +111,11 @@ export async function pullSession(
103
111
  participants: remote.participants,
104
112
  });
105
113
  const added = merged.annotations.length - before;
114
+
106
115
  deps.out(
107
116
  added > 0 ? `pulled ${added} new annotation${added === 1 ? "" : "s"}` : "no new annotations",
108
117
  );
118
+
109
119
  return 0;
110
120
  }
111
121
 
@@ -116,6 +126,7 @@ async function pickSession(
116
126
  ): Promise<ReviewSession | null> {
117
127
  if (sessionId) return client.sessionGet(sessionId);
118
128
  const sessions = await client.sessionList();
129
+
119
130
  return sessions.at(-1) ?? null;
120
131
  }
121
132
 
@@ -126,8 +137,10 @@ async function pickSharedSession(
126
137
  ): Promise<ReviewSession | null> {
127
138
  if (sessionId) {
128
139
  const session = await client.sessionGet(sessionId);
140
+
129
141
  return session.shareId ? session : null;
130
142
  }
131
143
  const sessions = await client.sessionList();
144
+
132
145
  return sessions.filter((session) => session.shareId).at(-1) ?? null;
133
146
  }
@@ -16,22 +16,29 @@ import { parseArgs, stringFlag } from "./args";
16
16
  export async function wakeCommand(argv: string[]): Promise<number> {
17
17
  const { positional, flags } = parseArgs(argv);
18
18
  const sessionId = positional[0];
19
+
19
20
  if (sessionId === undefined) {
20
21
  console.error(
21
22
  "usage: cueloop wake <session-id> [--harness claude-code|codex] [--thread <codex-thread-id>]",
22
23
  );
24
+
23
25
  return 2;
24
26
  }
25
27
  const harness = stringFlag(flags, "harness") ?? "claude-code";
28
+
26
29
  if (harness === "codex") {
27
30
  const threadId = stringFlag(flags, "thread");
31
+
28
32
  if (threadId === undefined) {
29
33
  console.error("cueloop wake --harness codex needs --thread <codex-thread-id>");
34
+
30
35
  return 2;
31
36
  }
32
37
  const { runCodexWake } = await import("@cueloop/adapters/codex/wake");
38
+
33
39
  return (await runCodexWake(sessionId, threadId)) ? 0 : 1;
34
40
  }
35
41
  const { runInboxWake } = await import("@cueloop/adapters/claude-code/wake");
42
+
36
43
  return (await runInboxWake(sessionId)) ? 0 : 1;
37
44
  }