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/server/daemon.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from "path";
|
|
|
10
10
|
// package.json
|
|
11
11
|
var package_default = {
|
|
12
12
|
name: "getgloss",
|
|
13
|
-
version: "0.
|
|
13
|
+
version: "0.14.0",
|
|
14
14
|
description: "Local browser-based diff review for coding-agent loops.",
|
|
15
15
|
type: "module",
|
|
16
16
|
packageManager: "pnpm@10.33.2",
|
|
@@ -92,6 +92,7 @@ var package_default = {
|
|
|
92
92
|
|
|
93
93
|
// src/shared/paths.ts
|
|
94
94
|
var packageVersion = package_default.version;
|
|
95
|
+
var protocolVersion = 1;
|
|
95
96
|
function expandHome(input) {
|
|
96
97
|
if (input === "~") {
|
|
97
98
|
return homedir();
|
|
@@ -149,6 +150,9 @@ function globalReviewMarkdownFile(reviewId) {
|
|
|
149
150
|
function globalReviewResolvedFile(reviewId) {
|
|
150
151
|
return path.join(globalReviewDir(reviewId), "resolved.json");
|
|
151
152
|
}
|
|
153
|
+
function globalReviewEventsFile(reviewId) {
|
|
154
|
+
return path.join(globalReviewDir(reviewId), "events.jsonl");
|
|
155
|
+
}
|
|
152
156
|
async function ensureDir(dir) {
|
|
153
157
|
await mkdir(dir, { recursive: true });
|
|
154
158
|
}
|
|
@@ -205,6 +209,14 @@ var SOURCE_PEEK_MAX_BYTES = 35e4;
|
|
|
205
209
|
var SOURCE_PEEK_RANGE_MAX_LINES = 240;
|
|
206
210
|
var REVIEW_SCOPE_MODES = ["all", "single", "range"];
|
|
207
211
|
var RESOLUTION_STATUSES = ["partial", "resolved"];
|
|
212
|
+
var REVIEW_EVENT_ACTORS = ["human", "agent", "system"];
|
|
213
|
+
var AGENT_STATUSES = ["claimed", "working", "blocked", "completed", "failed"];
|
|
214
|
+
var REVIEW_UPDATE_REASONS = [
|
|
215
|
+
"review-resolved",
|
|
216
|
+
"comment-resolved",
|
|
217
|
+
"comment-reopened",
|
|
218
|
+
"turn-created"
|
|
219
|
+
];
|
|
208
220
|
var OPEN_FILE_SCOPES = ["review", "repo"];
|
|
209
221
|
var OPEN_FILE_TARGETS = [
|
|
210
222
|
"default",
|
|
@@ -253,7 +265,7 @@ function parseJsonValue(value, guard, label) {
|
|
|
253
265
|
return value;
|
|
254
266
|
}
|
|
255
267
|
function isServerInfo(value) {
|
|
256
|
-
return isRecord(value) && isNumber(value.pid) && isNumber(value.port) && isString(value.version) && isString(value.startedAt) && isString(value.stateDir) && isOptionalString(value.cwd) && isOptionalString(value.daemonPath);
|
|
268
|
+
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);
|
|
257
269
|
}
|
|
258
270
|
function isClearReviewsRequest(value) {
|
|
259
271
|
return isRecord(value) && isOptionalNonNegativeInteger(value.olderThanDays) && isOptionalBoolean(value.dryRun);
|
|
@@ -276,6 +288,12 @@ function isSourcePeekRequest(value) {
|
|
|
276
288
|
function isSourcePeekRangeRequest(value) {
|
|
277
289
|
return isRecord(value) && isString(value.filePath) && isOptionalString(value.turnId) && isDiffContextSource(value.source) && isOneOf(value.side, SIDES) && isPositiveInteger(value.startLine) && isPositiveInteger(value.lineCount) && value.lineCount <= SOURCE_PEEK_RANGE_MAX_LINES;
|
|
278
290
|
}
|
|
291
|
+
function isAgentClaimRequest(value) {
|
|
292
|
+
return isRecord(value) && isOptionalString(value.message) && isOptionalString(value.turn);
|
|
293
|
+
}
|
|
294
|
+
function isAgentNoteRequest(value) {
|
|
295
|
+
return isRecord(value) && isString(value.message) && isOptional(value.status, isAgentStatus) && isOptionalString(value.turn);
|
|
296
|
+
}
|
|
279
297
|
function isSubmitReviewRequest(value) {
|
|
280
298
|
return isRecord(value) && isArrayOf(value.comments, isComment) && isOptional(value.reviewScope, isReviewScope);
|
|
281
299
|
}
|
|
@@ -300,6 +318,31 @@ function isFeedbackBundle(value) {
|
|
|
300
318
|
function isResolutionBundle(value) {
|
|
301
319
|
return isRecord(value) && isString(value.reviewId) && isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isResolutionStatus(value.status) && isNullableString(value.summary) && isNullableString(value.resolvedAt) && isArrayOf(value.comments, isResolvedComment);
|
|
302
320
|
}
|
|
321
|
+
function isReviewEvent(value) {
|
|
322
|
+
if (!isRecord(value) || !isString(value.reviewId) || !isString(value.type)) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
if (!hasValidReviewEventEnvelope(value)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
switch (value.type) {
|
|
329
|
+
case "review.opened":
|
|
330
|
+
case "review.cancelled":
|
|
331
|
+
return true;
|
|
332
|
+
case "review.turn.created":
|
|
333
|
+
return isString(value.turnId) && isNumber(value.turnIndex) && isBoolean(value.reused);
|
|
334
|
+
case "review.submitted":
|
|
335
|
+
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isRecord(value.counts) && isNumber(value.counts.files) && isNumber(value.counts.comments);
|
|
336
|
+
case "review.updated":
|
|
337
|
+
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isReviewUpdateReason(value.reason) && isReviewStatus(value.status) && isResolutionStatus(value.resolutionStatus) && isResolutionCounts(value.counts);
|
|
338
|
+
case "agent.claimed":
|
|
339
|
+
return isString(value.turnId) && isNumber(value.turnIndex) && value.status === "claimed" && isOptionalString(value.message);
|
|
340
|
+
case "agent.note":
|
|
341
|
+
return isOptionalString(value.turnId) && isOptionalNumber(value.turnIndex) && isOptional(value.status, isAgentStatus) && isString(value.message);
|
|
342
|
+
default:
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
303
346
|
function isReviewTurnMeta(value) {
|
|
304
347
|
return isRecord(value) && isString(value.id) && isNumber(value.index) && isReviewStatus(value.status) && isString(value.createdAt) && isOptionalString(value.submittedAt) && isOptionalString(value.resolvedAt) && isString(value.artifactDir) && isString(value.diffPath) && isOptionalString(value.feedbackPath) && isOptionalString(value.markdownPath) && isOptionalString(value.resolvedPath);
|
|
305
348
|
}
|
|
@@ -391,6 +434,18 @@ function isOpenFileScope(value) {
|
|
|
391
434
|
function isOpenFileTarget(value) {
|
|
392
435
|
return isOneOf(value, OPEN_FILE_TARGETS);
|
|
393
436
|
}
|
|
437
|
+
function isReviewUpdateReason(value) {
|
|
438
|
+
return isOneOf(value, REVIEW_UPDATE_REASONS);
|
|
439
|
+
}
|
|
440
|
+
function isReviewEventActor(value) {
|
|
441
|
+
return isOneOf(value, REVIEW_EVENT_ACTORS);
|
|
442
|
+
}
|
|
443
|
+
function isAgentStatus(value) {
|
|
444
|
+
return isOneOf(value, AGENT_STATUSES);
|
|
445
|
+
}
|
|
446
|
+
function hasValidReviewEventEnvelope(value) {
|
|
447
|
+
return isOptionalString(value.id) && isOptionalNumber(value.seq) && isOptionalString(value.createdAt) && isOptional(value.actor, isReviewEventActor);
|
|
448
|
+
}
|
|
394
449
|
function isRecord(value) {
|
|
395
450
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
396
451
|
}
|
|
@@ -585,50 +640,6 @@ import { fileURLToPath } from "url";
|
|
|
585
640
|
import { Hono } from "hono";
|
|
586
641
|
import { streamSSE } from "hono/streaming";
|
|
587
642
|
|
|
588
|
-
// src/shared/comments.ts
|
|
589
|
-
function isLineComment(comment) {
|
|
590
|
-
return comment.kind === void 0 || comment.kind === "line";
|
|
591
|
-
}
|
|
592
|
-
function compareCommentsByLocation(a, b) {
|
|
593
|
-
const aIsLine = isLineComment(a);
|
|
594
|
-
const bIsLine = isLineComment(b);
|
|
595
|
-
if (!aIsLine || !bIsLine) {
|
|
596
|
-
if (aIsLine !== bIsLine) {
|
|
597
|
-
return aIsLine ? 1 : -1;
|
|
598
|
-
}
|
|
599
|
-
return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
|
|
600
|
-
}
|
|
601
|
-
return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
|
|
602
|
-
}
|
|
603
|
-
function countCommentFiles(comments) {
|
|
604
|
-
const filePaths = /* @__PURE__ */ new Set();
|
|
605
|
-
for (const comment of comments) {
|
|
606
|
-
if (isLineComment(comment)) {
|
|
607
|
-
filePaths.add(comment.filePath);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
return filePaths.size;
|
|
611
|
-
}
|
|
612
|
-
function formatLineRange(range, options = {}) {
|
|
613
|
-
const startLine = Math.min(range.startLine, range.endLine);
|
|
614
|
-
const endLine = Math.max(range.startLine, range.endLine);
|
|
615
|
-
if (startLine === endLine) {
|
|
616
|
-
return `${range.side}${startLine}`;
|
|
617
|
-
}
|
|
618
|
-
const endPrefix = options.repeatSideOnEnd === false ? "" : range.side;
|
|
619
|
-
return `${range.side}${startLine}-${endPrefix}${endLine}`;
|
|
620
|
-
}
|
|
621
|
-
function resolutionCounts(feedback, resolvedComments = []) {
|
|
622
|
-
const comments = feedback?.comments ?? [];
|
|
623
|
-
const resolvedIds = new Set(resolvedComments.map((comment) => comment.commentId));
|
|
624
|
-
const resolved = comments.filter((comment) => resolvedIds.has(comment.id)).length;
|
|
625
|
-
return {
|
|
626
|
-
total: comments.length,
|
|
627
|
-
resolved,
|
|
628
|
-
open: comments.length - resolved
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
|
|
632
643
|
// src/shared/git-diff.ts
|
|
633
644
|
import { readFile as readFile2 } from "fs/promises";
|
|
634
645
|
import path5 from "path";
|
|
@@ -884,11 +895,6 @@ function splitFileLines(contents) {
|
|
|
884
895
|
return lines;
|
|
885
896
|
}
|
|
886
897
|
|
|
887
|
-
// src/shared/reviews.ts
|
|
888
|
-
function isResolvableReviewStatus(status) {
|
|
889
|
-
return status === "submitted" || status === "resolved";
|
|
890
|
-
}
|
|
891
|
-
|
|
892
898
|
// src/server/local-open.ts
|
|
893
899
|
import { execFile } from "child_process";
|
|
894
900
|
import { access } from "fs/promises";
|
|
@@ -1652,7 +1658,7 @@ function escapeExtendedGrep(value) {
|
|
|
1652
1658
|
|
|
1653
1659
|
// src/server/store.ts
|
|
1654
1660
|
import { createHash } from "crypto";
|
|
1655
|
-
import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
1661
|
+
import { appendFile, readdir as readdir2, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
1656
1662
|
import path8 from "path";
|
|
1657
1663
|
import { ulid } from "ulid";
|
|
1658
1664
|
|
|
@@ -1870,6 +1876,50 @@ function cleanupResult({
|
|
|
1870
1876
|
};
|
|
1871
1877
|
}
|
|
1872
1878
|
|
|
1879
|
+
// src/shared/comments.ts
|
|
1880
|
+
function isLineComment(comment) {
|
|
1881
|
+
return comment.kind === void 0 || comment.kind === "line";
|
|
1882
|
+
}
|
|
1883
|
+
function compareCommentsByLocation(a, b) {
|
|
1884
|
+
const aIsLine = isLineComment(a);
|
|
1885
|
+
const bIsLine = isLineComment(b);
|
|
1886
|
+
if (!aIsLine || !bIsLine) {
|
|
1887
|
+
if (aIsLine !== bIsLine) {
|
|
1888
|
+
return aIsLine ? 1 : -1;
|
|
1889
|
+
}
|
|
1890
|
+
return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
|
|
1891
|
+
}
|
|
1892
|
+
return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
|
|
1893
|
+
}
|
|
1894
|
+
function countCommentFiles(comments) {
|
|
1895
|
+
const filePaths = /* @__PURE__ */ new Set();
|
|
1896
|
+
for (const comment of comments) {
|
|
1897
|
+
if (isLineComment(comment)) {
|
|
1898
|
+
filePaths.add(comment.filePath);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
return filePaths.size;
|
|
1902
|
+
}
|
|
1903
|
+
function formatLineRange(range, options = {}) {
|
|
1904
|
+
const startLine = Math.min(range.startLine, range.endLine);
|
|
1905
|
+
const endLine = Math.max(range.startLine, range.endLine);
|
|
1906
|
+
if (startLine === endLine) {
|
|
1907
|
+
return `${range.side}${startLine}`;
|
|
1908
|
+
}
|
|
1909
|
+
const endPrefix = options.repeatSideOnEnd === false ? "" : range.side;
|
|
1910
|
+
return `${range.side}${startLine}-${endPrefix}${endLine}`;
|
|
1911
|
+
}
|
|
1912
|
+
function resolutionCounts(feedback, resolvedComments = []) {
|
|
1913
|
+
const comments = feedback?.comments ?? [];
|
|
1914
|
+
const resolvedIds = new Set(resolvedComments.map((comment) => comment.commentId));
|
|
1915
|
+
const resolved = comments.filter((comment) => resolvedIds.has(comment.id)).length;
|
|
1916
|
+
return {
|
|
1917
|
+
total: comments.length,
|
|
1918
|
+
resolved,
|
|
1919
|
+
open: comments.length - resolved
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1873
1923
|
// src/shared/review-scope.ts
|
|
1874
1924
|
var ALL_REVIEW_SCOPE = { mode: "all" };
|
|
1875
1925
|
function normalizeReviewScope(diff, scope = ALL_REVIEW_SCOPE) {
|
|
@@ -1992,6 +2042,11 @@ function firstNonEmptyLine(text) {
|
|
|
1992
2042
|
return void 0;
|
|
1993
2043
|
}
|
|
1994
2044
|
|
|
2045
|
+
// src/shared/reviews.ts
|
|
2046
|
+
function isResolvableReviewStatus(status) {
|
|
2047
|
+
return status === "submitted" || status === "resolved";
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1995
2050
|
// src/server/store.ts
|
|
1996
2051
|
var ReviewStore = class {
|
|
1997
2052
|
reviews = /* @__PURE__ */ new Map();
|
|
@@ -2013,8 +2068,11 @@ var ReviewStore = class {
|
|
|
2013
2068
|
const record = normalizeRecord({ meta, turns: [turn], diff: turn.diff });
|
|
2014
2069
|
this.reviews.set(id, record);
|
|
2015
2070
|
await this.persistInitial(record, turn);
|
|
2016
|
-
this.
|
|
2017
|
-
|
|
2071
|
+
const event = await this.appendReviewEvent(id, "system", {
|
|
2072
|
+
type: "review.opened",
|
|
2073
|
+
reviewId: id
|
|
2074
|
+
});
|
|
2075
|
+
return withEvents(record, [event]);
|
|
2018
2076
|
}
|
|
2019
2077
|
async appendTurn(id, diff) {
|
|
2020
2078
|
const record = await this.get(id);
|
|
@@ -2027,14 +2085,18 @@ var ReviewStore = class {
|
|
|
2027
2085
|
const latest = latestTurn(record);
|
|
2028
2086
|
if (latest.status === "pending") {
|
|
2029
2087
|
if (diffFingerprint(latest.diff) === diffFingerprint(diff)) {
|
|
2030
|
-
this.
|
|
2088
|
+
await this.appendReviewEvent(id, "system", {
|
|
2031
2089
|
type: "review.turn.created",
|
|
2032
2090
|
reviewId: id,
|
|
2033
2091
|
turnId: latest.id,
|
|
2034
2092
|
turnIndex: latest.index,
|
|
2035
2093
|
reused: true
|
|
2036
2094
|
});
|
|
2037
|
-
return {
|
|
2095
|
+
return {
|
|
2096
|
+
record: withEvents(record, await this.readEvents(id)),
|
|
2097
|
+
turn: latest,
|
|
2098
|
+
reused: true
|
|
2099
|
+
};
|
|
2038
2100
|
}
|
|
2039
2101
|
throw new Error(`Review ${id} already has a pending turn`);
|
|
2040
2102
|
}
|
|
@@ -2049,14 +2111,18 @@ var ReviewStore = class {
|
|
|
2049
2111
|
});
|
|
2050
2112
|
this.reviews.set(id, nextRecord);
|
|
2051
2113
|
await this.persistInitial(nextRecord, turn);
|
|
2052
|
-
this.
|
|
2114
|
+
await this.appendReviewEvent(id, "system", {
|
|
2053
2115
|
type: "review.turn.created",
|
|
2054
2116
|
reviewId: id,
|
|
2055
2117
|
turnId: turn.id,
|
|
2056
2118
|
turnIndex: turn.index,
|
|
2057
2119
|
reused: false
|
|
2058
2120
|
});
|
|
2059
|
-
return {
|
|
2121
|
+
return {
|
|
2122
|
+
record: withEvents(nextRecord, await this.readEvents(id)),
|
|
2123
|
+
turn,
|
|
2124
|
+
reused: false
|
|
2125
|
+
};
|
|
2060
2126
|
}
|
|
2061
2127
|
async list() {
|
|
2062
2128
|
await this.loadAllReviews();
|
|
@@ -2128,7 +2194,7 @@ var ReviewStore = class {
|
|
|
2128
2194
|
writeTextFile(markdownPath, serializeFeedbackMarkdown(feedback))
|
|
2129
2195
|
]);
|
|
2130
2196
|
await this.persistMeta(nextRecord);
|
|
2131
|
-
this.
|
|
2197
|
+
await this.appendReviewEvent(id, "human", {
|
|
2132
2198
|
type: "review.submitted",
|
|
2133
2199
|
reviewId: id,
|
|
2134
2200
|
turnId: nextTurn.id,
|
|
@@ -2138,12 +2204,86 @@ var ReviewStore = class {
|
|
|
2138
2204
|
comments: feedback.comments.length
|
|
2139
2205
|
}
|
|
2140
2206
|
});
|
|
2141
|
-
return {
|
|
2207
|
+
return {
|
|
2208
|
+
record: withEvents(nextRecord, await this.readEvents(id)),
|
|
2209
|
+
feedbackPath,
|
|
2210
|
+
markdownPath,
|
|
2211
|
+
turn: nextTurn
|
|
2212
|
+
};
|
|
2142
2213
|
}
|
|
2143
2214
|
async feedback(id) {
|
|
2144
2215
|
const record = await this.get(id);
|
|
2145
2216
|
return record?.feedback ?? null;
|
|
2146
2217
|
}
|
|
2218
|
+
async events(id, afterSeq = 0) {
|
|
2219
|
+
const record = await this.get(id);
|
|
2220
|
+
if (!record) {
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
2223
|
+
return this.readEvents(id, afterSeq);
|
|
2224
|
+
}
|
|
2225
|
+
async claim(id, message, turnSelector) {
|
|
2226
|
+
const record = await this.get(id);
|
|
2227
|
+
if (!record) {
|
|
2228
|
+
throw new Error(`Review ${id} not found`);
|
|
2229
|
+
}
|
|
2230
|
+
const turn = turnSelector ? this.resolveTurnSelector(record, turnSelector) : [...record.turns].reverse().find((candidate) => candidate.status === "submitted");
|
|
2231
|
+
if (!turn) {
|
|
2232
|
+
throw new Error(`Review ${id} has no submitted unresolved turn to claim`);
|
|
2233
|
+
}
|
|
2234
|
+
if (turn.status !== "submitted") {
|
|
2235
|
+
throw new Error(`Review ${id} turn ${turn.index} is ${turn.status} and cannot be claimed`);
|
|
2236
|
+
}
|
|
2237
|
+
this.assertResolvable(turn, id);
|
|
2238
|
+
if (!turn.feedbackPath || !turn.markdownPath) {
|
|
2239
|
+
throw new Error(`Review ${id} turn ${turn.index} is missing feedback paths`);
|
|
2240
|
+
}
|
|
2241
|
+
const event = await this.appendReviewEvent(id, "agent", {
|
|
2242
|
+
type: "agent.claimed",
|
|
2243
|
+
reviewId: id,
|
|
2244
|
+
turnId: turn.id,
|
|
2245
|
+
turnIndex: turn.index,
|
|
2246
|
+
status: "claimed",
|
|
2247
|
+
...message ? { message } : {}
|
|
2248
|
+
});
|
|
2249
|
+
return {
|
|
2250
|
+
ok: true,
|
|
2251
|
+
reviewId: id,
|
|
2252
|
+
turnId: turn.id,
|
|
2253
|
+
turnIndex: turn.index,
|
|
2254
|
+
status: "claimed",
|
|
2255
|
+
feedbackPath: turn.feedbackPath,
|
|
2256
|
+
markdownPath: turn.markdownPath,
|
|
2257
|
+
artifactDir: turn.artifactDir,
|
|
2258
|
+
feedback: turn.feedback,
|
|
2259
|
+
...turn.resolution ? { resolution: turn.resolution } : {},
|
|
2260
|
+
event
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
async addAgentNote(id, message, status, turnSelector) {
|
|
2264
|
+
const record = await this.get(id);
|
|
2265
|
+
if (!record) {
|
|
2266
|
+
throw new Error(`Review ${id} not found`);
|
|
2267
|
+
}
|
|
2268
|
+
const turn = turnSelector ? this.resolveTurnSelector(record, turnSelector) : activeTurn(record);
|
|
2269
|
+
const event = await this.appendReviewEvent(id, "agent", {
|
|
2270
|
+
type: "agent.note",
|
|
2271
|
+
reviewId: id,
|
|
2272
|
+
turnId: turn.id,
|
|
2273
|
+
turnIndex: turn.index,
|
|
2274
|
+
...status ? { status } : {},
|
|
2275
|
+
message
|
|
2276
|
+
});
|
|
2277
|
+
return {
|
|
2278
|
+
ok: true,
|
|
2279
|
+
reviewId: id,
|
|
2280
|
+
turnId: turn.id,
|
|
2281
|
+
turnIndex: turn.index,
|
|
2282
|
+
...status ? { status } : {},
|
|
2283
|
+
message,
|
|
2284
|
+
event
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2147
2287
|
async markResolved(id, summary, turnSelector) {
|
|
2148
2288
|
const record = await this.get(id);
|
|
2149
2289
|
if (!record) {
|
|
@@ -2272,6 +2412,75 @@ var ReviewStore = class {
|
|
|
2272
2412
|
}
|
|
2273
2413
|
};
|
|
2274
2414
|
}
|
|
2415
|
+
async appendReviewEvent(reviewId, actor, input) {
|
|
2416
|
+
const persistedEvents = await this.readPersistedEvents(reviewId);
|
|
2417
|
+
let existingEvents = persistedEvents ?? [];
|
|
2418
|
+
if (!persistedEvents && input.type !== "review.opened") {
|
|
2419
|
+
const record2 = this.reviews.get(reviewId) ?? await this.loadKnownReview(reviewId);
|
|
2420
|
+
existingEvents = record2 ? synthesizeReviewEvents(record2) : [];
|
|
2421
|
+
if (existingEvents.length > 0) {
|
|
2422
|
+
await ensureDir(globalReviewDir(reviewId));
|
|
2423
|
+
await writeFile3(
|
|
2424
|
+
globalReviewEventsFile(reviewId),
|
|
2425
|
+
`${existingEvents.map((event2) => JSON.stringify(event2)).join("\n")}
|
|
2426
|
+
`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const event = {
|
|
2431
|
+
...input,
|
|
2432
|
+
id: ulid(),
|
|
2433
|
+
seq: nextEventSeq(existingEvents),
|
|
2434
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2435
|
+
actor
|
|
2436
|
+
};
|
|
2437
|
+
await ensureDir(globalReviewDir(reviewId));
|
|
2438
|
+
await appendFile(globalReviewEventsFile(reviewId), `${JSON.stringify(event)}
|
|
2439
|
+
`);
|
|
2440
|
+
const record = this.reviews.get(reviewId);
|
|
2441
|
+
if (record) {
|
|
2442
|
+
this.reviews.set(reviewId, withEvents(record, [...existingEvents, event]));
|
|
2443
|
+
}
|
|
2444
|
+
this.emit(event);
|
|
2445
|
+
return event;
|
|
2446
|
+
}
|
|
2447
|
+
async readEvents(reviewId, afterSeq = 0) {
|
|
2448
|
+
const persistedEvents = await this.readPersistedEvents(reviewId);
|
|
2449
|
+
if (!persistedEvents) {
|
|
2450
|
+
const record = this.reviews.get(reviewId) ?? await this.loadKnownReview(reviewId);
|
|
2451
|
+
if (!record) {
|
|
2452
|
+
return [];
|
|
2453
|
+
}
|
|
2454
|
+
const synthesized = synthesizeReviewEvents(record);
|
|
2455
|
+
if (synthesized.length > 0) {
|
|
2456
|
+
await ensureDir(globalReviewDir(reviewId));
|
|
2457
|
+
await writeFile3(
|
|
2458
|
+
globalReviewEventsFile(reviewId),
|
|
2459
|
+
`${synthesized.map((event) => JSON.stringify(event)).join("\n")}
|
|
2460
|
+
`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
this.reviews.set(reviewId, withEvents(record, synthesized));
|
|
2464
|
+
return synthesized.filter((event) => (event.seq ?? 0) > afterSeq);
|
|
2465
|
+
}
|
|
2466
|
+
return persistedEvents.filter((event) => (event.seq ?? 0) > afterSeq);
|
|
2467
|
+
}
|
|
2468
|
+
async readPersistedEvents(reviewId) {
|
|
2469
|
+
let raw;
|
|
2470
|
+
try {
|
|
2471
|
+
raw = await readFile5(globalReviewEventsFile(reviewId), "utf8");
|
|
2472
|
+
} catch (error) {
|
|
2473
|
+
if (isFileNotFound(error)) {
|
|
2474
|
+
return null;
|
|
2475
|
+
}
|
|
2476
|
+
throw new Error(`Could not read review events for ${reviewId}: ${formatError(error)}`, {
|
|
2477
|
+
cause: error
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
return raw.split("\n").map((line) => line.trim()).filter(Boolean).map(
|
|
2481
|
+
(line) => parseJsonFile(line, isReviewEvent, "review event", globalReviewEventsFile(reviewId))
|
|
2482
|
+
).filter((event) => event.reviewId === reviewId);
|
|
2483
|
+
}
|
|
2275
2484
|
emit(event) {
|
|
2276
2485
|
for (const listener of this.listeners.get(event.reviewId) ?? []) {
|
|
2277
2486
|
listener(event);
|
|
@@ -2357,7 +2566,9 @@ var ReviewStore = class {
|
|
|
2357
2566
|
diff: latest.diff
|
|
2358
2567
|
});
|
|
2359
2568
|
this.reviews.set(id, record);
|
|
2360
|
-
|
|
2569
|
+
const withTimeline = withEvents(record, await this.readEvents(id));
|
|
2570
|
+
this.reviews.set(id, withTimeline);
|
|
2571
|
+
return withTimeline;
|
|
2361
2572
|
}
|
|
2362
2573
|
async loadReviewFromTurnsOnly(id) {
|
|
2363
2574
|
const turns = await this.loadPersistedTurns(id);
|
|
@@ -2380,8 +2591,10 @@ var ReviewStore = class {
|
|
|
2380
2591
|
diff: latest.diff
|
|
2381
2592
|
});
|
|
2382
2593
|
this.reviews.set(id, record);
|
|
2383
|
-
await this.
|
|
2384
|
-
|
|
2594
|
+
const withTimeline = withEvents(record, await this.readEvents(id));
|
|
2595
|
+
this.reviews.set(id, withTimeline);
|
|
2596
|
+
await this.persistMeta(withTimeline);
|
|
2597
|
+
return withTimeline;
|
|
2385
2598
|
}
|
|
2386
2599
|
async loadPersistedTurns(id) {
|
|
2387
2600
|
let entries;
|
|
@@ -2526,7 +2739,7 @@ var ReviewStore = class {
|
|
|
2526
2739
|
path: resolvedPath,
|
|
2527
2740
|
resolution
|
|
2528
2741
|
};
|
|
2529
|
-
this.
|
|
2742
|
+
await this.appendReviewEvent(record.meta.id, "agent", {
|
|
2530
2743
|
type: "review.updated",
|
|
2531
2744
|
reviewId: record.meta.id,
|
|
2532
2745
|
turnId: nextTurn.id,
|
|
@@ -2618,6 +2831,88 @@ function turnSummary(turn) {
|
|
|
2618
2831
|
comments: resolutionCounts(turn.feedback, turn.resolution?.comments ?? [])
|
|
2619
2832
|
};
|
|
2620
2833
|
}
|
|
2834
|
+
function withEvents(record, events) {
|
|
2835
|
+
return { ...record, events: events.toSorted(compareReviewEvents) };
|
|
2836
|
+
}
|
|
2837
|
+
function nextEventSeq(events) {
|
|
2838
|
+
return Math.max(0, ...events.map((event) => event.seq ?? 0)) + 1;
|
|
2839
|
+
}
|
|
2840
|
+
function synthesizeReviewEvents(record) {
|
|
2841
|
+
let seq = 1;
|
|
2842
|
+
const next = (actor, input, createdAt) => ({
|
|
2843
|
+
...input,
|
|
2844
|
+
id: ulid(),
|
|
2845
|
+
seq: seq++,
|
|
2846
|
+
createdAt,
|
|
2847
|
+
actor
|
|
2848
|
+
});
|
|
2849
|
+
const events = [
|
|
2850
|
+
next("system", { type: "review.opened", reviewId: record.meta.id }, record.meta.createdAt)
|
|
2851
|
+
];
|
|
2852
|
+
const turns = record.turns.toSorted((a, b) => a.index - b.index);
|
|
2853
|
+
for (const turn of turns) {
|
|
2854
|
+
if (turn.index > 1) {
|
|
2855
|
+
events.push(
|
|
2856
|
+
next(
|
|
2857
|
+
"system",
|
|
2858
|
+
{
|
|
2859
|
+
type: "review.turn.created",
|
|
2860
|
+
reviewId: record.meta.id,
|
|
2861
|
+
turnId: turn.id,
|
|
2862
|
+
turnIndex: turn.index,
|
|
2863
|
+
reused: false
|
|
2864
|
+
},
|
|
2865
|
+
turn.createdAt
|
|
2866
|
+
)
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
if (turn.feedback) {
|
|
2870
|
+
events.push(
|
|
2871
|
+
next(
|
|
2872
|
+
"human",
|
|
2873
|
+
{
|
|
2874
|
+
type: "review.submitted",
|
|
2875
|
+
reviewId: record.meta.id,
|
|
2876
|
+
turnId: turn.id,
|
|
2877
|
+
turnIndex: turn.index,
|
|
2878
|
+
counts: {
|
|
2879
|
+
files: countCommentFiles(turn.feedback.comments),
|
|
2880
|
+
comments: turn.feedback.comments.length
|
|
2881
|
+
}
|
|
2882
|
+
},
|
|
2883
|
+
turn.submittedAt ?? turn.feedback.timestamp
|
|
2884
|
+
)
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
if (turn.feedback && turn.resolution) {
|
|
2888
|
+
const counts = resolutionCounts(turn.feedback, turn.resolution.comments);
|
|
2889
|
+
events.push(
|
|
2890
|
+
next(
|
|
2891
|
+
"agent",
|
|
2892
|
+
{
|
|
2893
|
+
type: "review.updated",
|
|
2894
|
+
reviewId: record.meta.id,
|
|
2895
|
+
turnId: turn.id,
|
|
2896
|
+
turnIndex: turn.index,
|
|
2897
|
+
reason: turn.resolution.status === "resolved" ? "review-resolved" : "comment-resolved",
|
|
2898
|
+
status: turn.status,
|
|
2899
|
+
resolutionStatus: turn.resolution.status,
|
|
2900
|
+
counts
|
|
2901
|
+
},
|
|
2902
|
+
turn.resolution.resolvedAt ?? turn.resolution.comments.at(-1)?.resolvedAt ?? turn.resolvedAt ?? turn.submittedAt ?? turn.createdAt
|
|
2903
|
+
)
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
return events.toSorted(compareReviewEvents);
|
|
2908
|
+
}
|
|
2909
|
+
function compareReviewEvents(left, right) {
|
|
2910
|
+
const seqDelta = (left.seq ?? 0) - (right.seq ?? 0);
|
|
2911
|
+
if (seqDelta !== 0) {
|
|
2912
|
+
return seqDelta;
|
|
2913
|
+
}
|
|
2914
|
+
return (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
|
|
2915
|
+
}
|
|
2621
2916
|
function reconcileTurn(meta, diff, feedback, resolution) {
|
|
2622
2917
|
const status = resolution?.status === "resolved" ? "resolved" : feedback ? "submitted" : "pending";
|
|
2623
2918
|
return {
|
|
@@ -2700,7 +2995,9 @@ function createApp(origin2, options = {}) {
|
|
|
2700
2995
|
const response = {
|
|
2701
2996
|
ok: true,
|
|
2702
2997
|
version: packageVersion,
|
|
2998
|
+
protocolVersion,
|
|
2703
2999
|
activeReviews: reviews.filter((review) => review.status === "pending").length,
|
|
3000
|
+
stateDir: globalStateDir(),
|
|
2704
3001
|
...options.health?.()
|
|
2705
3002
|
};
|
|
2706
3003
|
return c.json(response);
|
|
@@ -2782,14 +3079,56 @@ function createApp(origin2, options = {}) {
|
|
|
2782
3079
|
}
|
|
2783
3080
|
return c.json(feedback);
|
|
2784
3081
|
});
|
|
3082
|
+
app.post("/api/reviews/:id/agent/claim", async (c) => {
|
|
3083
|
+
const id = c.req.param("id");
|
|
3084
|
+
const existing = await reviewStore.get(id);
|
|
3085
|
+
if (!existing) {
|
|
3086
|
+
return c.json({ error: "review not found" }, 404);
|
|
3087
|
+
}
|
|
3088
|
+
const parsed = await readOptionalJsonBody(c, isAgentClaimRequest, "agent claim request");
|
|
3089
|
+
if (!parsed.ok) {
|
|
3090
|
+
return parsed.response;
|
|
3091
|
+
}
|
|
3092
|
+
const body = parsed.body;
|
|
3093
|
+
try {
|
|
3094
|
+
const result = await reviewStore.claim(id, body.message, body.turn);
|
|
3095
|
+
options.onReviewActivity?.();
|
|
3096
|
+
return c.json(result);
|
|
3097
|
+
} catch (error) {
|
|
3098
|
+
return c.json({ error: formatError(error) }, statusForStoreError(error));
|
|
3099
|
+
}
|
|
3100
|
+
});
|
|
3101
|
+
app.post("/api/reviews/:id/agent/notes", async (c) => {
|
|
3102
|
+
const id = c.req.param("id");
|
|
3103
|
+
const existing = await reviewStore.get(id);
|
|
3104
|
+
if (!existing) {
|
|
3105
|
+
return c.json({ error: "review not found" }, 404);
|
|
3106
|
+
}
|
|
3107
|
+
const parsed = await readJsonBody(c, isAgentNoteRequest, "agent note request");
|
|
3108
|
+
if (!parsed.ok) {
|
|
3109
|
+
return parsed.response;
|
|
3110
|
+
}
|
|
3111
|
+
const body = parsed.body;
|
|
3112
|
+
try {
|
|
3113
|
+
const result = await reviewStore.addAgentNote(id, body.message, body.status, body.turn);
|
|
3114
|
+
options.onReviewActivity?.();
|
|
3115
|
+
return c.json(result);
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
return c.json({ error: formatError(error) }, statusForStoreError(error));
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
2785
3120
|
app.get("/api/reviews/:id/events", async (c) => {
|
|
2786
3121
|
const id = c.req.param("id");
|
|
2787
3122
|
const record = await reviewStore.get(id);
|
|
2788
3123
|
if (!record) {
|
|
2789
3124
|
return c.json({ error: "review not found" }, 404);
|
|
2790
3125
|
}
|
|
3126
|
+
const afterSeq = eventReplayAfterSeq(c);
|
|
2791
3127
|
return streamSSE(c, async (stream) => {
|
|
2792
3128
|
let closed = false;
|
|
3129
|
+
let replaying = true;
|
|
3130
|
+
let lastSentSeq = afterSeq;
|
|
3131
|
+
const bufferedEvents = [];
|
|
2793
3132
|
let pending = Promise.resolve();
|
|
2794
3133
|
let cleanup = null;
|
|
2795
3134
|
let close = null;
|
|
@@ -2806,14 +3145,30 @@ function createApp(origin2, options = {}) {
|
|
|
2806
3145
|
});
|
|
2807
3146
|
unregisterEventStream = options.registerEventStream?.(() => close?.()) ?? null;
|
|
2808
3147
|
const send = (event) => {
|
|
2809
|
-
|
|
3148
|
+
const eventSeq = event.seq ?? 0;
|
|
3149
|
+
if (eventSeq > 0 && eventSeq <= lastSentSeq) {
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
if (eventSeq > 0) {
|
|
3153
|
+
lastSentSeq = eventSeq;
|
|
3154
|
+
}
|
|
3155
|
+
pending = pending.then(() => {
|
|
3156
|
+
const data = JSON.stringify(event);
|
|
3157
|
+
return event.seq ? stream.writeSSE({ data, id: String(event.seq) }) : stream.writeSSE({ data });
|
|
3158
|
+
}).then(() => {
|
|
2810
3159
|
if (event.type === "review.cancelled") {
|
|
2811
3160
|
close?.();
|
|
2812
3161
|
}
|
|
2813
3162
|
});
|
|
2814
3163
|
void pending.catch(() => close?.());
|
|
2815
3164
|
};
|
|
2816
|
-
const unsubscribe = reviewStore.subscribe(id,
|
|
3165
|
+
const unsubscribe = reviewStore.subscribe(id, (event) => {
|
|
3166
|
+
if (replaying) {
|
|
3167
|
+
bufferedEvents.push(event);
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
send(event);
|
|
3171
|
+
});
|
|
2817
3172
|
const heartbeat = setInterval(() => {
|
|
2818
3173
|
pending = pending.then(async () => {
|
|
2819
3174
|
await stream.write(`: keep-alive ${Date.now()}
|
|
@@ -2829,18 +3184,12 @@ function createApp(origin2, options = {}) {
|
|
|
2829
3184
|
unregisterEventStream = null;
|
|
2830
3185
|
};
|
|
2831
3186
|
stream.onAbort(() => close?.());
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
turnIndex: record.meta.turns?.find((turn) => turn.id === record.meta.activeTurnId)?.index,
|
|
2839
|
-
counts: {
|
|
2840
|
-
files: countCommentFiles(record.feedback.comments),
|
|
2841
|
-
comments: record.feedback.comments.length
|
|
2842
|
-
}
|
|
2843
|
-
});
|
|
3187
|
+
for (const event of await reviewStore.events(id, afterSeq)) {
|
|
3188
|
+
send(event);
|
|
3189
|
+
}
|
|
3190
|
+
replaying = false;
|
|
3191
|
+
for (const event of bufferedEvents) {
|
|
3192
|
+
send(event);
|
|
2844
3193
|
}
|
|
2845
3194
|
await closedPromise;
|
|
2846
3195
|
});
|
|
@@ -3310,6 +3659,12 @@ function serveRootFile(fileName, contentType) {
|
|
|
3310
3659
|
}
|
|
3311
3660
|
};
|
|
3312
3661
|
}
|
|
3662
|
+
function eventReplayAfterSeq(c) {
|
|
3663
|
+
const url = new URL(c.req.url);
|
|
3664
|
+
const rawAfter = url.searchParams.get("after") ?? c.req.header("last-event-id") ?? "0";
|
|
3665
|
+
const afterSeq = Number(rawAfter);
|
|
3666
|
+
return Number.isInteger(afterSeq) && afterSeq > 0 ? afterSeq : 0;
|
|
3667
|
+
}
|
|
3313
3668
|
async function readJsonBody(c, guard, label) {
|
|
3314
3669
|
let body;
|
|
3315
3670
|
try {
|
|
@@ -3329,6 +3684,34 @@ async function readJsonBody(c, guard, label) {
|
|
|
3329
3684
|
};
|
|
3330
3685
|
}
|
|
3331
3686
|
}
|
|
3687
|
+
async function readOptionalJsonBody(c, guard, label) {
|
|
3688
|
+
let raw;
|
|
3689
|
+
try {
|
|
3690
|
+
raw = await c.req.text();
|
|
3691
|
+
} catch (error) {
|
|
3692
|
+
return {
|
|
3693
|
+
ok: false,
|
|
3694
|
+
response: c.json({ error: `invalid JSON body: ${formatError(error)}` }, 400)
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
let body;
|
|
3698
|
+
try {
|
|
3699
|
+
body = raw.trim().length > 0 ? JSON.parse(raw) : {};
|
|
3700
|
+
} catch (error) {
|
|
3701
|
+
return {
|
|
3702
|
+
ok: false,
|
|
3703
|
+
response: c.json({ error: `invalid JSON body: ${formatError(error)}` }, 400)
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
try {
|
|
3707
|
+
return { ok: true, body: parseJsonValue(body, guard, label) };
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
return {
|
|
3710
|
+
ok: false,
|
|
3711
|
+
response: c.json({ error: formatError(error) }, 400)
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3332
3715
|
function isPathWithin(parentPath, childPath) {
|
|
3333
3716
|
const relative = path9.relative(parentPath, childPath);
|
|
3334
3717
|
return relative === "" || !relative.startsWith("..") && !path9.isAbsolute(relative);
|
|
@@ -3473,6 +3856,7 @@ await writeServerInfo({
|
|
|
3473
3856
|
pid: process.pid,
|
|
3474
3857
|
port,
|
|
3475
3858
|
version: packageVersion,
|
|
3859
|
+
protocolVersion,
|
|
3476
3860
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3477
3861
|
stateDir: globalStateDir(),
|
|
3478
3862
|
cwd: process.cwd(),
|