getgloss 0.13.2 → 0.14.0
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/README.md +16 -7
- package/dist/cli/index.js +380 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/server/daemon.js +462 -78
- package/dist/server/daemon.js.map +1 -1
- package/dist/web/assets/index-D7vKIB2t.js +307 -0
- package/dist/web/assets/index-DuRtp1zX.css +1 -0
- package/dist/web/assets/{syntax-DrIezw90.js → syntax-CD0nrxxU.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/prompt.md +9 -2
- package/dist/web/setup/index.html +7 -4
- package/dist/web/setup.md +15 -5
- package/package.json +1 -1
- package/skill/SKILL.md +7 -4
- package/dist/web/assets/index-CIEjEgi6.js +0 -307
- package/dist/web/assets/index-Du5EuwDc.css +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5
5
|
import { constants } from "fs";
|
|
6
|
-
import { access, rm as rm5, writeFile as
|
|
6
|
+
import { access, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
7
7
|
import path7 from "path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import openBrowser from "open";
|
|
@@ -30,7 +30,7 @@ import path from "path";
|
|
|
30
30
|
// package.json
|
|
31
31
|
var package_default = {
|
|
32
32
|
name: "getgloss",
|
|
33
|
-
version: "0.
|
|
33
|
+
version: "0.14.0",
|
|
34
34
|
description: "Local browser-based diff review for coding-agent loops.",
|
|
35
35
|
type: "module",
|
|
36
36
|
packageManager: "pnpm@10.33.2",
|
|
@@ -112,6 +112,7 @@ var package_default = {
|
|
|
112
112
|
|
|
113
113
|
// src/shared/paths.ts
|
|
114
114
|
var packageVersion = package_default.version;
|
|
115
|
+
var protocolVersion = 1;
|
|
115
116
|
function expandHome(input) {
|
|
116
117
|
if (input === "~") {
|
|
117
118
|
return homedir();
|
|
@@ -127,6 +128,9 @@ function globalStateDir() {
|
|
|
127
128
|
function globalServerFile() {
|
|
128
129
|
return path.join(globalStateDir(), "server.json");
|
|
129
130
|
}
|
|
131
|
+
function globalLastPortFile() {
|
|
132
|
+
return path.join(globalStateDir(), "last-port");
|
|
133
|
+
}
|
|
130
134
|
function globalServerLockDir() {
|
|
131
135
|
return path.join(globalStateDir(), "server.lock");
|
|
132
136
|
}
|
|
@@ -178,6 +182,9 @@ function globalReviewMarkdownFile(reviewId) {
|
|
|
178
182
|
function globalReviewResolvedFile(reviewId) {
|
|
179
183
|
return path.join(globalReviewDir(reviewId), "resolved.json");
|
|
180
184
|
}
|
|
185
|
+
function globalReviewEventsFile(reviewId) {
|
|
186
|
+
return path.join(globalReviewDir(reviewId), "events.jsonl");
|
|
187
|
+
}
|
|
181
188
|
async function ensureDir(dir) {
|
|
182
189
|
await mkdir(dir, { recursive: true });
|
|
183
190
|
}
|
|
@@ -190,6 +197,8 @@ var DIFF_SCOPE_MODES = ["working", "branch", "explicit"];
|
|
|
190
197
|
var DIFF_FALLBACK_REASONS = ["working-tree-clean", "missing-branch-base"];
|
|
191
198
|
var REVIEW_SCOPE_MODES = ["all", "single", "range"];
|
|
192
199
|
var RESOLUTION_STATUSES = ["partial", "resolved"];
|
|
200
|
+
var REVIEW_EVENT_ACTORS = ["human", "agent", "system"];
|
|
201
|
+
var AGENT_STATUSES = ["claimed", "working", "blocked", "completed", "failed"];
|
|
193
202
|
var REVIEW_UPDATE_REASONS = [
|
|
194
203
|
"review-resolved",
|
|
195
204
|
"comment-resolved",
|
|
@@ -209,10 +218,10 @@ function parseJsonValue(value, guard, label) {
|
|
|
209
218
|
return value;
|
|
210
219
|
}
|
|
211
220
|
function isServerInfo(value) {
|
|
212
|
-
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
221
|
+
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isOptionalNumber(value.protocolVersion) && isString(value.startedAt) && isString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
213
222
|
}
|
|
214
223
|
function isHealthResponse(value) {
|
|
215
|
-
return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.activeReviews) && isOptionalNumber(value.connections) && isOptionalString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
224
|
+
return isRecord(value) && isBoolean(value.ok) && isString(value.version) && isNumber(value.protocolVersion) && isNumber(value.activeReviews) && isOptionalNumber(value.connections) && isOptionalString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
216
225
|
}
|
|
217
226
|
function isClearReviewsResult(value) {
|
|
218
227
|
return isRecord(value) && isString(value.reviewsDir) && isString(value.cutoff) && isNumber(value.olderThanDays) && isBoolean(value.dryRun) && isArrayOf(value.candidates, isClearReviewEntry) && isArrayOf(value.deleted, isClearReviewEntry) && isArrayOf(value.skipped, isClearReviewSkipped) && isRecord(value.counts) && isNumber(value.counts.candidates) && isNumber(value.counts.deleted) && isNumber(value.counts.skipped);
|
|
@@ -232,8 +241,14 @@ function isOpenResult(value) {
|
|
|
232
241
|
function isResolveResult(value) {
|
|
233
242
|
return isRecord(value) && value.ok === true && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.comments) && isString(value.path) && isResolutionBundle(value.resolution);
|
|
234
243
|
}
|
|
244
|
+
function isAgentClaimResponse(value) {
|
|
245
|
+
return isRecord(value) && value.ok === true && isString(value.reviewId) && isString(value.turnId) && isNumber(value.turnIndex) && value.status === "claimed" && isString(value.feedbackPath) && isString(value.markdownPath) && isString(value.artifactDir) && isFeedbackBundle(value.feedback) && isOptional(value.resolution, isResolutionBundle) && isReviewEvent(value.event);
|
|
246
|
+
}
|
|
247
|
+
function isAgentNoteResponse(value) {
|
|
248
|
+
return isRecord(value) && value.ok === true && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isOptional(value.status, isAgentStatus) && isString(value.message) && isReviewEvent(value.event);
|
|
249
|
+
}
|
|
235
250
|
function isReviewRecord(value) {
|
|
236
|
-
return isRecord(value) && isReviewMeta(value.meta) && isArrayOf(value.turns, isReviewTurn) && isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle);
|
|
251
|
+
return isRecord(value) && isReviewMeta(value.meta) && isArrayOf(value.turns, isReviewTurn) && isDiffPayload(value.diff) && isOptional(value.feedback, isFeedbackBundle) && isOptional(value.resolution, isResolutionBundle) && isOptional(value.events, (events) => isArrayOf(events, isReviewEvent));
|
|
237
252
|
}
|
|
238
253
|
function isStoredReviewMeta(value) {
|
|
239
254
|
return isRecord(value) && isString(value.id) && isString(value.cwd) && isBaseRef(value.base) && isNullableString(value.branch) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isOptionalString(value.artifactDir) && isOptionalString(value.activeTurnId) && isOptional(
|
|
@@ -263,6 +278,9 @@ function isReviewEvent(value) {
|
|
|
263
278
|
if (!isRecord(value) || !isString(value.reviewId) || !isString(value.type)) {
|
|
264
279
|
return false;
|
|
265
280
|
}
|
|
281
|
+
if (!hasValidReviewEventEnvelope(value)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
266
284
|
switch (value.type) {
|
|
267
285
|
case "review.opened":
|
|
268
286
|
case "review.cancelled":
|
|
@@ -273,6 +291,10 @@ function isReviewEvent(value) {
|
|
|
273
291
|
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isRecord(value.counts) && isNumber(value.counts.files) && isNumber(value.counts.comments);
|
|
274
292
|
case "review.updated":
|
|
275
293
|
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isReviewUpdateReason(value.reason) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.counts);
|
|
294
|
+
case "agent.claimed":
|
|
295
|
+
return isString(value.turnId) && isNumber(value.turnIndex) && value.status === "claimed" && isOptionalString(value.message);
|
|
296
|
+
case "agent.note":
|
|
297
|
+
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isOptional(value.status, isAgentStatus) && isString(value.message);
|
|
276
298
|
default:
|
|
277
299
|
return false;
|
|
278
300
|
}
|
|
@@ -362,6 +384,15 @@ function isResolutionStatus(value) {
|
|
|
362
384
|
function isReviewUpdateReason(value) {
|
|
363
385
|
return isOneOf(value, REVIEW_UPDATE_REASONS);
|
|
364
386
|
}
|
|
387
|
+
function isReviewEventActor(value) {
|
|
388
|
+
return isOneOf(value, REVIEW_EVENT_ACTORS);
|
|
389
|
+
}
|
|
390
|
+
function isAgentStatus(value) {
|
|
391
|
+
return isOneOf(value, AGENT_STATUSES);
|
|
392
|
+
}
|
|
393
|
+
function hasValidReviewEventEnvelope(value) {
|
|
394
|
+
return isOptionalString(value.id) && isOptionalNumber(value.seq) && isOptionalString(value.createdAt) && isOptional(value.actor, isReviewEventActor);
|
|
395
|
+
}
|
|
365
396
|
function isRecord(value) {
|
|
366
397
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
367
398
|
}
|
|
@@ -1182,6 +1213,24 @@ var ServerClient = class {
|
|
|
1182
1213
|
"resolve response"
|
|
1183
1214
|
);
|
|
1184
1215
|
}
|
|
1216
|
+
async claimReview(reviewId, message, turn) {
|
|
1217
|
+
const request = { message, turn };
|
|
1218
|
+
return this.post(
|
|
1219
|
+
`/api/reviews/${reviewId}/agent/claim`,
|
|
1220
|
+
request,
|
|
1221
|
+
isAgentClaimResponse,
|
|
1222
|
+
"agent claim response"
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
async addAgentNote(reviewId, message, status, turn) {
|
|
1226
|
+
const request = { message, status, turn };
|
|
1227
|
+
return this.post(
|
|
1228
|
+
`/api/reviews/${reviewId}/agent/notes`,
|
|
1229
|
+
request,
|
|
1230
|
+
isAgentNoteResponse,
|
|
1231
|
+
"agent note response"
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1185
1234
|
async resolveComment(reviewId, commentId, summary) {
|
|
1186
1235
|
const request = { summary };
|
|
1187
1236
|
return this.post(
|
|
@@ -1207,7 +1256,8 @@ var ServerClient = class {
|
|
|
1207
1256
|
"submit review response"
|
|
1208
1257
|
);
|
|
1209
1258
|
}
|
|
1210
|
-
async watchReview(reviewId,
|
|
1259
|
+
async watchReview(reviewId, options = {}) {
|
|
1260
|
+
const { timeoutSeconds, turnId } = options;
|
|
1211
1261
|
const deadline = timeoutSeconds && timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1e3 : null;
|
|
1212
1262
|
while (true) {
|
|
1213
1263
|
const remainingMs = deadline ? deadline - Date.now() : null;
|
|
@@ -1218,7 +1268,7 @@ var ServerClient = class {
|
|
|
1218
1268
|
const timeoutMs = remainingMs === null ? null : Math.min(remainingMs, timedWatchAttemptMs);
|
|
1219
1269
|
const timeout = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
1220
1270
|
try {
|
|
1221
|
-
return await this.readReviewEvents(reviewId, controller.signal);
|
|
1271
|
+
return await this.readReviewEvents(reviewId, controller.signal, turnId);
|
|
1222
1272
|
} catch (error) {
|
|
1223
1273
|
if (isAbortError(error)) {
|
|
1224
1274
|
if (deadline && Date.now() < deadline) {
|
|
@@ -1237,7 +1287,7 @@ var ServerClient = class {
|
|
|
1237
1287
|
}
|
|
1238
1288
|
}
|
|
1239
1289
|
}
|
|
1240
|
-
async readReviewEvents(reviewId, signal) {
|
|
1290
|
+
async readReviewEvents(reviewId, signal, turnId) {
|
|
1241
1291
|
const response = await fetch(`${this.baseUrl}/api/reviews/${reviewId}/events`, {
|
|
1242
1292
|
signal
|
|
1243
1293
|
});
|
|
@@ -1261,7 +1311,11 @@ var ServerClient = class {
|
|
|
1261
1311
|
continue;
|
|
1262
1312
|
}
|
|
1263
1313
|
const event = parseJson(dataLine.slice(5).trim(), isReviewEvent, "review event");
|
|
1264
|
-
if (event.type === "review.
|
|
1314
|
+
if (event.type === "review.cancelled") {
|
|
1315
|
+
await reader.cancel().catch(() => void 0);
|
|
1316
|
+
return event;
|
|
1317
|
+
}
|
|
1318
|
+
if (event.type === "review.submitted" && (!turnId || event.turnId === turnId)) {
|
|
1265
1319
|
await reader.cancel().catch(() => void 0);
|
|
1266
1320
|
return event;
|
|
1267
1321
|
}
|
|
@@ -1317,7 +1371,13 @@ async function isServerResponsive(info) {
|
|
|
1317
1371
|
}
|
|
1318
1372
|
try {
|
|
1319
1373
|
const health = await new ServerClient(serverUrl(info)).health();
|
|
1320
|
-
|
|
1374
|
+
if (health.ok !== true || health.version !== packageVersion || health.protocolVersion !== protocolVersion || health.stateDir !== info.stateDir) {
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
if (info.daemonPath && health.daemonPath !== info.daemonPath) {
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
return true;
|
|
1321
1381
|
} catch {
|
|
1322
1382
|
return false;
|
|
1323
1383
|
}
|
|
@@ -1346,14 +1406,17 @@ async function startServerUnlocked(existing, options = {}) {
|
|
|
1346
1406
|
if (existing) {
|
|
1347
1407
|
await retireServer(existing);
|
|
1348
1408
|
}
|
|
1349
|
-
const
|
|
1409
|
+
const rememberedPort = options.port ?? existing?.port ?? await readLastServerPort();
|
|
1410
|
+
const preferredPort = rememberedPort ?? await getPort();
|
|
1350
1411
|
try {
|
|
1351
1412
|
return await launchServer(preferredPort);
|
|
1352
1413
|
} catch (error) {
|
|
1353
|
-
if (options.port
|
|
1414
|
+
if (options.port) {
|
|
1354
1415
|
throw error;
|
|
1355
1416
|
}
|
|
1356
|
-
|
|
1417
|
+
if (existing) {
|
|
1418
|
+
await removeServerInfoForPid(existing.pid);
|
|
1419
|
+
}
|
|
1357
1420
|
return launchServer(await getPort());
|
|
1358
1421
|
}
|
|
1359
1422
|
}
|
|
@@ -1380,6 +1443,7 @@ async function launchServer(port) {
|
|
|
1380
1443
|
pid: child.pid ?? -1,
|
|
1381
1444
|
port,
|
|
1382
1445
|
version: packageVersion,
|
|
1446
|
+
protocolVersion,
|
|
1383
1447
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1384
1448
|
stateDir: globalStateDir(),
|
|
1385
1449
|
cwd: process.cwd(),
|
|
@@ -1394,6 +1458,7 @@ async function launchServer(port) {
|
|
|
1394
1458
|
const deadline = Date.now() + 8e3;
|
|
1395
1459
|
while (Date.now() < deadline) {
|
|
1396
1460
|
if (await isServerResponsive(info)) {
|
|
1461
|
+
await writeLastServerPort(port).catch(() => void 0);
|
|
1397
1462
|
return info;
|
|
1398
1463
|
}
|
|
1399
1464
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
@@ -1402,6 +1467,21 @@ async function launchServer(port) {
|
|
|
1402
1467
|
await removeServerInfoForPid(info.pid);
|
|
1403
1468
|
throw new Error(`Server did not become responsive. See ${globalServerLogFile()}`);
|
|
1404
1469
|
}
|
|
1470
|
+
async function readLastServerPort() {
|
|
1471
|
+
let raw;
|
|
1472
|
+
try {
|
|
1473
|
+
raw = await readFile3(globalLastPortFile(), "utf8");
|
|
1474
|
+
} catch {
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
const port = Number(raw.trim());
|
|
1478
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
|
|
1479
|
+
}
|
|
1480
|
+
async function writeLastServerPort(port) {
|
|
1481
|
+
await ensureDir(globalStateDir());
|
|
1482
|
+
await writeFile3(globalLastPortFile(), `${port}
|
|
1483
|
+
`);
|
|
1484
|
+
}
|
|
1405
1485
|
async function withServerLock(fn) {
|
|
1406
1486
|
const release = await acquireServerLock();
|
|
1407
1487
|
try {
|
|
@@ -1674,7 +1754,7 @@ async function sleep2(milliseconds) {
|
|
|
1674
1754
|
|
|
1675
1755
|
// src/server/store.ts
|
|
1676
1756
|
import { createHash } from "crypto";
|
|
1677
|
-
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
1757
|
+
import { appendFile, readdir as readdir2, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1678
1758
|
import path6 from "path";
|
|
1679
1759
|
import { ulid } from "ulid";
|
|
1680
1760
|
|
|
@@ -1870,8 +1950,11 @@ var ReviewStore = class {
|
|
|
1870
1950
|
const record = normalizeRecord({ meta, turns: [turn], diff: turn.diff });
|
|
1871
1951
|
this.reviews.set(id, record);
|
|
1872
1952
|
await this.persistInitial(record, turn);
|
|
1873
|
-
this.
|
|
1874
|
-
|
|
1953
|
+
const event = await this.appendReviewEvent(id, "system", {
|
|
1954
|
+
type: "review.opened",
|
|
1955
|
+
reviewId: id
|
|
1956
|
+
});
|
|
1957
|
+
return withEvents(record, [event]);
|
|
1875
1958
|
}
|
|
1876
1959
|
async appendTurn(id, diff) {
|
|
1877
1960
|
const record = await this.get(id);
|
|
@@ -1884,14 +1967,18 @@ var ReviewStore = class {
|
|
|
1884
1967
|
const latest = latestTurn(record);
|
|
1885
1968
|
if (latest.status === "pending") {
|
|
1886
1969
|
if (diffFingerprint(latest.diff) === diffFingerprint(diff)) {
|
|
1887
|
-
this.
|
|
1970
|
+
await this.appendReviewEvent(id, "system", {
|
|
1888
1971
|
type: "review.turn.created",
|
|
1889
1972
|
reviewId: id,
|
|
1890
1973
|
turnId: latest.id,
|
|
1891
1974
|
turnIndex: latest.index,
|
|
1892
1975
|
reused: true
|
|
1893
1976
|
});
|
|
1894
|
-
return {
|
|
1977
|
+
return {
|
|
1978
|
+
record: withEvents(record, await this.readEvents(id)),
|
|
1979
|
+
turn: latest,
|
|
1980
|
+
reused: true
|
|
1981
|
+
};
|
|
1895
1982
|
}
|
|
1896
1983
|
throw new Error(`Review ${id} already has a pending turn`);
|
|
1897
1984
|
}
|
|
@@ -1906,14 +1993,18 @@ var ReviewStore = class {
|
|
|
1906
1993
|
});
|
|
1907
1994
|
this.reviews.set(id, nextRecord);
|
|
1908
1995
|
await this.persistInitial(nextRecord, turn);
|
|
1909
|
-
this.
|
|
1996
|
+
await this.appendReviewEvent(id, "system", {
|
|
1910
1997
|
type: "review.turn.created",
|
|
1911
1998
|
reviewId: id,
|
|
1912
1999
|
turnId: turn.id,
|
|
1913
2000
|
turnIndex: turn.index,
|
|
1914
2001
|
reused: false
|
|
1915
2002
|
});
|
|
1916
|
-
return {
|
|
2003
|
+
return {
|
|
2004
|
+
record: withEvents(nextRecord, await this.readEvents(id)),
|
|
2005
|
+
turn,
|
|
2006
|
+
reused: false
|
|
2007
|
+
};
|
|
1917
2008
|
}
|
|
1918
2009
|
async list() {
|
|
1919
2010
|
await this.loadAllReviews();
|
|
@@ -1985,7 +2076,7 @@ var ReviewStore = class {
|
|
|
1985
2076
|
writeTextFile(markdownPath, serializeFeedbackMarkdown(feedback))
|
|
1986
2077
|
]);
|
|
1987
2078
|
await this.persistMeta(nextRecord);
|
|
1988
|
-
this.
|
|
2079
|
+
await this.appendReviewEvent(id, "human", {
|
|
1989
2080
|
type: "review.submitted",
|
|
1990
2081
|
reviewId: id,
|
|
1991
2082
|
turnId: nextTurn.id,
|
|
@@ -1995,12 +2086,86 @@ var ReviewStore = class {
|
|
|
1995
2086
|
comments: feedback.comments.length
|
|
1996
2087
|
}
|
|
1997
2088
|
});
|
|
1998
|
-
return {
|
|
2089
|
+
return {
|
|
2090
|
+
record: withEvents(nextRecord, await this.readEvents(id)),
|
|
2091
|
+
feedbackPath,
|
|
2092
|
+
markdownPath,
|
|
2093
|
+
turn: nextTurn
|
|
2094
|
+
};
|
|
1999
2095
|
}
|
|
2000
2096
|
async feedback(id) {
|
|
2001
2097
|
const record = await this.get(id);
|
|
2002
2098
|
return record?.feedback ?? null;
|
|
2003
2099
|
}
|
|
2100
|
+
async events(id, afterSeq = 0) {
|
|
2101
|
+
const record = await this.get(id);
|
|
2102
|
+
if (!record) {
|
|
2103
|
+
return [];
|
|
2104
|
+
}
|
|
2105
|
+
return this.readEvents(id, afterSeq);
|
|
2106
|
+
}
|
|
2107
|
+
async claim(id, message, turnSelector) {
|
|
2108
|
+
const record = await this.get(id);
|
|
2109
|
+
if (!record) {
|
|
2110
|
+
throw new Error(`Review ${id} not found`);
|
|
2111
|
+
}
|
|
2112
|
+
const turn = turnSelector ? this.resolveTurnSelector(record, turnSelector) : [...record.turns].reverse().find((candidate) => candidate.status === "submitted");
|
|
2113
|
+
if (!turn) {
|
|
2114
|
+
throw new Error(`Review ${id} has no submitted unresolved turn to claim`);
|
|
2115
|
+
}
|
|
2116
|
+
if (turn.status !== "submitted") {
|
|
2117
|
+
throw new Error(`Review ${id} turn ${turn.index} is ${turn.status} and cannot be claimed`);
|
|
2118
|
+
}
|
|
2119
|
+
this.assertResolvable(turn, id);
|
|
2120
|
+
if (!turn.feedbackPath || !turn.markdownPath) {
|
|
2121
|
+
throw new Error(`Review ${id} turn ${turn.index} is missing feedback paths`);
|
|
2122
|
+
}
|
|
2123
|
+
const event = await this.appendReviewEvent(id, "agent", {
|
|
2124
|
+
type: "agent.claimed",
|
|
2125
|
+
reviewId: id,
|
|
2126
|
+
turnId: turn.id,
|
|
2127
|
+
turnIndex: turn.index,
|
|
2128
|
+
status: "claimed",
|
|
2129
|
+
...message ? { message } : {}
|
|
2130
|
+
});
|
|
2131
|
+
return {
|
|
2132
|
+
ok: true,
|
|
2133
|
+
reviewId: id,
|
|
2134
|
+
turnId: turn.id,
|
|
2135
|
+
turnIndex: turn.index,
|
|
2136
|
+
status: "claimed",
|
|
2137
|
+
feedbackPath: turn.feedbackPath,
|
|
2138
|
+
markdownPath: turn.markdownPath,
|
|
2139
|
+
artifactDir: turn.artifactDir,
|
|
2140
|
+
feedback: turn.feedback,
|
|
2141
|
+
...turn.resolution ? { resolution: turn.resolution } : {},
|
|
2142
|
+
event
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
async addAgentNote(id, message, status, turnSelector) {
|
|
2146
|
+
const record = await this.get(id);
|
|
2147
|
+
if (!record) {
|
|
2148
|
+
throw new Error(`Review ${id} not found`);
|
|
2149
|
+
}
|
|
2150
|
+
const turn = turnSelector ? this.resolveTurnSelector(record, turnSelector) : activeTurn(record);
|
|
2151
|
+
const event = await this.appendReviewEvent(id, "agent", {
|
|
2152
|
+
type: "agent.note",
|
|
2153
|
+
reviewId: id,
|
|
2154
|
+
turnId: turn.id,
|
|
2155
|
+
turnIndex: turn.index,
|
|
2156
|
+
...status ? { status } : {},
|
|
2157
|
+
message
|
|
2158
|
+
});
|
|
2159
|
+
return {
|
|
2160
|
+
ok: true,
|
|
2161
|
+
reviewId: id,
|
|
2162
|
+
turnId: turn.id,
|
|
2163
|
+
turnIndex: turn.index,
|
|
2164
|
+
...status ? { status } : {},
|
|
2165
|
+
message,
|
|
2166
|
+
event
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2004
2169
|
async markResolved(id, summary, turnSelector) {
|
|
2005
2170
|
const record = await this.get(id);
|
|
2006
2171
|
if (!record) {
|
|
@@ -2129,6 +2294,75 @@ var ReviewStore = class {
|
|
|
2129
2294
|
}
|
|
2130
2295
|
};
|
|
2131
2296
|
}
|
|
2297
|
+
async appendReviewEvent(reviewId, actor, input) {
|
|
2298
|
+
const persistedEvents = await this.readPersistedEvents(reviewId);
|
|
2299
|
+
let existingEvents = persistedEvents ?? [];
|
|
2300
|
+
if (!persistedEvents && input.type !== "review.opened") {
|
|
2301
|
+
const record2 = this.reviews.get(reviewId) ?? await this.loadKnownReview(reviewId);
|
|
2302
|
+
existingEvents = record2 ? synthesizeReviewEvents(record2) : [];
|
|
2303
|
+
if (existingEvents.length > 0) {
|
|
2304
|
+
await ensureDir(globalReviewDir(reviewId));
|
|
2305
|
+
await writeFile4(
|
|
2306
|
+
globalReviewEventsFile(reviewId),
|
|
2307
|
+
`${existingEvents.map((event2) => JSON.stringify(event2)).join("\n")}
|
|
2308
|
+
`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
const event = {
|
|
2313
|
+
...input,
|
|
2314
|
+
id: ulid(),
|
|
2315
|
+
seq: nextEventSeq(existingEvents),
|
|
2316
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2317
|
+
actor
|
|
2318
|
+
};
|
|
2319
|
+
await ensureDir(globalReviewDir(reviewId));
|
|
2320
|
+
await appendFile(globalReviewEventsFile(reviewId), `${JSON.stringify(event)}
|
|
2321
|
+
`);
|
|
2322
|
+
const record = this.reviews.get(reviewId);
|
|
2323
|
+
if (record) {
|
|
2324
|
+
this.reviews.set(reviewId, withEvents(record, [...existingEvents, event]));
|
|
2325
|
+
}
|
|
2326
|
+
this.emit(event);
|
|
2327
|
+
return event;
|
|
2328
|
+
}
|
|
2329
|
+
async readEvents(reviewId, afterSeq = 0) {
|
|
2330
|
+
const persistedEvents = await this.readPersistedEvents(reviewId);
|
|
2331
|
+
if (!persistedEvents) {
|
|
2332
|
+
const record = this.reviews.get(reviewId) ?? await this.loadKnownReview(reviewId);
|
|
2333
|
+
if (!record) {
|
|
2334
|
+
return [];
|
|
2335
|
+
}
|
|
2336
|
+
const synthesized = synthesizeReviewEvents(record);
|
|
2337
|
+
if (synthesized.length > 0) {
|
|
2338
|
+
await ensureDir(globalReviewDir(reviewId));
|
|
2339
|
+
await writeFile4(
|
|
2340
|
+
globalReviewEventsFile(reviewId),
|
|
2341
|
+
`${synthesized.map((event) => JSON.stringify(event)).join("\n")}
|
|
2342
|
+
`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
this.reviews.set(reviewId, withEvents(record, synthesized));
|
|
2346
|
+
return synthesized.filter((event) => (event.seq ?? 0) > afterSeq);
|
|
2347
|
+
}
|
|
2348
|
+
return persistedEvents.filter((event) => (event.seq ?? 0) > afterSeq);
|
|
2349
|
+
}
|
|
2350
|
+
async readPersistedEvents(reviewId) {
|
|
2351
|
+
let raw;
|
|
2352
|
+
try {
|
|
2353
|
+
raw = await readFile4(globalReviewEventsFile(reviewId), "utf8");
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
if (isFileNotFound(error)) {
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
throw new Error(`Could not read review events for ${reviewId}: ${formatError(error)}`, {
|
|
2359
|
+
cause: error
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
return raw.split("\n").map((line) => line.trim()).filter(Boolean).map(
|
|
2363
|
+
(line) => parseJsonFile(line, isReviewEvent, "review event", globalReviewEventsFile(reviewId))
|
|
2364
|
+
).filter((event) => event.reviewId === reviewId);
|
|
2365
|
+
}
|
|
2132
2366
|
emit(event) {
|
|
2133
2367
|
for (const listener of this.listeners.get(event.reviewId) ?? []) {
|
|
2134
2368
|
listener(event);
|
|
@@ -2214,7 +2448,9 @@ var ReviewStore = class {
|
|
|
2214
2448
|
diff: latest.diff
|
|
2215
2449
|
});
|
|
2216
2450
|
this.reviews.set(id, record);
|
|
2217
|
-
|
|
2451
|
+
const withTimeline = withEvents(record, await this.readEvents(id));
|
|
2452
|
+
this.reviews.set(id, withTimeline);
|
|
2453
|
+
return withTimeline;
|
|
2218
2454
|
}
|
|
2219
2455
|
async loadReviewFromTurnsOnly(id) {
|
|
2220
2456
|
const turns = await this.loadPersistedTurns(id);
|
|
@@ -2237,8 +2473,10 @@ var ReviewStore = class {
|
|
|
2237
2473
|
diff: latest.diff
|
|
2238
2474
|
});
|
|
2239
2475
|
this.reviews.set(id, record);
|
|
2240
|
-
await this.
|
|
2241
|
-
|
|
2476
|
+
const withTimeline = withEvents(record, await this.readEvents(id));
|
|
2477
|
+
this.reviews.set(id, withTimeline);
|
|
2478
|
+
await this.persistMeta(withTimeline);
|
|
2479
|
+
return withTimeline;
|
|
2242
2480
|
}
|
|
2243
2481
|
async loadPersistedTurns(id) {
|
|
2244
2482
|
let entries;
|
|
@@ -2383,7 +2621,7 @@ var ReviewStore = class {
|
|
|
2383
2621
|
path: resolvedPath,
|
|
2384
2622
|
resolution
|
|
2385
2623
|
};
|
|
2386
|
-
this.
|
|
2624
|
+
await this.appendReviewEvent(record.meta.id, "agent", {
|
|
2387
2625
|
type: "review.updated",
|
|
2388
2626
|
reviewId: record.meta.id,
|
|
2389
2627
|
turnId: nextTurn.id,
|
|
@@ -2475,6 +2713,88 @@ function turnSummary(turn) {
|
|
|
2475
2713
|
comments: resolutionCounts(turn.feedback, turn.resolution?.comments ?? [])
|
|
2476
2714
|
};
|
|
2477
2715
|
}
|
|
2716
|
+
function withEvents(record, events) {
|
|
2717
|
+
return { ...record, events: events.toSorted(compareReviewEvents) };
|
|
2718
|
+
}
|
|
2719
|
+
function nextEventSeq(events) {
|
|
2720
|
+
return Math.max(0, ...events.map((event) => event.seq ?? 0)) + 1;
|
|
2721
|
+
}
|
|
2722
|
+
function synthesizeReviewEvents(record) {
|
|
2723
|
+
let seq = 1;
|
|
2724
|
+
const next = (actor, input, createdAt) => ({
|
|
2725
|
+
...input,
|
|
2726
|
+
id: ulid(),
|
|
2727
|
+
seq: seq++,
|
|
2728
|
+
createdAt,
|
|
2729
|
+
actor
|
|
2730
|
+
});
|
|
2731
|
+
const events = [
|
|
2732
|
+
next("system", { type: "review.opened", reviewId: record.meta.id }, record.meta.createdAt)
|
|
2733
|
+
];
|
|
2734
|
+
const turns = record.turns.toSorted((a, b) => a.index - b.index);
|
|
2735
|
+
for (const turn of turns) {
|
|
2736
|
+
if (turn.index > 1) {
|
|
2737
|
+
events.push(
|
|
2738
|
+
next(
|
|
2739
|
+
"system",
|
|
2740
|
+
{
|
|
2741
|
+
type: "review.turn.created",
|
|
2742
|
+
reviewId: record.meta.id,
|
|
2743
|
+
turnId: turn.id,
|
|
2744
|
+
turnIndex: turn.index,
|
|
2745
|
+
reused: false
|
|
2746
|
+
},
|
|
2747
|
+
turn.createdAt
|
|
2748
|
+
)
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (turn.feedback) {
|
|
2752
|
+
events.push(
|
|
2753
|
+
next(
|
|
2754
|
+
"human",
|
|
2755
|
+
{
|
|
2756
|
+
type: "review.submitted",
|
|
2757
|
+
reviewId: record.meta.id,
|
|
2758
|
+
turnId: turn.id,
|
|
2759
|
+
turnIndex: turn.index,
|
|
2760
|
+
counts: {
|
|
2761
|
+
files: countCommentFiles(turn.feedback.comments),
|
|
2762
|
+
comments: turn.feedback.comments.length
|
|
2763
|
+
}
|
|
2764
|
+
},
|
|
2765
|
+
turn.submittedAt ?? turn.feedback.timestamp
|
|
2766
|
+
)
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
if (turn.feedback && turn.resolution) {
|
|
2770
|
+
const counts = resolutionCounts(turn.feedback, turn.resolution.comments);
|
|
2771
|
+
events.push(
|
|
2772
|
+
next(
|
|
2773
|
+
"agent",
|
|
2774
|
+
{
|
|
2775
|
+
type: "review.updated",
|
|
2776
|
+
reviewId: record.meta.id,
|
|
2777
|
+
turnId: turn.id,
|
|
2778
|
+
turnIndex: turn.index,
|
|
2779
|
+
reason: turn.resolution.status === "resolved" ? "review-resolved" : "comment-resolved",
|
|
2780
|
+
status: turn.status,
|
|
2781
|
+
resolutionStatus: turn.resolution.status,
|
|
2782
|
+
counts
|
|
2783
|
+
},
|
|
2784
|
+
turn.resolution.resolvedAt ?? turn.resolution.comments.at(-1)?.resolvedAt ?? turn.resolvedAt ?? turn.submittedAt ?? turn.createdAt
|
|
2785
|
+
)
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
return events.toSorted(compareReviewEvents);
|
|
2790
|
+
}
|
|
2791
|
+
function compareReviewEvents(left, right) {
|
|
2792
|
+
const seqDelta = (left.seq ?? 0) - (right.seq ?? 0);
|
|
2793
|
+
if (seqDelta !== 0) {
|
|
2794
|
+
return seqDelta;
|
|
2795
|
+
}
|
|
2796
|
+
return (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
|
|
2797
|
+
}
|
|
2478
2798
|
function reconcileTurn(meta, diff, feedback, resolution) {
|
|
2479
2799
|
const status = resolution?.status === "resolved" ? "resolved" : feedback ? "submitted" : "pending";
|
|
2480
2800
|
return {
|
|
@@ -2612,7 +2932,8 @@ program.command("open").description("Capture local changes and open them for rev
|
|
|
2612
2932
|
if (options.open !== false) {
|
|
2613
2933
|
await openBrowser(url);
|
|
2614
2934
|
}
|
|
2615
|
-
}
|
|
2935
|
+
},
|
|
2936
|
+
{ turnId: turn.id }
|
|
2616
2937
|
);
|
|
2617
2938
|
info = watched.info;
|
|
2618
2939
|
client = new ServerClient(serverUrl(info));
|
|
@@ -2715,6 +3036,23 @@ program.command("resolve").argument("<reviewId>", "review id").description("Mark
|
|
|
2715
3036
|
);
|
|
2716
3037
|
}
|
|
2717
3038
|
);
|
|
3039
|
+
program.command("claim").argument("<reviewId>", "review id").description("Claim the latest submitted unresolved review turn for agent work").option("--message <text>", "optional visible agent status message").option("--turn <idOrIndex>", "claim a specific submitted turn").action(async (reviewId, options) => {
|
|
3040
|
+
const globals = program.opts();
|
|
3041
|
+
const info = await ensureServer();
|
|
3042
|
+
const client = new ServerClient(serverUrl(info));
|
|
3043
|
+
const result = await client.claimReview(reviewId, options.message, options.turn);
|
|
3044
|
+
globals.json ? printJson(result) : printPlain(`Claimed review ${reviewId} turn ${result.turnIndex}`);
|
|
3045
|
+
});
|
|
3046
|
+
program.command("note").argument("<reviewId>", "review id").description("Post a visible agent progress note to a review").requiredOption("--message <text>", "agent note text").option("--status <status>", "agent status: claimed, working, blocked, completed, or failed").option("--turn <idOrIndex>", "attach the note to a specific turn").action(
|
|
3047
|
+
async (reviewId, options) => {
|
|
3048
|
+
const globals = program.opts();
|
|
3049
|
+
const status = parseAgentStatus(options.status);
|
|
3050
|
+
const info = await ensureServer();
|
|
3051
|
+
const client = new ServerClient(serverUrl(info));
|
|
3052
|
+
const result = await client.addAgentNote(reviewId, options.message, status, options.turn);
|
|
3053
|
+
globals.json ? printJson(result) : printPlain(`Added note to review ${reviewId}${status ? ` (${status})` : ""}`);
|
|
3054
|
+
}
|
|
3055
|
+
);
|
|
2718
3056
|
program.command("doctor").description("Diagnose setup and validate git/state").action(async () => {
|
|
2719
3057
|
const globals = program.opts();
|
|
2720
3058
|
const checks = [];
|
|
@@ -2777,7 +3115,7 @@ program.command("doctor").description("Diagnose setup and validate git/state").a
|
|
|
2777
3115
|
}
|
|
2778
3116
|
}
|
|
2779
3117
|
});
|
|
2780
|
-
async function watchReviewWithReconnect(reviewId, initialInfo, timeoutSeconds, onServerChanged) {
|
|
3118
|
+
async function watchReviewWithReconnect(reviewId, initialInfo, timeoutSeconds, onServerChanged, options = {}) {
|
|
2781
3119
|
const startedAt = Date.now();
|
|
2782
3120
|
let info = initialInfo;
|
|
2783
3121
|
while (true) {
|
|
@@ -2786,7 +3124,10 @@ async function watchReviewWithReconnect(reviewId, initialInfo, timeoutSeconds, o
|
|
|
2786
3124
|
throw new Error(`watch timed out after ${timeoutSeconds} seconds`);
|
|
2787
3125
|
}
|
|
2788
3126
|
try {
|
|
2789
|
-
const event = await new ServerClient(serverUrl(info)).watchReview(reviewId,
|
|
3127
|
+
const event = await new ServerClient(serverUrl(info)).watchReview(reviewId, {
|
|
3128
|
+
timeoutSeconds: remainingSeconds,
|
|
3129
|
+
turnId: options.turnId
|
|
3130
|
+
});
|
|
2790
3131
|
return { event, info };
|
|
2791
3132
|
} catch (error) {
|
|
2792
3133
|
if (isWatchTimeout(error)) {
|
|
@@ -2814,7 +3155,7 @@ async function checkStateDirAccess() {
|
|
|
2814
3155
|
try {
|
|
2815
3156
|
await ensureDir(globalStateDir());
|
|
2816
3157
|
await access(globalStateDir(), constants.R_OK | constants.W_OK | constants.X_OK);
|
|
2817
|
-
await
|
|
3158
|
+
await writeFile5(probePath, "");
|
|
2818
3159
|
await rm5(probePath, { force: true });
|
|
2819
3160
|
return { name: "state-dir", ok: true, detail: stateDirDetail() };
|
|
2820
3161
|
} catch (error) {
|
|
@@ -2862,6 +3203,15 @@ function parseOlderThanDays(value) {
|
|
|
2862
3203
|
}
|
|
2863
3204
|
return days;
|
|
2864
3205
|
}
|
|
3206
|
+
function parseAgentStatus(value) {
|
|
3207
|
+
if (value === void 0) {
|
|
3208
|
+
return void 0;
|
|
3209
|
+
}
|
|
3210
|
+
if (value === "claimed" || value === "working" || value === "blocked" || value === "completed" || value === "failed") {
|
|
3211
|
+
return value;
|
|
3212
|
+
}
|
|
3213
|
+
throw new Error("--status must be one of claimed, working, blocked, completed, or failed");
|
|
3214
|
+
}
|
|
2865
3215
|
function formatClearResult(result) {
|
|
2866
3216
|
const action = result.dryRun ? "Would delete" : "Deleted";
|
|
2867
3217
|
const count = result.dryRun ? result.counts.candidates : result.counts.deleted;
|