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 +5 -5
- package/src/actions-command.ts +5 -0
- package/src/args.ts +6 -0
- package/src/main.ts +43 -2
- package/src/open-target.test.ts +8 -0
- package/src/open-target.ts +8 -0
- package/src/pr.ts +21 -0
- package/src/refine-command.test.ts +4 -0
- package/src/refine-command.ts +20 -0
- package/src/session-commands.ts +24 -0
- package/src/share-command.test.ts +7 -0
- package/src/share-command.ts +13 -0
- package/src/wake-command.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cueloop",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
34
|
-
"@cueloop/client": "0.1.0-alpha.
|
|
35
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
36
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
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"
|
package/src/actions-command.ts
CHANGED
|
@@ -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
|
|
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)",
|
package/src/open-target.test.ts
CHANGED
|
@@ -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" });
|
package/src/open-target.ts
CHANGED
|
@@ -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
|
});
|
package/src/refine-command.ts
CHANGED
|
@@ -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
|
}
|
package/src/session-commands.ts
CHANGED
|
@@ -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),
|
package/src/share-command.ts
CHANGED
|
@@ -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
|
}
|
package/src/wake-command.ts
CHANGED
|
@@ -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
|
}
|