runcastle 1.2.12 → 1.3.1
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/bin/runcastle.js +825 -355
- package/drizzle/0034_funny_doctor_octopus.sql +2 -0
- package/drizzle/0034_hard_killraven.sql +1 -0
- package/drizzle/0034_review_evidence_stamps.sql +5 -0
- package/drizzle/meta/0034_snapshot.json +1085 -0
- package/drizzle/meta/0035_snapshot.json +1099 -0
- package/drizzle/meta/_journal.json +14 -0
- package/index.js +825 -355
- package/package.json +1 -1
- package/skills/burner/review-ticket.md +1 -1
- package/skills/burner/verify-fixes.md +39 -0
- package/web/assets/index-DcOfTvdV.js +145 -0
- package/web/assets/index-J07ijZNl.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-CcFZnrux.js +0 -143
- package/web/assets/index-jOHWTE5S.css +0 -1
package/bin/runcastle.js
CHANGED
|
@@ -176,7 +176,23 @@ import * as z2 from "zod";
|
|
|
176
176
|
function isProjectSessionKind(kind) {
|
|
177
177
|
return PROJECT_SESSION_KINDS.includes(kind);
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
function unresolvedMergeConflict(events) {
|
|
180
|
+
let conflict = null;
|
|
181
|
+
for (const event of events) {
|
|
182
|
+
if (event.type === "merge.conflict") {
|
|
183
|
+
const data = event.data ?? {};
|
|
184
|
+
conflict = {
|
|
185
|
+
base: typeof data.base === "string" ? data.base : "",
|
|
186
|
+
files: Array.isArray(data.files) ? data.files.filter((file) => typeof file === "string") : [],
|
|
187
|
+
at: event.ts
|
|
188
|
+
};
|
|
189
|
+
} else if (["burn.started", "merge.resolved", "feature.shipped"].includes(event.type)) {
|
|
190
|
+
conflict = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return conflict;
|
|
194
|
+
}
|
|
195
|
+
var Phase, TicketStatus, SessionKind, PROJECT_SESSION_KINDS, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketKind, FindingKind, FindingSeverity, FindingStatus, FindingOpenReason, ReviewFindingInput, ReviewFinding, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNoteAuthor, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow, DriveState;
|
|
180
196
|
var init_schemas = __esm(() => {
|
|
181
197
|
init_config();
|
|
182
198
|
Phase = z2.enum([
|
|
@@ -211,7 +227,7 @@ var init_schemas = __esm(() => {
|
|
|
211
227
|
FindingKind = z2.enum(["defect", "observation"]);
|
|
212
228
|
FindingSeverity = z2.enum(["high", "medium", "low"]);
|
|
213
229
|
FindingStatus = z2.enum(["open", "fixing", "fixed", "failed", "dismissed"]);
|
|
214
|
-
FindingOpenReason = z2.enum(["over-cap", "fix-failed"]);
|
|
230
|
+
FindingOpenReason = z2.enum(["over-cap", "fix-failed", "verification"]);
|
|
215
231
|
ReviewFindingInput = z2.object({
|
|
216
232
|
kind: FindingKind,
|
|
217
233
|
severity: FindingSeverity,
|
|
@@ -245,6 +261,7 @@ var init_schemas = __esm(() => {
|
|
|
245
261
|
seams: z2.array(z2.string()),
|
|
246
262
|
blockedBy: z2.array(z2.number()),
|
|
247
263
|
kind: TicketKind.default("implementation"),
|
|
264
|
+
passKind: z2.enum(["review", "verification"]).default("review"),
|
|
248
265
|
model: z2.string().optional(),
|
|
249
266
|
originFindingId: z2.string().optional()
|
|
250
267
|
});
|
|
@@ -255,6 +272,8 @@ var init_schemas = __esm(() => {
|
|
|
255
272
|
status: TicketStatus,
|
|
256
273
|
lap: z2.number(),
|
|
257
274
|
commits: z2.array(z2.string()),
|
|
275
|
+
reviewedCommit: z2.string().nullable().default(null),
|
|
276
|
+
completedAt: z2.number().nullable().default(null),
|
|
258
277
|
error: z2.string().optional(),
|
|
259
278
|
attemptBranch: z2.string().optional(),
|
|
260
279
|
conflictFiles: z2.array(z2.string()).optional(),
|
|
@@ -280,7 +299,7 @@ var init_schemas = __esm(() => {
|
|
|
280
299
|
lastSessionId: z2.string().optional(),
|
|
281
300
|
summary: z2.string().optional()
|
|
282
301
|
});
|
|
283
|
-
TestNoteStatus = z2.enum(["open", "done", "promoted"]);
|
|
302
|
+
TestNoteStatus = z2.enum(["open", "done", "promoted", "carried"]);
|
|
284
303
|
TestNoteAuthor = z2.enum(["human", "agent"]);
|
|
285
304
|
TestNote = z2.object({
|
|
286
305
|
id: z2.string(),
|
|
@@ -290,6 +309,8 @@ var init_schemas = __esm(() => {
|
|
|
290
309
|
status: TestNoteStatus,
|
|
291
310
|
author: TestNoteAuthor.default("human"),
|
|
292
311
|
ticketId: z2.string().optional(),
|
|
312
|
+
reviewTicketId: z2.string().optional(),
|
|
313
|
+
carriedLap: z2.number().optional(),
|
|
293
314
|
videoTimestamp: z2.number().optional(),
|
|
294
315
|
screenshotUrl: z2.string().optional(),
|
|
295
316
|
createdAt: z2.number(),
|
|
@@ -356,6 +377,7 @@ var init_schemas = __esm(() => {
|
|
|
356
377
|
id: z2.string(),
|
|
357
378
|
featureId: z2.string().optional(),
|
|
358
379
|
projectId: z2.string().optional(),
|
|
380
|
+
lap: z2.number(),
|
|
359
381
|
kind: SessionKind,
|
|
360
382
|
purpose: SessionPurpose.optional(),
|
|
361
383
|
purposeData: MergeBranchPair.optional(),
|
|
@@ -366,8 +388,10 @@ var init_schemas = __esm(() => {
|
|
|
366
388
|
worktreePath: z2.string(),
|
|
367
389
|
model: z2.string().optional(),
|
|
368
390
|
runtime: AgentRuntime.optional(),
|
|
369
|
-
title: z2.string().optional(),
|
|
370
|
-
|
|
391
|
+
title: z2.string().nullable().optional(),
|
|
392
|
+
transcriptMissing: z2.boolean().optional(),
|
|
393
|
+
createdAt: z2.number().optional(),
|
|
394
|
+
endedAt: z2.number().optional()
|
|
371
395
|
});
|
|
372
396
|
Run = z2.object({
|
|
373
397
|
id: z2.string(),
|
|
@@ -409,16 +433,27 @@ var init_schemas = __esm(() => {
|
|
|
409
433
|
message: z2.string(),
|
|
410
434
|
data: z2.unknown().optional()
|
|
411
435
|
});
|
|
436
|
+
DriveState = z2.enum([
|
|
437
|
+
"idle",
|
|
438
|
+
"starting",
|
|
439
|
+
"serving",
|
|
440
|
+
"bare-checkout",
|
|
441
|
+
"setup-failed",
|
|
442
|
+
"review-agent-driving"
|
|
443
|
+
]);
|
|
412
444
|
});
|
|
413
445
|
|
|
414
446
|
// ../core/src/routes.ts
|
|
447
|
+
function reviewWalkthroughUrl(ticketId) {
|
|
448
|
+
return `${REVIEWS_BASE}${REVIEW_WALKTHROUGH_ROUTE.replace(":ticketId", encodeURIComponent(ticketId))}`;
|
|
449
|
+
}
|
|
415
450
|
function fillNoteId(route, noteId) {
|
|
416
451
|
return `${REVIEWS_BASE}${route.replace(":noteId", encodeURIComponent(noteId))}`;
|
|
417
452
|
}
|
|
418
453
|
function noteScreenshotUrl(noteId) {
|
|
419
454
|
return fillNoteId(NOTE_SCREENSHOT_ROUTE, noteId);
|
|
420
455
|
}
|
|
421
|
-
var REVIEWS_BASE = "/api/reviews", NOTE_SCREENSHOT_UPLOAD_ROUTE = "/note/:noteId/screenshot", NOTE_SCREENSHOT_ROUTE;
|
|
456
|
+
var REVIEWS_BASE = "/api/reviews", REVIEW_ARTIFACTS_ROUTE = "/:featureId", REVIEW_WALKTHROUGH_ROUTE = "/ticket/:ticketId/walkthrough.webm", NOTE_SCREENSHOT_UPLOAD_ROUTE = "/note/:noteId/screenshot", NOTE_SCREENSHOT_ROUTE;
|
|
422
457
|
var init_routes = __esm(() => {
|
|
423
458
|
NOTE_SCREENSHOT_ROUTE = `${NOTE_SCREENSHOT_UPLOAD_ROUTE}.png`;
|
|
424
459
|
});
|
|
@@ -434,6 +469,17 @@ function fmtClock(seconds) {
|
|
|
434
469
|
const mm = h > 0 ? String(m).padStart(2, "0") : String(m);
|
|
435
470
|
return `${h > 0 ? `${h}:` : ""}${mm}:${String(s).padStart(2, "0")}`;
|
|
436
471
|
}
|
|
472
|
+
function ticketTitleFromNote(text) {
|
|
473
|
+
const clean = text.trim();
|
|
474
|
+
const boundary = clean.search(/(?:[.!?](?:\s|$)|\n)/);
|
|
475
|
+
const sentence = (boundary < 0 ? clean : clean.slice(0, boundary + (clean[boundary] === `
|
|
476
|
+
` ? 0 : 1))).trim();
|
|
477
|
+
if (sentence.length <= 80)
|
|
478
|
+
return sentence;
|
|
479
|
+
const cut = sentence.slice(0, 80);
|
|
480
|
+
const wordBoundary = cut.lastIndexOf(" ");
|
|
481
|
+
return (wordBoundary > 0 ? cut.slice(0, wordBoundary) : cut).trimEnd();
|
|
482
|
+
}
|
|
437
483
|
|
|
438
484
|
// ../core/src/blocking.ts
|
|
439
485
|
function resolveBatchBlocking(nodes, { startSeq, label = "ticket" }) {
|
|
@@ -636,7 +682,8 @@ var init_db_schema = __esm(() => {
|
|
|
636
682
|
model: text("model"),
|
|
637
683
|
runtime: text("runtime").$type(),
|
|
638
684
|
title: text("title"),
|
|
639
|
-
createdAt: integer("created_at")
|
|
685
|
+
createdAt: integer("created_at"),
|
|
686
|
+
endedAt: integer("ended_at")
|
|
640
687
|
});
|
|
641
688
|
tickets = sqliteTable("tickets", {
|
|
642
689
|
id: text("id").primaryKey(),
|
|
@@ -649,6 +696,9 @@ var init_db_schema = __esm(() => {
|
|
|
649
696
|
seams: text("seams", { mode: "json" }).notNull().$type(),
|
|
650
697
|
blockedBy: text("blocked_by", { mode: "json" }).notNull().$type(),
|
|
651
698
|
kind: text("kind").notNull().$type().default("implementation"),
|
|
699
|
+
passKind: text("pass_kind").notNull().$type().default("review"),
|
|
700
|
+
reviewedCommit: text("reviewed_commit"),
|
|
701
|
+
completedAt: integer("completed_at"),
|
|
652
702
|
model: text("model"),
|
|
653
703
|
lap: integer("lap").notNull().default(1),
|
|
654
704
|
status: text("status").notNull().$type(),
|
|
@@ -685,6 +735,8 @@ var init_db_schema = __esm(() => {
|
|
|
685
735
|
status: text("status").notNull().$type(),
|
|
686
736
|
author: text("author").notNull().$type().default("human"),
|
|
687
737
|
ticketId: text("ticket_id"),
|
|
738
|
+
reviewTicketId: text("review_ticket_id"),
|
|
739
|
+
carriedLap: integer("carried_lap"),
|
|
688
740
|
videoTimestamp: real("video_timestamp"),
|
|
689
741
|
createdAt: integer("created_at").notNull(),
|
|
690
742
|
updatedAt: integer("updated_at").notNull()
|
|
@@ -2077,6 +2129,7 @@ function rowToSession(row) {
|
|
|
2077
2129
|
id: row.id,
|
|
2078
2130
|
featureId: row.featureId ?? undefined,
|
|
2079
2131
|
projectId: row.projectId ?? undefined,
|
|
2132
|
+
lap: row.lap,
|
|
2080
2133
|
kind: row.kind,
|
|
2081
2134
|
purpose: row.purpose ?? undefined,
|
|
2082
2135
|
purposeData: row.purposeData ?? undefined,
|
|
@@ -2088,7 +2141,8 @@ function rowToSession(row) {
|
|
|
2088
2141
|
model: row.model ?? undefined,
|
|
2089
2142
|
runtime: row.runtime ?? undefined,
|
|
2090
2143
|
title: row.title ?? undefined,
|
|
2091
|
-
createdAt: row.createdAt ?? undefined
|
|
2144
|
+
createdAt: row.createdAt ?? undefined,
|
|
2145
|
+
endedAt: row.endedAt ?? undefined
|
|
2092
2146
|
});
|
|
2093
2147
|
}
|
|
2094
2148
|
function tryGetFeature(ctx, id) {
|
|
@@ -2854,6 +2908,19 @@ import {
|
|
|
2854
2908
|
import { tmpdir } from "os";
|
|
2855
2909
|
import { dirname, join as join6, resolve as resolve4 } from "path";
|
|
2856
2910
|
import { simpleGit } from "simple-git";
|
|
2911
|
+
function deriveDriveState(info) {
|
|
2912
|
+
if (!info)
|
|
2913
|
+
return "idle";
|
|
2914
|
+
if (info.hookFailure)
|
|
2915
|
+
return "setup-failed";
|
|
2916
|
+
if (info.purpose === "review")
|
|
2917
|
+
return "review-agent-driving";
|
|
2918
|
+
if (!info.devConfigured)
|
|
2919
|
+
return "bare-checkout";
|
|
2920
|
+
if (info.devReady)
|
|
2921
|
+
return "serving";
|
|
2922
|
+
return "starting";
|
|
2923
|
+
}
|
|
2857
2924
|
function migrationPaths(paths) {
|
|
2858
2925
|
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
2859
2926
|
}
|
|
@@ -2949,12 +3016,12 @@ async function createFeatureBranch(project, slug, base) {
|
|
|
2949
3016
|
await g.raw(["branch", branch, from]);
|
|
2950
3017
|
return branch;
|
|
2951
3018
|
}
|
|
2952
|
-
async function listBranches(project) {
|
|
3019
|
+
async function listBranches(project, options = {}) {
|
|
2953
3020
|
const g = git(project.repoPath);
|
|
2954
3021
|
const local = await g.branchLocal();
|
|
2955
3022
|
const detachedRef = local.detached ? local.current : null;
|
|
2956
3023
|
const current = local.detached ? "" : local.current;
|
|
2957
|
-
const branches = local.all.filter((name) => !name.startsWith("feature/") && name !== detachedRef);
|
|
3024
|
+
const branches = local.all.filter((name) => (options.includeFeatureBranches || !name.startsWith("feature/")) && name !== detachedRef);
|
|
2958
3025
|
const localSet = new Set(local.all);
|
|
2959
3026
|
let remoteBranches = [];
|
|
2960
3027
|
try {
|
|
@@ -2964,7 +3031,8 @@ async function listBranches(project) {
|
|
|
2964
3031
|
return !short.startsWith("feature/") && !localSet.has(short);
|
|
2965
3032
|
});
|
|
2966
3033
|
} catch {}
|
|
2967
|
-
|
|
3034
|
+
const detected = await detectMainBranch(project.repoPath);
|
|
3035
|
+
return { current, detected, branches, remoteBranches };
|
|
2968
3036
|
}
|
|
2969
3037
|
async function resolveBaseBranch(project, base) {
|
|
2970
3038
|
const g = git(project.repoPath);
|
|
@@ -3262,6 +3330,17 @@ async function reviewCommitCount(project, feature) {
|
|
|
3262
3330
|
return { base };
|
|
3263
3331
|
}
|
|
3264
3332
|
}
|
|
3333
|
+
async function mergeDelta(project, feature) {
|
|
3334
|
+
const counted = await reviewCommitCount(project, feature);
|
|
3335
|
+
const branch = featureBranch(feature.slug);
|
|
3336
|
+
try {
|
|
3337
|
+
const files = (await git(project.repoPath).raw(["diff", "--name-only", `${counted.base}...${branch}`])).split(`
|
|
3338
|
+
`).map((path) => path.trim()).filter(Boolean).length;
|
|
3339
|
+
return { base: counted.base, commits: counted.count, files };
|
|
3340
|
+
} catch {
|
|
3341
|
+
return { base: counted.base, commits: counted.count };
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3265
3344
|
async function featureBranchDelta(project, feature) {
|
|
3266
3345
|
const base = featureBase(feature);
|
|
3267
3346
|
const branch = featureBranch(feature.slug);
|
|
@@ -3545,6 +3624,18 @@ async function commitDocs(worktreePath, message) {
|
|
|
3545
3624
|
return;
|
|
3546
3625
|
await g.commit(message, [DOCS_PATHSPEC]);
|
|
3547
3626
|
}
|
|
3627
|
+
async function onBranch(repoPath, branch, action) {
|
|
3628
|
+
const g = git(repoPath);
|
|
3629
|
+
const previous = (await g.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
3630
|
+
if (previous !== branch)
|
|
3631
|
+
await g.checkout(branch);
|
|
3632
|
+
try {
|
|
3633
|
+
return await action();
|
|
3634
|
+
} finally {
|
|
3635
|
+
if (previous !== branch && previous !== "HEAD")
|
|
3636
|
+
await g.checkout(previous);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3548
3639
|
async function commitPipelineDocs(repoPath) {
|
|
3549
3640
|
try {
|
|
3550
3641
|
await commitDocs(repoPath, "runcastle: docs the pipeline left uncommitted");
|
|
@@ -3557,7 +3648,7 @@ function activeDriveInfo() {
|
|
|
3557
3648
|
const state = testDriveState;
|
|
3558
3649
|
if (!state)
|
|
3559
3650
|
return null;
|
|
3560
|
-
|
|
3651
|
+
const info = {
|
|
3561
3652
|
...state.kind === "feature" ? {
|
|
3562
3653
|
featureId: state.featureId,
|
|
3563
3654
|
purpose: state.purpose,
|
|
@@ -3571,6 +3662,7 @@ function activeDriveInfo() {
|
|
|
3571
3662
|
...state.devReadiness === "timedOut" ? { devReadyTimedOut: true } : {},
|
|
3572
3663
|
devConfigured: state.devConfigured
|
|
3573
3664
|
};
|
|
3665
|
+
return { ...info, state: deriveDriveState(info) };
|
|
3574
3666
|
}
|
|
3575
3667
|
function recordDriveUrl(ctx, featureId, url) {
|
|
3576
3668
|
if (testDriveState?.kind !== "feature")
|
|
@@ -5155,7 +5247,7 @@ async function sandcastleImageProbe(exec, imageName, burnerDockerfile, fileMtime
|
|
|
5155
5247
|
status: "stale",
|
|
5156
5248
|
severity: "error",
|
|
5157
5249
|
detail: `${imageName} built ${date(created)}, burner Dockerfile changed ${date(dockerfileChanged)} \u2014 rebuild`,
|
|
5158
|
-
fix:
|
|
5250
|
+
fix: "Open Settings \u2192 Burns (Rebuild image)."
|
|
5159
5251
|
};
|
|
5160
5252
|
}
|
|
5161
5253
|
return { id, label, tier: 2, status: "ok", severity: "error", detail: `${imageName} present` };
|
|
@@ -5785,7 +5877,7 @@ function markSessionEnded(ctx, id) {
|
|
|
5785
5877
|
forgetKickoff(id);
|
|
5786
5878
|
if (existing.featureId)
|
|
5787
5879
|
stopDocsWatch(existing.featureId);
|
|
5788
|
-
ctx.db.update(sessions).set({ status: "ended" }).where(eq5(sessions.id, id)).run();
|
|
5880
|
+
ctx.db.update(sessions).set({ status: "ended", ...existing.endedAt === undefined ? { endedAt: Date.now() } : {} }).where(eq5(sessions.id, id)).run();
|
|
5789
5881
|
return getSessionRow(ctx, id);
|
|
5790
5882
|
}
|
|
5791
5883
|
function mostRecentResumableSession(ctx, featureId, kind) {
|
|
@@ -5802,6 +5894,9 @@ function projectSessions(ctx, projectId, kind) {
|
|
|
5802
5894
|
function setSessionTitle(ctx, id, title) {
|
|
5803
5895
|
ctx.db.update(sessions).set({ title }).where(eq5(sessions.id, id)).run();
|
|
5804
5896
|
}
|
|
5897
|
+
function clearSessionTitle(ctx, id) {
|
|
5898
|
+
ctx.db.update(sessions).set({ title: null }).where(eq5(sessions.id, id)).run();
|
|
5899
|
+
}
|
|
5805
5900
|
function hasCompletedProjectSession(ctx, projectId, kind) {
|
|
5806
5901
|
return ctx.db.select({ id: sessions.id }).from(sessions).where(and5(eq5(sessions.projectId, projectId), eq5(sessions.kind, kind), eq5(sessions.status, "ended"))).limit(1).get() !== undefined;
|
|
5807
5902
|
}
|
|
@@ -5933,8 +6028,198 @@ async function getRuntimeCtx() {
|
|
|
5933
6028
|
}
|
|
5934
6029
|
var current = null;
|
|
5935
6030
|
|
|
6031
|
+
// src/services/review-findings.ts
|
|
6032
|
+
import { and as and6, asc as asc3, eq as eq6, isNotNull as isNotNull2 } from "drizzle-orm";
|
|
6033
|
+
function rowToFinding(row) {
|
|
6034
|
+
return ReviewFinding.parse(row);
|
|
6035
|
+
}
|
|
6036
|
+
function getFinding(ctx, id) {
|
|
6037
|
+
const row = ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.id, id)).get();
|
|
6038
|
+
if (!row)
|
|
6039
|
+
throw new NotFoundError(`review finding ${id} not found`);
|
|
6040
|
+
return rowToFinding(row);
|
|
6041
|
+
}
|
|
6042
|
+
function listByFeature3(ctx, featureId) {
|
|
6043
|
+
return ctx.db.select().from(reviewFindings).where(eq6(reviewFindings.featureId, featureId)).orderBy(asc3(reviewFindings.createdAt), asc3(reviewFindings.id)).all().map(rowToFinding);
|
|
6044
|
+
}
|
|
6045
|
+
function buildFixTicket(finding) {
|
|
6046
|
+
return {
|
|
6047
|
+
title: finding.title,
|
|
6048
|
+
goal: `Fix: ${finding.title}`,
|
|
6049
|
+
context: [
|
|
6050
|
+
`Location: ${finding.location}`,
|
|
6051
|
+
`Citation: ${finding.citation}`,
|
|
6052
|
+
`Detail: ${finding.detail}`,
|
|
6053
|
+
`Repro step: ${finding.reproStep}`
|
|
6054
|
+
].join(`
|
|
6055
|
+
|
|
6056
|
+
`),
|
|
6057
|
+
acceptanceCriteria: [
|
|
6058
|
+
`The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
|
|
6059
|
+
],
|
|
6060
|
+
seams: [],
|
|
6061
|
+
blockedBy: [],
|
|
6062
|
+
kind: "implementation",
|
|
6063
|
+
originFindingId: finding.id
|
|
6064
|
+
};
|
|
6065
|
+
}
|
|
6066
|
+
function reportFinding(ctx, args) {
|
|
6067
|
+
const { featureId, reviewTicket, input } = args;
|
|
6068
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
6069
|
+
const id = newId("finding");
|
|
6070
|
+
const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and6(eq6(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull2(reviewFindings.fixTicketId))).all().length;
|
|
6071
|
+
const verification = reviewTicket.passKind === "verification";
|
|
6072
|
+
const overCap = input.kind === "defect" && !verification && ticketCount >= AUTO_FIX_CAP;
|
|
6073
|
+
ctx.db.insert(reviewFindings).values({
|
|
6074
|
+
id,
|
|
6075
|
+
featureId,
|
|
6076
|
+
lap: feature.lap,
|
|
6077
|
+
reviewTicketId: reviewTicket.id,
|
|
6078
|
+
...input,
|
|
6079
|
+
reproStep: input.reproStep ?? "",
|
|
6080
|
+
status: "open",
|
|
6081
|
+
openReason: verification && input.kind === "defect" ? "verification" : overCap ? "over-cap" : null,
|
|
6082
|
+
failureReason: null,
|
|
6083
|
+
fixTicketId: null,
|
|
6084
|
+
createdAt: Date.now()
|
|
6085
|
+
}).run();
|
|
6086
|
+
let fixTicket = null;
|
|
6087
|
+
if (input.kind === "defect" && !overCap && !verification) {
|
|
6088
|
+
const finding2 = getFinding(ctx, id);
|
|
6089
|
+
fixTicket = storeTickets(ctx, featureId, [
|
|
6090
|
+
{ ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
|
|
6091
|
+
], { blockedByAreGlobal: true })[0];
|
|
6092
|
+
ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq6(reviewFindings.id, id)).run();
|
|
6093
|
+
}
|
|
6094
|
+
const finding = getFinding(ctx, id);
|
|
6095
|
+
emit(ctx, featureId, {
|
|
6096
|
+
type: "finding.reported",
|
|
6097
|
+
message: `${input.kind} reported: ${input.title}`,
|
|
6098
|
+
ticketId: fixTicket?.id,
|
|
6099
|
+
data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
|
|
6100
|
+
});
|
|
6101
|
+
return { finding, fixTicket, overCap };
|
|
6102
|
+
}
|
|
6103
|
+
function updateStatus(ctx, findingId, patch) {
|
|
6104
|
+
const current2 = getFinding(ctx, findingId);
|
|
6105
|
+
ctx.db.update(reviewFindings).set(patch).where(eq6(reviewFindings.id, findingId)).run();
|
|
6106
|
+
emit(ctx, current2.featureId, {
|
|
6107
|
+
type: "finding.updated",
|
|
6108
|
+
message: `finding ${findingId} marked ${patch.status}`,
|
|
6109
|
+
data: { findingId, status: patch.status, reason: patch.failureReason }
|
|
6110
|
+
});
|
|
6111
|
+
return getFinding(ctx, findingId);
|
|
6112
|
+
}
|
|
6113
|
+
function markFixing(ctx, findingId) {
|
|
6114
|
+
return updateStatus(ctx, findingId, {
|
|
6115
|
+
status: "fixing",
|
|
6116
|
+
openReason: null,
|
|
6117
|
+
failureReason: null
|
|
6118
|
+
});
|
|
6119
|
+
}
|
|
6120
|
+
function markFixed(ctx, findingId) {
|
|
6121
|
+
return updateStatus(ctx, findingId, {
|
|
6122
|
+
status: "fixed",
|
|
6123
|
+
openReason: null,
|
|
6124
|
+
failureReason: null
|
|
6125
|
+
});
|
|
6126
|
+
}
|
|
6127
|
+
function markFailed(ctx, findingId, reason) {
|
|
6128
|
+
return updateStatus(ctx, findingId, {
|
|
6129
|
+
status: "failed",
|
|
6130
|
+
openReason: "fix-failed",
|
|
6131
|
+
failureReason: reason ?? null
|
|
6132
|
+
});
|
|
6133
|
+
}
|
|
6134
|
+
function markFixProgress(ctx, findingId, progress, reason) {
|
|
6135
|
+
if (progress === "fixing")
|
|
6136
|
+
return markFixing(ctx, findingId);
|
|
6137
|
+
if (progress === "fixed")
|
|
6138
|
+
return markFixed(ctx, findingId);
|
|
6139
|
+
return markFailed(ctx, findingId, reason);
|
|
6140
|
+
}
|
|
6141
|
+
function dismiss(ctx, findingId) {
|
|
6142
|
+
return updateStatus(ctx, findingId, {
|
|
6143
|
+
status: "dismissed",
|
|
6144
|
+
openReason: null,
|
|
6145
|
+
failureReason: null
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6148
|
+
function defectState(finding, fixTicket) {
|
|
6149
|
+
if (finding.status === "dismissed")
|
|
6150
|
+
return "dismissed";
|
|
6151
|
+
if (finding.status === "fixed" || fixTicket?.status === "done")
|
|
6152
|
+
return "fixed";
|
|
6153
|
+
if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
|
|
6154
|
+
return "fixing";
|
|
6155
|
+
}
|
|
6156
|
+
return finding.status === "open" || finding.status === "failed" || fixTicket?.status === "failed" || fixTicket?.status === "cancelled" ? "open" : "fixing";
|
|
6157
|
+
}
|
|
6158
|
+
function viewByFeature(ctx, featureId) {
|
|
6159
|
+
const findings = listByFeature3(ctx, featureId);
|
|
6160
|
+
const tickets2 = listByFeature2(ctx, featureId);
|
|
6161
|
+
const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
|
|
6162
|
+
const openDefects = [];
|
|
6163
|
+
for (const finding of findings) {
|
|
6164
|
+
if (finding.kind === "observation") {
|
|
6165
|
+
summary.observations += 1;
|
|
6166
|
+
continue;
|
|
6167
|
+
}
|
|
6168
|
+
summary.found += 1;
|
|
6169
|
+
const state = defectState(finding, fixTicketOf(finding, tickets2));
|
|
6170
|
+
if (state === "fixed")
|
|
6171
|
+
summary.fixed += 1;
|
|
6172
|
+
else if (state === "open") {
|
|
6173
|
+
summary.open += 1;
|
|
6174
|
+
openDefects.push(finding);
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
return { findings, openDefects, summary };
|
|
6178
|
+
}
|
|
6179
|
+
function fixTicketOf(finding, tickets2) {
|
|
6180
|
+
return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
|
|
6181
|
+
}
|
|
6182
|
+
function promoteOpenDefects(ctx, featureId, findingIds) {
|
|
6183
|
+
const open = viewByFeature(ctx, featureId).openDefects;
|
|
6184
|
+
const defects = findingIds ? findingIds.map((id) => {
|
|
6185
|
+
const finding = open.find((candidate) => candidate.id === id);
|
|
6186
|
+
if (!finding)
|
|
6187
|
+
throw new InvalidInputError(`finding ${id} is not an open defect of this feature`);
|
|
6188
|
+
return finding;
|
|
6189
|
+
}) : open;
|
|
6190
|
+
if (findingIds && new Set(findingIds).size !== findingIds.length)
|
|
6191
|
+
throw new InvalidInputError("duplicate finding selected");
|
|
6192
|
+
if (defects.length === 0)
|
|
6193
|
+
throw new InvalidInputError("no open defects to fix");
|
|
6194
|
+
const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
|
|
6195
|
+
const findings = defects.map((defect, index2) => {
|
|
6196
|
+
ctx.db.update(reviewFindings).set({
|
|
6197
|
+
status: "fixing",
|
|
6198
|
+
openReason: null,
|
|
6199
|
+
failureReason: null,
|
|
6200
|
+
fixTicketId: tickets2[index2].id
|
|
6201
|
+
}).where(eq6(reviewFindings.id, defect.id)).run();
|
|
6202
|
+
return getFinding(ctx, defect.id);
|
|
6203
|
+
});
|
|
6204
|
+
emit(ctx, featureId, {
|
|
6205
|
+
type: "finding.fixing",
|
|
6206
|
+
message: `${findings.length} open defect${findings.length === 1 ? "" : "s"} promoted to fix ticket${tickets2.length === 1 ? ` ${tickets2[0].seq}` : `s ${tickets2.map((t) => t.seq).join(", ")}`}`,
|
|
6207
|
+
data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
|
|
6208
|
+
});
|
|
6209
|
+
return { findings, tickets: tickets2 };
|
|
6210
|
+
}
|
|
6211
|
+
var AUTO_FIX_CAP = 8;
|
|
6212
|
+
var init_review_findings = __esm(() => {
|
|
6213
|
+
init_src();
|
|
6214
|
+
init_schema();
|
|
6215
|
+
init_errors();
|
|
6216
|
+
init_events();
|
|
6217
|
+
init_repo();
|
|
6218
|
+
init_tickets();
|
|
6219
|
+
});
|
|
6220
|
+
|
|
5936
6221
|
// src/services/tickets.ts
|
|
5937
|
-
import { and as
|
|
6222
|
+
import { and as and7, asc as asc4, eq as eq7, inArray as inArray3 } from "drizzle-orm";
|
|
5938
6223
|
function rowToTicket(row) {
|
|
5939
6224
|
return Ticket.parse({
|
|
5940
6225
|
id: row.id,
|
|
@@ -5947,6 +6232,9 @@ function rowToTicket(row) {
|
|
|
5947
6232
|
seams: row.seams,
|
|
5948
6233
|
blockedBy: row.blockedBy,
|
|
5949
6234
|
kind: row.kind,
|
|
6235
|
+
passKind: row.passKind,
|
|
6236
|
+
reviewedCommit: row.reviewedCommit,
|
|
6237
|
+
completedAt: row.completedAt,
|
|
5950
6238
|
model: row.model ?? undefined,
|
|
5951
6239
|
lap: row.lap,
|
|
5952
6240
|
status: row.status,
|
|
@@ -5978,13 +6266,13 @@ function normalizeModel(ctx, value) {
|
|
|
5978
6266
|
return id;
|
|
5979
6267
|
}
|
|
5980
6268
|
function listByFeature2(ctx, featureId) {
|
|
5981
|
-
return ctx.db.select().from(tickets).where(
|
|
6269
|
+
return ctx.db.select().from(tickets).where(eq7(tickets.featureId, featureId)).orderBy(asc4(tickets.seq)).all().map(rowToTicket);
|
|
5982
6270
|
}
|
|
5983
6271
|
function storeTickets(ctx, featureId, inputs, options = {}) {
|
|
5984
6272
|
if (inputs.length === 0)
|
|
5985
6273
|
return [];
|
|
5986
6274
|
const { lap } = getFeatureRow(ctx, featureId);
|
|
5987
|
-
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(
|
|
6275
|
+
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
|
|
5988
6276
|
const startSeq = existing.reduce((max2, r) => Math.max(max2, r.seq), 0) + 1;
|
|
5989
6277
|
const resolved = options.blockedByAreGlobal ? inputs.map((input, index2) => ({ seq: startSeq + index2, blockedBy: input.blockedBy })) : resolveBlocking(inputs, startSeq);
|
|
5990
6278
|
const rows = inputs.map((t, i) => ({
|
|
@@ -5998,6 +6286,9 @@ function storeTickets(ctx, featureId, inputs, options = {}) {
|
|
|
5998
6286
|
seams: t.seams,
|
|
5999
6287
|
blockedBy: resolved[i].blockedBy,
|
|
6000
6288
|
kind: t.kind ?? "implementation",
|
|
6289
|
+
passKind: t.passKind ?? "review",
|
|
6290
|
+
reviewedCommit: null,
|
|
6291
|
+
completedAt: null,
|
|
6001
6292
|
model: normalizeModel(ctx, t.model),
|
|
6002
6293
|
lap,
|
|
6003
6294
|
status: "pending",
|
|
@@ -6017,7 +6308,7 @@ function storeTickets(ctx, featureId, inputs, options = {}) {
|
|
|
6017
6308
|
return rows.map(rowToTicket);
|
|
6018
6309
|
}
|
|
6019
6310
|
function getTicket(ctx, id) {
|
|
6020
|
-
const row = ctx.db.select().from(tickets).where(
|
|
6311
|
+
const row = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
6021
6312
|
if (!row)
|
|
6022
6313
|
throw new NotFoundError(`ticket ${id} not found`);
|
|
6023
6314
|
return rowToTicket(row);
|
|
@@ -6046,7 +6337,7 @@ function editTicket(ctx, id, patch) {
|
|
|
6046
6337
|
if (Object.keys(set).length === 0) {
|
|
6047
6338
|
throw new InvalidInputError("nothing to edit \u2014 pass at least one field");
|
|
6048
6339
|
}
|
|
6049
|
-
ctx.db.update(tickets).set(set).where(
|
|
6340
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
6050
6341
|
emit(ctx, current2.featureId, {
|
|
6051
6342
|
type: "ticket.edited",
|
|
6052
6343
|
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
@@ -6058,7 +6349,11 @@ function editTicket(ctx, id, patch) {
|
|
|
6058
6349
|
function cancelTicket(ctx, id, reason) {
|
|
6059
6350
|
const current2 = getTicket(ctx, id);
|
|
6060
6351
|
assertMutable(current2, "cancel");
|
|
6061
|
-
ctx.db.update(tickets).set({
|
|
6352
|
+
ctx.db.update(tickets).set({
|
|
6353
|
+
status: "cancelled",
|
|
6354
|
+
error: reason?.trim() ? reason.trim() : null,
|
|
6355
|
+
completedAt: Date.now()
|
|
6356
|
+
}).where(eq7(tickets.id, id)).run();
|
|
6062
6357
|
emit(ctx, current2.featureId, {
|
|
6063
6358
|
type: "ticket.cancelled",
|
|
6064
6359
|
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
@@ -6068,9 +6363,12 @@ function cancelTicket(ctx, id, reason) {
|
|
|
6068
6363
|
return getTicket(ctx, id);
|
|
6069
6364
|
}
|
|
6070
6365
|
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
6071
|
-
const orphaned = ctx.db.select().from(tickets).where(
|
|
6366
|
+
const orphaned = ctx.db.select().from(tickets).where(and7(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
|
|
6367
|
+
const swept = [];
|
|
6072
6368
|
for (const t of orphaned) {
|
|
6073
|
-
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
6369
|
+
swept.push(updateTicket(ctx, t.id, { status: "failed", error: reason }));
|
|
6370
|
+
if (t.originFindingId)
|
|
6371
|
+
markFailed(ctx, t.originFindingId, reason);
|
|
6074
6372
|
emit(ctx, featureId, {
|
|
6075
6373
|
type: "ticket.failed",
|
|
6076
6374
|
message: `ticket ${t.seq} failed: ${reason}`,
|
|
@@ -6078,15 +6376,18 @@ function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
|
6078
6376
|
data: { error: reason, orphaned: true }
|
|
6079
6377
|
});
|
|
6080
6378
|
}
|
|
6081
|
-
return
|
|
6379
|
+
return swept;
|
|
6082
6380
|
}
|
|
6083
6381
|
function updateTicket(ctx, id, patch) {
|
|
6084
|
-
const current2 = ctx.db.select().from(tickets).where(
|
|
6382
|
+
const current2 = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
6085
6383
|
if (!current2)
|
|
6086
6384
|
throw new NotFoundError(`ticket ${id} not found`);
|
|
6087
6385
|
const set = {};
|
|
6088
6386
|
if (patch.status !== undefined)
|
|
6089
6387
|
set.status = patch.status;
|
|
6388
|
+
if (patch.status !== undefined) {
|
|
6389
|
+
set.completedAt = ["done", "failed", "cancelled"].includes(patch.status) ? Date.now() : null;
|
|
6390
|
+
}
|
|
6090
6391
|
if (patch.commits !== undefined)
|
|
6091
6392
|
set.commits = patch.commits;
|
|
6092
6393
|
if (patch.error !== undefined)
|
|
@@ -6097,16 +6398,44 @@ function updateTicket(ctx, id, patch) {
|
|
|
6097
6398
|
set.conflictFiles = patch.conflictFiles;
|
|
6098
6399
|
if (patch.digest !== undefined)
|
|
6099
6400
|
set.digest = patch.digest;
|
|
6100
|
-
|
|
6401
|
+
if (patch.reviewedCommit !== undefined)
|
|
6402
|
+
set.reviewedCommit = patch.reviewedCommit;
|
|
6403
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
6101
6404
|
emit(ctx, current2.featureId, {
|
|
6102
6405
|
type: "ticket.updated",
|
|
6103
6406
|
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
6104
6407
|
ticketId: id,
|
|
6105
6408
|
data: patch
|
|
6106
6409
|
});
|
|
6107
|
-
const updated = ctx.db.select().from(tickets).where(
|
|
6410
|
+
const updated = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
6108
6411
|
return rowToTicket(updated);
|
|
6109
6412
|
}
|
|
6413
|
+
function listByIds(ctx, ids2) {
|
|
6414
|
+
if (ids2.length === 0)
|
|
6415
|
+
return [];
|
|
6416
|
+
return ctx.db.select().from(tickets).where(inArray3(tickets.id, [...ids2])).orderBy(asc4(tickets.seq)).all().map(rowToTicket);
|
|
6417
|
+
}
|
|
6418
|
+
function timingWallMs(data) {
|
|
6419
|
+
if (typeof data !== "object" || data === null)
|
|
6420
|
+
return;
|
|
6421
|
+
const ms = data.wallMs;
|
|
6422
|
+
return typeof ms === "number" && Number.isFinite(ms) && ms > 0 ? ms : undefined;
|
|
6423
|
+
}
|
|
6424
|
+
function ticketDurationStats(ctx, projectId) {
|
|
6425
|
+
const rows = ctx.db.select({ ticketId: events.ticketId, data: events.data }).from(events).innerJoin(tickets, eq7(events.ticketId, tickets.id)).where(and7(eq7(events.projectId, projectId), eq7(events.type, "ticket.timing"), eq7(tickets.status, "done"), eq7(tickets.kind, "implementation"))).orderBy(asc4(events.id)).all();
|
|
6426
|
+
const perTicket = new Map;
|
|
6427
|
+
for (const row of rows) {
|
|
6428
|
+
const ms = timingWallMs(row.data);
|
|
6429
|
+
if (row.ticketId && ms !== undefined)
|
|
6430
|
+
perTicket.set(row.ticketId, ms);
|
|
6431
|
+
}
|
|
6432
|
+
const samples = [...perTicket.values()].sort((a, b) => a - b);
|
|
6433
|
+
if (samples.length === 0)
|
|
6434
|
+
return { medianMs: 0, sampleSize: 0 };
|
|
6435
|
+
const mid = samples.length >> 1;
|
|
6436
|
+
const medianMs = samples.length % 2 === 1 ? samples[mid] : Math.round((samples[mid - 1] + samples[mid]) / 2);
|
|
6437
|
+
return { medianMs, sampleSize: samples.length };
|
|
6438
|
+
}
|
|
6110
6439
|
var MUTABLE_STATUSES;
|
|
6111
6440
|
var init_tickets = __esm(() => {
|
|
6112
6441
|
init_src();
|
|
@@ -6114,12 +6443,13 @@ var init_tickets = __esm(() => {
|
|
|
6114
6443
|
init_errors();
|
|
6115
6444
|
init_events();
|
|
6116
6445
|
init_repo();
|
|
6446
|
+
init_review_findings();
|
|
6117
6447
|
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
6118
6448
|
});
|
|
6119
6449
|
|
|
6120
6450
|
// src/services/gates.ts
|
|
6121
6451
|
import { existsSync as existsSync11 } from "fs";
|
|
6122
|
-
import { and as
|
|
6452
|
+
import { and as and8, desc as desc4, eq as eq8, inArray as inArray4 } from "drizzle-orm";
|
|
6123
6453
|
function checkGate(ctx, check, feature) {
|
|
6124
6454
|
switch (check) {
|
|
6125
6455
|
case "decisions-file-exists":
|
|
@@ -6130,10 +6460,17 @@ function checkGate(ctx, check, feature) {
|
|
|
6130
6460
|
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
6131
6461
|
}
|
|
6132
6462
|
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6463
|
+
if (open.length > 0)
|
|
6464
|
+
return {
|
|
6465
|
+
satisfied: false,
|
|
6466
|
+
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
6467
|
+
};
|
|
6468
|
+
if (feature.lap > 1 && !lapWorked(ctx, feature))
|
|
6469
|
+
return {
|
|
6470
|
+
satisfied: false,
|
|
6471
|
+
reason: `the map is an earlier lap's \u2014 no lap ${feature.lap} session has worked this feature yet; open the lap's session before converging`
|
|
6472
|
+
};
|
|
6473
|
+
return { satisfied: true };
|
|
6137
6474
|
}
|
|
6138
6475
|
case "spec-file-exists":
|
|
6139
6476
|
return docGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
@@ -6170,12 +6507,14 @@ function fileGate(ctx, feature, fileName, reason) {
|
|
|
6170
6507
|
const project = projectForFeature(ctx, feature);
|
|
6171
6508
|
return existsSync11(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
6172
6509
|
}
|
|
6510
|
+
function lapWorked(ctx, feature) {
|
|
6511
|
+
return ctx.db.select({ id: sessions.id }).from(sessions).where(and8(eq8(sessions.featureId, feature.id), eq8(sessions.lap, feature.lap), inArray4(sessions.kind, [...DOC_WRITING_KINDS]))).all().length > 0;
|
|
6512
|
+
}
|
|
6173
6513
|
function docGate(ctx, feature, fileName, reason) {
|
|
6174
6514
|
const file = fileGate(ctx, feature, fileName, reason);
|
|
6175
6515
|
if (!file.satisfied || feature.lap === 1)
|
|
6176
6516
|
return file;
|
|
6177
|
-
|
|
6178
|
-
return worked.length > 0 ? { satisfied: true } : {
|
|
6517
|
+
return lapWorked(ctx, feature) ? { satisfied: true } : {
|
|
6179
6518
|
satisfied: false,
|
|
6180
6519
|
reason: `${fileName} is an earlier lap's \u2014 no lap ${feature.lap} session has worked this feature yet; open the lap's session before promoting`
|
|
6181
6520
|
};
|
|
@@ -6201,10 +6540,10 @@ function overrideGate(ctx, featureId, gate, reason) {
|
|
|
6201
6540
|
return feature;
|
|
6202
6541
|
}
|
|
6203
6542
|
function undoLastGateOverride(ctx, featureId, gate) {
|
|
6204
|
-
const last = ctx.db.select({ id: gateOverrides.id }).from(gateOverrides).where(
|
|
6543
|
+
const last = ctx.db.select({ id: gateOverrides.id }).from(gateOverrides).where(and8(eq8(gateOverrides.featureId, featureId), eq8(gateOverrides.gate, gate))).orderBy(desc4(gateOverrides.id)).get();
|
|
6205
6544
|
if (!last)
|
|
6206
6545
|
return false;
|
|
6207
|
-
ctx.db.delete(gateOverrides).where(
|
|
6546
|
+
ctx.db.delete(gateOverrides).where(eq8(gateOverrides.id, last.id)).run();
|
|
6208
6547
|
return true;
|
|
6209
6548
|
}
|
|
6210
6549
|
function undoGateOverride(ctx, featureId, gate) {
|
|
@@ -6232,7 +6571,7 @@ var init_gates = __esm(() => {
|
|
|
6232
6571
|
});
|
|
6233
6572
|
|
|
6234
6573
|
// src/services/knowledge.ts
|
|
6235
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6574
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
6236
6575
|
import { join as join11, relative as relative2, resolve as resolve8, sep } from "path";
|
|
6237
6576
|
function scaffoldDocs(ctx, feature, opts = {}) {
|
|
6238
6577
|
const project = projectForFeature(ctx, feature);
|
|
@@ -6320,7 +6659,10 @@ function listDocs(ctx, feature) {
|
|
|
6320
6659
|
const dir = featureDocsDir(project, feature);
|
|
6321
6660
|
if (!existsSync12(dir))
|
|
6322
6661
|
return [];
|
|
6323
|
-
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) =>
|
|
6662
|
+
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => {
|
|
6663
|
+
const path = join11(dir, e.name);
|
|
6664
|
+
return { relPath: e.name, title: readTitle(path, e.name), ...writtenAt(path) };
|
|
6665
|
+
}).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
6324
6666
|
}
|
|
6325
6667
|
function readDoc(ctx, feature, relPath) {
|
|
6326
6668
|
const project = projectForFeature(ctx, feature);
|
|
@@ -6335,6 +6677,13 @@ function readDoc(ctx, feature, relPath) {
|
|
|
6335
6677
|
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
6336
6678
|
return { content: readFileSync5(target, "utf8") };
|
|
6337
6679
|
}
|
|
6680
|
+
function writtenAt(path) {
|
|
6681
|
+
try {
|
|
6682
|
+
return { updatedAt: Math.floor(statSync4(path).mtimeMs) };
|
|
6683
|
+
} catch {
|
|
6684
|
+
return {};
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6338
6687
|
function readTitle(path, fallback) {
|
|
6339
6688
|
try {
|
|
6340
6689
|
const firstLine = readFileSync5(path, "utf8").split(`
|
|
@@ -6388,187 +6737,6 @@ var init_end_session = __esm(() => {
|
|
|
6388
6737
|
init_registry();
|
|
6389
6738
|
});
|
|
6390
6739
|
|
|
6391
|
-
// src/services/review-findings.ts
|
|
6392
|
-
import { and as and8, asc as asc4, eq as eq8, isNotNull as isNotNull2 } from "drizzle-orm";
|
|
6393
|
-
function rowToFinding(row) {
|
|
6394
|
-
return ReviewFinding.parse(row);
|
|
6395
|
-
}
|
|
6396
|
-
function getFinding(ctx, id) {
|
|
6397
|
-
const row = ctx.db.select().from(reviewFindings).where(eq8(reviewFindings.id, id)).get();
|
|
6398
|
-
if (!row)
|
|
6399
|
-
throw new NotFoundError(`review finding ${id} not found`);
|
|
6400
|
-
return rowToFinding(row);
|
|
6401
|
-
}
|
|
6402
|
-
function listByFeature3(ctx, featureId) {
|
|
6403
|
-
return ctx.db.select().from(reviewFindings).where(eq8(reviewFindings.featureId, featureId)).orderBy(asc4(reviewFindings.createdAt), asc4(reviewFindings.id)).all().map(rowToFinding);
|
|
6404
|
-
}
|
|
6405
|
-
function buildFixTicket(finding) {
|
|
6406
|
-
return {
|
|
6407
|
-
title: finding.title,
|
|
6408
|
-
goal: `Fix: ${finding.title}`,
|
|
6409
|
-
context: [
|
|
6410
|
-
`Location: ${finding.location}`,
|
|
6411
|
-
`Citation: ${finding.citation}`,
|
|
6412
|
-
`Detail: ${finding.detail}`,
|
|
6413
|
-
`Repro step: ${finding.reproStep}`
|
|
6414
|
-
].join(`
|
|
6415
|
-
|
|
6416
|
-
`),
|
|
6417
|
-
acceptanceCriteria: [
|
|
6418
|
-
`The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
|
|
6419
|
-
],
|
|
6420
|
-
seams: [],
|
|
6421
|
-
blockedBy: [],
|
|
6422
|
-
kind: "implementation",
|
|
6423
|
-
originFindingId: finding.id
|
|
6424
|
-
};
|
|
6425
|
-
}
|
|
6426
|
-
function reportFinding(ctx, args) {
|
|
6427
|
-
const { featureId, reviewTicket, input } = args;
|
|
6428
|
-
const feature = getFeatureRow(ctx, featureId);
|
|
6429
|
-
const id = newId("finding");
|
|
6430
|
-
const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and8(eq8(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull2(reviewFindings.fixTicketId))).all().length;
|
|
6431
|
-
const overCap = input.kind === "defect" && ticketCount >= AUTO_FIX_CAP;
|
|
6432
|
-
ctx.db.insert(reviewFindings).values({
|
|
6433
|
-
id,
|
|
6434
|
-
featureId,
|
|
6435
|
-
lap: feature.lap,
|
|
6436
|
-
reviewTicketId: reviewTicket.id,
|
|
6437
|
-
...input,
|
|
6438
|
-
reproStep: input.reproStep ?? "",
|
|
6439
|
-
status: "open",
|
|
6440
|
-
openReason: overCap ? "over-cap" : null,
|
|
6441
|
-
failureReason: null,
|
|
6442
|
-
fixTicketId: null,
|
|
6443
|
-
createdAt: Date.now()
|
|
6444
|
-
}).run();
|
|
6445
|
-
let fixTicket = null;
|
|
6446
|
-
if (input.kind === "defect" && !overCap) {
|
|
6447
|
-
const finding2 = getFinding(ctx, id);
|
|
6448
|
-
fixTicket = storeTickets(ctx, featureId, [
|
|
6449
|
-
{ ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
|
|
6450
|
-
], { blockedByAreGlobal: true })[0];
|
|
6451
|
-
ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq8(reviewFindings.id, id)).run();
|
|
6452
|
-
}
|
|
6453
|
-
const finding = getFinding(ctx, id);
|
|
6454
|
-
emit(ctx, featureId, {
|
|
6455
|
-
type: "finding.reported",
|
|
6456
|
-
message: `${input.kind} reported: ${input.title}`,
|
|
6457
|
-
ticketId: fixTicket?.id,
|
|
6458
|
-
data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
|
|
6459
|
-
});
|
|
6460
|
-
return { finding, fixTicket, overCap };
|
|
6461
|
-
}
|
|
6462
|
-
function updateStatus(ctx, findingId, patch) {
|
|
6463
|
-
const current2 = getFinding(ctx, findingId);
|
|
6464
|
-
ctx.db.update(reviewFindings).set(patch).where(eq8(reviewFindings.id, findingId)).run();
|
|
6465
|
-
emit(ctx, current2.featureId, {
|
|
6466
|
-
type: "finding.updated",
|
|
6467
|
-
message: `finding ${findingId} marked ${patch.status}`,
|
|
6468
|
-
data: { findingId, status: patch.status, reason: patch.failureReason }
|
|
6469
|
-
});
|
|
6470
|
-
return getFinding(ctx, findingId);
|
|
6471
|
-
}
|
|
6472
|
-
function markFixing(ctx, findingId) {
|
|
6473
|
-
return updateStatus(ctx, findingId, {
|
|
6474
|
-
status: "fixing",
|
|
6475
|
-
openReason: null,
|
|
6476
|
-
failureReason: null
|
|
6477
|
-
});
|
|
6478
|
-
}
|
|
6479
|
-
function markFixed(ctx, findingId) {
|
|
6480
|
-
return updateStatus(ctx, findingId, {
|
|
6481
|
-
status: "fixed",
|
|
6482
|
-
openReason: null,
|
|
6483
|
-
failureReason: null
|
|
6484
|
-
});
|
|
6485
|
-
}
|
|
6486
|
-
function markFailed(ctx, findingId, reason) {
|
|
6487
|
-
return updateStatus(ctx, findingId, {
|
|
6488
|
-
status: "failed",
|
|
6489
|
-
openReason: "fix-failed",
|
|
6490
|
-
failureReason: reason ?? null
|
|
6491
|
-
});
|
|
6492
|
-
}
|
|
6493
|
-
function markFixProgress(ctx, findingId, progress, reason) {
|
|
6494
|
-
if (progress === "fixing")
|
|
6495
|
-
return markFixing(ctx, findingId);
|
|
6496
|
-
if (progress === "fixed")
|
|
6497
|
-
return markFixed(ctx, findingId);
|
|
6498
|
-
return markFailed(ctx, findingId, reason);
|
|
6499
|
-
}
|
|
6500
|
-
function dismiss(ctx, findingId) {
|
|
6501
|
-
return updateStatus(ctx, findingId, {
|
|
6502
|
-
status: "dismissed",
|
|
6503
|
-
openReason: null,
|
|
6504
|
-
failureReason: null
|
|
6505
|
-
});
|
|
6506
|
-
}
|
|
6507
|
-
function defectState(finding, fixTicket) {
|
|
6508
|
-
if (finding.status === "dismissed")
|
|
6509
|
-
return "dismissed";
|
|
6510
|
-
if (finding.status === "fixed" || fixTicket?.status === "done")
|
|
6511
|
-
return "fixed";
|
|
6512
|
-
if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
|
|
6513
|
-
return "fixing";
|
|
6514
|
-
}
|
|
6515
|
-
return finding.status === "open" || finding.status === "failed" ? "open" : "fixing";
|
|
6516
|
-
}
|
|
6517
|
-
function viewByFeature(ctx, featureId) {
|
|
6518
|
-
const findings = listByFeature3(ctx, featureId);
|
|
6519
|
-
const tickets2 = listByFeature2(ctx, featureId);
|
|
6520
|
-
const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
|
|
6521
|
-
const openDefects = [];
|
|
6522
|
-
for (const finding of findings) {
|
|
6523
|
-
if (finding.kind === "observation") {
|
|
6524
|
-
summary.observations += 1;
|
|
6525
|
-
continue;
|
|
6526
|
-
}
|
|
6527
|
-
summary.found += 1;
|
|
6528
|
-
const state = defectState(finding, fixTicketOf(finding, tickets2));
|
|
6529
|
-
if (state === "fixed")
|
|
6530
|
-
summary.fixed += 1;
|
|
6531
|
-
else if (state === "open") {
|
|
6532
|
-
summary.open += 1;
|
|
6533
|
-
openDefects.push(finding);
|
|
6534
|
-
}
|
|
6535
|
-
}
|
|
6536
|
-
return { findings, openDefects, summary };
|
|
6537
|
-
}
|
|
6538
|
-
function fixTicketOf(finding, tickets2) {
|
|
6539
|
-
return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
|
|
6540
|
-
}
|
|
6541
|
-
function promoteOpenDefects(ctx, featureId) {
|
|
6542
|
-
const defects = viewByFeature(ctx, featureId).openDefects;
|
|
6543
|
-
if (defects.length === 0)
|
|
6544
|
-
throw new InvalidInputError("no open defects to fix");
|
|
6545
|
-
const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
|
|
6546
|
-
const findings = defects.map((defect, index2) => {
|
|
6547
|
-
ctx.db.update(reviewFindings).set({
|
|
6548
|
-
status: "fixing",
|
|
6549
|
-
openReason: null,
|
|
6550
|
-
failureReason: null,
|
|
6551
|
-
fixTicketId: tickets2[index2].id
|
|
6552
|
-
}).where(eq8(reviewFindings.id, defect.id)).run();
|
|
6553
|
-
return getFinding(ctx, defect.id);
|
|
6554
|
-
});
|
|
6555
|
-
emit(ctx, featureId, {
|
|
6556
|
-
type: "finding.fixing",
|
|
6557
|
-
message: `${findings.length} open defect${findings.length === 1 ? "" : "s"} promoted to fix ticket${tickets2.length === 1 ? ` ${tickets2[0].seq}` : `s ${tickets2.map((t) => t.seq).join(", ")}`}`,
|
|
6558
|
-
data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
|
|
6559
|
-
});
|
|
6560
|
-
return { findings, tickets: tickets2 };
|
|
6561
|
-
}
|
|
6562
|
-
var AUTO_FIX_CAP = 8;
|
|
6563
|
-
var init_review_findings = __esm(() => {
|
|
6564
|
-
init_src();
|
|
6565
|
-
init_schema();
|
|
6566
|
-
init_errors();
|
|
6567
|
-
init_events();
|
|
6568
|
-
init_repo();
|
|
6569
|
-
init_tickets();
|
|
6570
|
-
});
|
|
6571
|
-
|
|
6572
6740
|
// ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/chunk-NGBM7T3E.js
|
|
6573
6741
|
import { createRequire as createRequire3 } from "module";
|
|
6574
6742
|
var require$1, __create, __defProp2, __getOwnPropDesc, __getOwnPropNames, __getProtoOf, __hasOwnProp, __require2, __commonJS = (cb, mod) => function __require22() {
|
|
@@ -6628,7 +6796,7 @@ import { createRequire as createRequire4 } from "module";
|
|
|
6628
6796
|
import * as clack from "@clack/prompts";
|
|
6629
6797
|
import { join as join$1, basename as basename2, dirname as dirname4, resolve as resolve9, posix as posix2, normalize, isAbsolute as isAbsolute2 } from "path";
|
|
6630
6798
|
import { styleText, promisify } from "util";
|
|
6631
|
-
import { existsSync as existsSync13, statSync as
|
|
6799
|
+
import { existsSync as existsSync13, statSync as statSync5 } from "fs";
|
|
6632
6800
|
import { tmpdir as tmpdir2, homedir as homedir3 } from "os";
|
|
6633
6801
|
import { mkdtemp, writeFile, mkdir, readFile, rm, readdir, stat } from "fs/promises";
|
|
6634
6802
|
import { exec, execFile } from "child_process";
|
|
@@ -18440,7 +18608,7 @@ Worktree removed (no uncommitted changes)`);
|
|
|
18440
18608
|
const selinux = mount.volume !== undefined ? undefined : selinuxLabel || undefined;
|
|
18441
18609
|
const options = [mount.readonly ? "ro" : undefined, selinux].filter((option5) => option5 !== undefined).join(",");
|
|
18442
18610
|
return options ? `${base}:${options}` : base;
|
|
18443
|
-
}, processFileMountParents = (mounts, sandboxHomedir, statFn =
|
|
18611
|
+
}, processFileMountParents = (mounts, sandboxHomedir, statFn = statSync5) => {
|
|
18444
18612
|
const parentDirs = /* @__PURE__ */ new Set;
|
|
18445
18613
|
for (const mount of mounts) {
|
|
18446
18614
|
let isFile2;
|
|
@@ -55378,10 +55546,11 @@ var init_no_sandbox = __esm(() => {
|
|
|
55378
55546
|
// src/workflows/review-ticket.ts
|
|
55379
55547
|
import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
55380
55548
|
import { delimiter, join as join16 } from "path";
|
|
55381
|
-
function reviewTemplatePath() {
|
|
55382
|
-
return burnerAssetPath("review-ticket.md");
|
|
55549
|
+
function reviewTemplatePath(ticket) {
|
|
55550
|
+
return burnerAssetPath(ticket?.passKind === "verification" ? "verify-fixes.md" : "review-ticket.md");
|
|
55383
55551
|
}
|
|
55384
|
-
function renderReviewPrompt(
|
|
55552
|
+
function renderReviewPrompt(templateOrTicket, values) {
|
|
55553
|
+
const template = typeof templateOrTicket === "string" ? templateOrTicket : readFileSync8(reviewTemplatePath(templateOrTicket), "utf8");
|
|
55385
55554
|
return renderTemplate(template, values);
|
|
55386
55555
|
}
|
|
55387
55556
|
function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
@@ -55396,7 +55565,10 @@ function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
|
55396
55565
|
}
|
|
55397
55566
|
return;
|
|
55398
55567
|
}
|
|
55399
|
-
function buildDriveAvailability(browserPath, devCommand) {
|
|
55568
|
+
function buildDriveAvailability(browserPath, devCommand, inheritedMode) {
|
|
55569
|
+
if (inheritedMode) {
|
|
55570
|
+
return inheritedMode === "drive" ? "Inherited mode: **Drive**. The pass being verified left a recording, so run Drive mode and record the full tour; do not choose Gates mode." : "Inherited mode: **Gates**. The pass being verified left no recording, so run Gates mode; do not choose Drive mode or call `review_drive`.";
|
|
55571
|
+
}
|
|
55400
55572
|
const missing = [];
|
|
55401
55573
|
if (!browserPath) {
|
|
55402
55574
|
missing.push(`\`${AGENT_BROWSER_BIN}\` is not on this machine's PATH, so there is no browser to walk the app with`);
|
|
@@ -55409,6 +55581,9 @@ function buildDriveAvailability(browserPath, devCommand) {
|
|
|
55409
55581
|
}
|
|
55410
55582
|
return `A drive is **not** available: ${missing.join(", and ")}. So the mode is already decided \u2014 ` + "run Gates mode, whatever this lap touched, and do not call `review_drive`. This is not a " + "degraded review; it is the whole review this lap gets.";
|
|
55411
55583
|
}
|
|
55584
|
+
function inheritedReviewMode(verifiedTicketId, fileExists2 = existsSync16) {
|
|
55585
|
+
return verifiedTicketId && fileExists2(reviewWalkthroughPath(verifiedTicketId)) ? "drive" : "gates";
|
|
55586
|
+
}
|
|
55412
55587
|
function buildGateNotes(config2) {
|
|
55413
55588
|
const commands = config2.verifyCommands?.trim();
|
|
55414
55589
|
const failures2 = config2.knownFailures?.trim();
|
|
@@ -55462,18 +55637,24 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
|
|
|
55462
55637
|
return couldNotReview(ticket, `feature ${feature.slug} has no recorded base branch, so there is nothing to diff \`${feature.branch}\` against.`);
|
|
55463
55638
|
}
|
|
55464
55639
|
const artifacts = writeReviewArtifacts(ticket, ctx.runId, deps.config);
|
|
55465
|
-
const
|
|
55640
|
+
const allTickets = ctx.listTickets?.() ?? ctx.tickets;
|
|
55641
|
+
const verifies = allTickets.filter((candidate) => candidate.kind === "review" && candidate.id !== ticket.id && candidate.status === "done").sort((a, b) => (a.completedAt ?? 0) - (b.completedAt ?? 0) || a.seq - b.seq).at(-1);
|
|
55642
|
+
const inheritedMode = ticket.passKind === "verification" ? inheritedReviewMode(verifies?.id) : undefined;
|
|
55643
|
+
const prompt = renderReviewPrompt(ticket, {
|
|
55466
55644
|
TICKET_JSON: buildTicketJson(ticket),
|
|
55467
55645
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
55468
55646
|
DOCS_DIGEST: deps.docsDigest,
|
|
55469
55647
|
LAP_DIGESTS: buildLapDigestsBlock(deps.lapDigests),
|
|
55470
55648
|
FEATURE_BRANCH: feature.branch,
|
|
55471
55649
|
BASE_BRANCH: feature.baseBranch,
|
|
55472
|
-
DRIVE_AVAILABILITY: buildDriveAvailability(findOnPath(AGENT_BROWSER_BIN), project3.devCommand),
|
|
55650
|
+
DRIVE_AVAILABILITY: buildDriveAvailability(findOnPath(AGENT_BROWSER_BIN), project3.devCommand, inheritedMode),
|
|
55473
55651
|
GATE_NOTES: buildGateNotes(deps.config),
|
|
55474
55652
|
DIGEST_PATH: artifacts.digestPath,
|
|
55475
55653
|
BLOCKED_PATH: artifacts.blockedPath,
|
|
55476
|
-
WALKTHROUGH_PATH: artifacts.walkthroughPath
|
|
55654
|
+
WALKTHROUGH_PATH: artifacts.walkthroughPath,
|
|
55655
|
+
LANDED_FIXES: ticket.context,
|
|
55656
|
+
VERIFIES_PASS: verifies ? `#${verifies.seq} \xB7 ${inheritedMode === "drive" ? "Drive" : "Gates"} mode` : "no earlier review in this run \xB7 Gates mode",
|
|
55657
|
+
AUTO_FIX_CAP: String(AUTO_FIX_CAP)
|
|
55477
55658
|
});
|
|
55478
55659
|
mkdirSync9(logsDir(), { recursive: true });
|
|
55479
55660
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
@@ -55551,6 +55732,7 @@ var init_review_ticket = __esm(() => {
|
|
|
55551
55732
|
init_artifacts();
|
|
55552
55733
|
init_agent_stream();
|
|
55553
55734
|
init_git();
|
|
55735
|
+
init_review_findings();
|
|
55554
55736
|
init_ticket_burner();
|
|
55555
55737
|
});
|
|
55556
55738
|
|
|
@@ -56111,7 +56293,7 @@ import { dirname as dirname9, join as join17, posix as posixPath, win32 as winPa
|
|
|
56111
56293
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
56112
56294
|
function missingImageRuntimeMessage(runtime5, image) {
|
|
56113
56295
|
const binary = RUNTIME_BINARY[runtime5];
|
|
56114
|
-
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192
|
|
56296
|
+
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 Burns (Rebuild image).`;
|
|
56115
56297
|
}
|
|
56116
56298
|
function burnAuthReady(runtime5, token, loggedIn = codexLoggedIn) {
|
|
56117
56299
|
if (token !== undefined)
|
|
@@ -56121,6 +56303,17 @@ function burnAuthReady(runtime5, token, loggedIn = codexLoggedIn) {
|
|
|
56121
56303
|
function isReviewTicket(ticket) {
|
|
56122
56304
|
return ticket.kind === "review";
|
|
56123
56305
|
}
|
|
56306
|
+
function verificationDue(tickets2, runTicketIds) {
|
|
56307
|
+
const inRun = tickets2.filter((ticket) => runTicketIds.has(ticket.id));
|
|
56308
|
+
const reviews = inRun.filter(isReviewTicket).filter((ticket) => ticket.completedAt !== null).sort((a, b) => (a.completedAt ?? 0) - (b.completedAt ?? 0) || a.seq - b.seq);
|
|
56309
|
+
const lastReview = reviews.at(-1) ?? null;
|
|
56310
|
+
const verifies = [...reviews].reverse().find((ticket) => ticket.status === "done") ?? null;
|
|
56311
|
+
if (lastReview?.status === "failed")
|
|
56312
|
+
return { due: false, landed: [], verifies };
|
|
56313
|
+
const cutoff = verifies?.completedAt ?? Number.NEGATIVE_INFINITY;
|
|
56314
|
+
const landed = inRun.filter((ticket) => !isReviewTicket(ticket) && ticket.status === "done" && ticket.completedAt !== null && (ticket.completedAt > cutoff || ticket.completedAt === cutoff && verifies !== null && ticket.seq > verifies.seq));
|
|
56315
|
+
return { due: landed.length > 0, landed, verifies };
|
|
56316
|
+
}
|
|
56124
56317
|
function indexBySeq(tickets2) {
|
|
56125
56318
|
return new Map(tickets2.map((t) => [t.seq, t]));
|
|
56126
56319
|
}
|
|
@@ -56487,7 +56680,7 @@ function buildRepoSetupSteps(repoPath, tempBranch, setupCommand, pm) {
|
|
|
56487
56680
|
const repoAttachmentsDir = `${repoPath}/${ATTACHMENTS_DIR}`;
|
|
56488
56681
|
const excludeFile = `${repoPath}/.git/info/exclude`;
|
|
56489
56682
|
const pre = [
|
|
56490
|
-
`if [ -d "${attachmentsDir}" ]; then mkdir -p "${repoAttachmentsDir}" && cp -r "${attachmentsDir}/." "${repoAttachmentsDir}/" && mkdir -p "${repoPath}/.git/info" && { grep -qxF '${
|
|
56683
|
+
`if [ -d "${attachmentsDir}" ]; then mkdir -p "${repoAttachmentsDir}" && cp -r "${attachmentsDir}/." "${repoAttachmentsDir}/" && mkdir -p "${repoPath}/.git/info" && { grep -qxF '${ATTACHMENTS_PREFIX}' "${excludeFile}" 2>/dev/null || printf '%s\\n' '${ATTACHMENTS_PREFIX}' >> "${excludeFile}"; }; fi`,
|
|
56491
56684
|
`printf '#!/bin/sh\\nunset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE\\ngit push --quiet origin HEAD:%s && exit 0\\nsleep 2\\ngit push --quiet origin HEAD:%s && exit 0\\necho "runcastle: commit sync failed (will retry on your next commit); do not re-commit" >&2\\nexit 0\\n' '${tempBranch}' '${tempBranch}' > ${hookFile}`,
|
|
56492
56685
|
`chmod +x ${hookFile}`
|
|
56493
56686
|
];
|
|
@@ -56574,7 +56767,7 @@ function buildWorkspaceNotes(mode, repoPath = ISOLATED_REPO_PATH) {
|
|
|
56574
56767
|
`);
|
|
56575
56768
|
}
|
|
56576
56769
|
function attachedNoteIds(context5) {
|
|
56577
|
-
const marker =
|
|
56770
|
+
const marker = ATTACHMENTS_PREFIX;
|
|
56578
56771
|
const ids3 = [];
|
|
56579
56772
|
for (let at = context5.indexOf(marker);at !== -1; at = context5.indexOf(marker, at + 1)) {
|
|
56580
56773
|
const id3 = /^([\w-]+)\.png/.exec(context5.slice(at + marker.length))?.[1];
|
|
@@ -56605,7 +56798,7 @@ function buildAttachmentCopyCommands(sources, platform = process.platform) {
|
|
|
56605
56798
|
}
|
|
56606
56799
|
async function clearAttachments(workspacePath, repoPath) {
|
|
56607
56800
|
rmSync3(join17(workspacePath, ATTACHMENTS_DIR), { recursive: true, force: true });
|
|
56608
|
-
await unexcludePath(repoPath,
|
|
56801
|
+
await unexcludePath(repoPath, ATTACHMENTS_PREFIX);
|
|
56609
56802
|
}
|
|
56610
56803
|
function buildVerifyNotes(config2) {
|
|
56611
56804
|
const commands = config2.verifyCommands?.trim();
|
|
@@ -56785,7 +56978,7 @@ function missingAgentBinaryMessage(err, runtime5, image) {
|
|
|
56785
56978
|
const missing = new RegExp(`\\b${binary}\\b[^\\n]*(?:exited with code 127|command not found|not found|no such file or directory)`, "i");
|
|
56786
56979
|
if (!missing.test(msg))
|
|
56787
56980
|
return;
|
|
56788
|
-
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192
|
|
56981
|
+
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 Burns (Rebuild image).`;
|
|
56789
56982
|
}
|
|
56790
56983
|
function classifyTicketRunError(err, runtime5) {
|
|
56791
56984
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
@@ -56913,6 +57106,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
56913
57106
|
const inFlight = new Map;
|
|
56914
57107
|
const digests = [];
|
|
56915
57108
|
let admitted = 0;
|
|
57109
|
+
let verificationChecked = false;
|
|
56916
57110
|
const satisfied = (t, s) => s === "done" || s === "cancelled" || isReviewTicket(t) && s === "failed";
|
|
56917
57111
|
const implementationsSettled = () => scheduled.every((t) => isReviewTicket(t) || !pending3.has(t.seq) && !inFlight.has(t.seq));
|
|
56918
57112
|
const admitNewTickets = () => {
|
|
@@ -56937,6 +57131,46 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
56937
57131
|
data: { seqs: added }
|
|
56938
57132
|
});
|
|
56939
57133
|
};
|
|
57134
|
+
const appendVerificationIfDue = () => {
|
|
57135
|
+
verificationChecked = true;
|
|
57136
|
+
if (!ctx.listTickets || !ctx.storeTickets)
|
|
57137
|
+
return;
|
|
57138
|
+
const allTickets = ctx.listTickets();
|
|
57139
|
+
const runTicketIds = new Set(scheduled.map((ticket) => ticket.id));
|
|
57140
|
+
const decision = verificationDue(allTickets, runTicketIds);
|
|
57141
|
+
if (!decision.due)
|
|
57142
|
+
return;
|
|
57143
|
+
const findings = new Map((ctx.listFindings?.() ?? []).map((finding) => [finding.id, finding]));
|
|
57144
|
+
const landedLines = decision.landed.map((ticket) => {
|
|
57145
|
+
const finding = ticket.originFindingId ? findings.get(ticket.originFindingId) : undefined;
|
|
57146
|
+
const detail = finding ? `
|
|
57147
|
+
Finding: ${finding.title}
|
|
57148
|
+
Location: ${finding.location}
|
|
57149
|
+
Repro step: ${finding.reproStep}` : "";
|
|
57150
|
+
return `- #${ticket.seq} ${ticket.title}${detail}`;
|
|
57151
|
+
});
|
|
57152
|
+
ctx.storeTickets([{
|
|
57153
|
+
kind: "review",
|
|
57154
|
+
passKind: "verification",
|
|
57155
|
+
title: "Verify the fixes that landed",
|
|
57156
|
+
goal: "Tour the current build and verify the fixes that landed in this run.",
|
|
57157
|
+
context: [
|
|
57158
|
+
`Verifies pass: ${decision.verifies ? `#${decision.verifies.seq}` : "none in this run"}`,
|
|
57159
|
+
"Landed implementation tickets:",
|
|
57160
|
+
...landedLines
|
|
57161
|
+
].join(`
|
|
57162
|
+
`),
|
|
57163
|
+
acceptanceCriteria: ["Tour the current build and verify every landed fix listed in the context."],
|
|
57164
|
+
seams: ["review evidence and the landed fixes listed in the context"],
|
|
57165
|
+
blockedBy: []
|
|
57166
|
+
}]);
|
|
57167
|
+
ctx.emitEvent({
|
|
57168
|
+
type: "ticket.verification_minted",
|
|
57169
|
+
message: `verification pass appended \u2014 ${decision.landed.length} fixes landed after the review`,
|
|
57170
|
+
data: { landedSeqs: decision.landed.map((ticket) => ticket.seq) }
|
|
57171
|
+
});
|
|
57172
|
+
admitNewTickets();
|
|
57173
|
+
};
|
|
56940
57174
|
const mirrorFinding = (t, progress, reason) => {
|
|
56941
57175
|
if (!t.originFindingId)
|
|
56942
57176
|
return;
|
|
@@ -56990,7 +57224,13 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
56990
57224
|
const outcome = await execute(ctx, t, { digests: [...digests] });
|
|
56991
57225
|
if (outcome.status === "done") {
|
|
56992
57226
|
status3.set(seq2, "done");
|
|
56993
|
-
|
|
57227
|
+
const reviewedCommit = isReviewTicket(t) ? await headSha(ctx.project.repoPath, ctx.feature.branch) : undefined;
|
|
57228
|
+
ctx.updateTicket(t.id, {
|
|
57229
|
+
status: "done",
|
|
57230
|
+
commits: outcome.commits,
|
|
57231
|
+
digest: outcome.digest,
|
|
57232
|
+
...reviewedCommit ? { reviewedCommit } : {}
|
|
57233
|
+
});
|
|
56994
57234
|
mirrorFinding(t, "fixed");
|
|
56995
57235
|
ctx.emitEvent({
|
|
56996
57236
|
type: "ticket.done",
|
|
@@ -57018,8 +57258,13 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57018
57258
|
if (isReviewTicket(t))
|
|
57019
57259
|
admitNewTickets();
|
|
57020
57260
|
};
|
|
57021
|
-
while (pending3.size > 0 || inFlight.size > 0) {
|
|
57261
|
+
while (pending3.size > 0 || inFlight.size > 0 || !verificationChecked) {
|
|
57022
57262
|
ctx.signal.throwIfAborted();
|
|
57263
|
+
if (pending3.size === 0 && inFlight.size === 0) {
|
|
57264
|
+
appendVerificationIfDue();
|
|
57265
|
+
if (pending3.size === 0)
|
|
57266
|
+
break;
|
|
57267
|
+
}
|
|
57023
57268
|
for (const seq2 of [...pending3]) {
|
|
57024
57269
|
const st = readyState(seq2);
|
|
57025
57270
|
if (typeof st === "object") {
|
|
@@ -57365,7 +57610,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
57365
57610
|
const attachmentCommands = buildAttachmentCopyCommands(attachments);
|
|
57366
57611
|
const excludeAttachments = async () => {
|
|
57367
57612
|
if (attachments.length > 0)
|
|
57368
|
-
await excludePath(project3.repoPath,
|
|
57613
|
+
await excludePath(project3.repoPath, ATTACHMENTS_PREFIX);
|
|
57369
57614
|
};
|
|
57370
57615
|
const clearAttachmentsFor = async (branch) => {
|
|
57371
57616
|
if (attachments.length > 0) {
|
|
@@ -57878,7 +58123,7 @@ function readTokenFromEnvFile(path2, runtime5) {
|
|
|
57878
58123
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
|
|
57879
58124
|
return token && token.length > 0 ? token : undefined;
|
|
57880
58125
|
}
|
|
57881
|
-
var AUTH_MISSING_EVENT = "auth.missing", IMAGE_RUNTIME_MISSING_EVENT = "burn.image_runtime_missing", RUNTIME_BINARY, DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", SETUP_MARKER_FILE = ".runcastle-setup", SETUP_MARKER = "RUNCASTLE_SETUP", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, AGENT_BINARY, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
58126
|
+
var ATTACHMENTS_PREFIX, AUTH_MISSING_EVENT = "auth.missing", IMAGE_RUNTIME_MISSING_EVENT = "burn.image_runtime_missing", RUNTIME_BINARY, DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", SETUP_MARKER_FILE = ".runcastle-setup", SETUP_MARKER = "RUNCASTLE_SETUP", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, AGENT_BINARY, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
57882
58127
|
var init_ticket_burner = __esm(() => {
|
|
57883
58128
|
init_src();
|
|
57884
58129
|
init_config_load();
|
|
@@ -57897,6 +58142,7 @@ var init_ticket_burner = __esm(() => {
|
|
|
57897
58142
|
init_docker();
|
|
57898
58143
|
init_podman();
|
|
57899
58144
|
init_no_sandbox();
|
|
58145
|
+
ATTACHMENTS_PREFIX = `${ATTACHMENTS_DIR}/`;
|
|
57900
58146
|
RUNTIME_BINARY = {
|
|
57901
58147
|
"claude-code": "claude",
|
|
57902
58148
|
codex: "codex"
|
|
@@ -58226,6 +58472,8 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
58226
58472
|
updateTicket(ctx, id3, patch9);
|
|
58227
58473
|
},
|
|
58228
58474
|
listTickets: () => listByFeature2(ctx, featureId),
|
|
58475
|
+
storeTickets: (inputs) => storeTickets(ctx, featureId, inputs),
|
|
58476
|
+
listFindings: () => listByFeature3(ctx, featureId),
|
|
58229
58477
|
updateFinding: (findingId, progress, reason) => {
|
|
58230
58478
|
markFixProgress(ctx, findingId, progress, reason);
|
|
58231
58479
|
},
|
|
@@ -58508,7 +58756,7 @@ function getFeatureFull(ctx, id3) {
|
|
|
58508
58756
|
return {
|
|
58509
58757
|
feature,
|
|
58510
58758
|
tickets: listByFeature2(ctx, id3),
|
|
58511
|
-
sessions: listSessionsByFeature(ctx, id3),
|
|
58759
|
+
sessions: listSessionsByFeature(ctx, id3).map((session) => session.kind === "qa" && session.status === "ended" && !session.ccSessionId ? { ...session, title: null, transcriptMissing: true } : session),
|
|
58512
58760
|
runs: listRunsByFeature(ctx, id3),
|
|
58513
58761
|
docs: listDocs(ctx, feature),
|
|
58514
58762
|
gate: gateState(ctx, feature),
|
|
@@ -58615,22 +58863,25 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
58615
58863
|
throw e;
|
|
58616
58864
|
}
|
|
58617
58865
|
}
|
|
58866
|
+
function assertIterable(ctx, feature) {
|
|
58867
|
+
if (hasActiveRun(ctx, feature.id)) {
|
|
58868
|
+
throw new GateError("a run is burning this feature \u2014 cancel or wait for it before iterating");
|
|
58869
|
+
}
|
|
58870
|
+
if (activeTestDriveFeatureId() === feature.id) {
|
|
58871
|
+
throw new GateError(`${feature.slug} is being test-driven \u2014 stop the test drive first, it holds the feature branch`);
|
|
58872
|
+
}
|
|
58873
|
+
}
|
|
58618
58874
|
function rethink(ctx, featureId) {
|
|
58619
58875
|
const feature = getFeatureRow(ctx, featureId);
|
|
58620
58876
|
requireNotDraft(feature);
|
|
58621
58877
|
if (feature.phase !== RETHINK_LOOP_BACK.from) {
|
|
58622
58878
|
throw new GateError(`feature must be in the review phase to rethink (currently ${feature.phase})`);
|
|
58623
58879
|
}
|
|
58624
|
-
|
|
58625
|
-
throw new GateError("a run is burning this feature \u2014 cancel or wait for it before rethinking");
|
|
58626
|
-
}
|
|
58880
|
+
assertIterable(ctx, feature);
|
|
58627
58881
|
const live = activeSessionsForFeature(ctx, featureId);
|
|
58628
58882
|
if (live.length > 0) {
|
|
58629
58883
|
throw new GateError(`a ${live[0].kind} session is already live for ${feature.slug} \u2014 only one terminal per feature; end or resume it first`);
|
|
58630
58884
|
}
|
|
58631
|
-
if (activeTestDriveFeatureId() === featureId) {
|
|
58632
|
-
throw new GateError(`${feature.slug} is being test-driven \u2014 stop the test drive first, it holds the feature branch`);
|
|
58633
|
-
}
|
|
58634
58885
|
const lap = feature.lap + 1;
|
|
58635
58886
|
ctx.db.update(features).set({ lap }).where(eq11(features.id, featureId)).run();
|
|
58636
58887
|
return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
|
|
@@ -58842,7 +59093,7 @@ var init_features = __esm(() => {
|
|
|
58842
59093
|
// src/services/test-notes.ts
|
|
58843
59094
|
import { existsSync as existsSync19, mkdirSync as mkdirSync12, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
58844
59095
|
import { dirname as dirname11 } from "path";
|
|
58845
|
-
import { asc as asc5, eq as eq12 } from "drizzle-orm";
|
|
59096
|
+
import { and as and9, asc as asc5, eq as eq12, lt } from "drizzle-orm";
|
|
58846
59097
|
function rowToNote(row) {
|
|
58847
59098
|
return TestNote.parse({
|
|
58848
59099
|
id: row.id,
|
|
@@ -58852,6 +59103,8 @@ function rowToNote(row) {
|
|
|
58852
59103
|
status: row.status,
|
|
58853
59104
|
author: row.author,
|
|
58854
59105
|
ticketId: row.ticketId ?? undefined,
|
|
59106
|
+
reviewTicketId: row.reviewTicketId ?? undefined,
|
|
59107
|
+
carriedLap: row.carriedLap ?? undefined,
|
|
58855
59108
|
videoTimestamp: row.videoTimestamp ?? undefined,
|
|
58856
59109
|
screenshotUrl: existsSync19(annotationPath(row.id)) ? noteScreenshotUrl(row.id) : undefined,
|
|
58857
59110
|
createdAt: row.createdAt,
|
|
@@ -58878,9 +59131,14 @@ function assertOpen(note2, action) {
|
|
|
58878
59131
|
function listByFeature4(ctx, featureId) {
|
|
58879
59132
|
return ctx.db.select().from(testNotes).where(eq12(testNotes.featureId, featureId)).orderBy(asc5(testNotes.createdAt), asc5(testNotes.id)).all().map(rowToNote);
|
|
58880
59133
|
}
|
|
58881
|
-
function addNote(ctx, featureId, text3, author = "human", videoTimestamp) {
|
|
59134
|
+
function addNote(ctx, featureId, text3, author = "human", videoTimestamp, reviewTicketId) {
|
|
58882
59135
|
const body = cleanText(text3);
|
|
58883
59136
|
const feature = getFeatureRow(ctx, featureId);
|
|
59137
|
+
if (reviewTicketId) {
|
|
59138
|
+
const ticket = getTicket(ctx, reviewTicketId);
|
|
59139
|
+
if (ticket.featureId !== featureId || ticket.kind !== "review")
|
|
59140
|
+
throw new InvalidInputError("review ticket must be a review of the same feature");
|
|
59141
|
+
}
|
|
58884
59142
|
const now = Date.now();
|
|
58885
59143
|
const row = ctx.db.insert(testNotes).values({
|
|
58886
59144
|
id: newId("note"),
|
|
@@ -58891,6 +59149,8 @@ function addNote(ctx, featureId, text3, author = "human", videoTimestamp) {
|
|
|
58891
59149
|
author,
|
|
58892
59150
|
ticketId: null,
|
|
58893
59151
|
videoTimestamp: videoTimestamp ?? null,
|
|
59152
|
+
reviewTicketId: reviewTicketId ?? null,
|
|
59153
|
+
carriedLap: null,
|
|
58894
59154
|
createdAt: now,
|
|
58895
59155
|
updatedAt: now
|
|
58896
59156
|
}).returning().get();
|
|
@@ -58942,7 +59202,7 @@ function attachScreenshot(ctx, noteId, png) {
|
|
|
58942
59202
|
}
|
|
58943
59203
|
function toggleNote(ctx, noteId) {
|
|
58944
59204
|
const current2 = getNote(ctx, noteId);
|
|
58945
|
-
if (current2.status === "promoted") {
|
|
59205
|
+
if (current2.status === "promoted" || current2.status === "carried") {
|
|
58946
59206
|
throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
|
|
58947
59207
|
}
|
|
58948
59208
|
const status3 = current2.status === "open" ? "done" : "open";
|
|
@@ -58962,12 +59222,10 @@ function screenshotParagraph(note2) {
|
|
|
58962
59222
|
return `An annotated screenshot of the problem is at ${attachmentRelPath(note2.id)} in your workspace${moment} \u2014 Read it before starting; the drawing marks the problem area.`;
|
|
58963
59223
|
}
|
|
58964
59224
|
function promotionTicket(feature, note2) {
|
|
58965
|
-
const firstLine = note2.text.split(`
|
|
58966
|
-
`)[0].trim();
|
|
58967
59225
|
const docs2 = featureDocsRel(feature.slug);
|
|
58968
59226
|
const screenshot = screenshotParagraph(note2);
|
|
58969
59227
|
return {
|
|
58970
|
-
title:
|
|
59228
|
+
title: ticketTitleFromNote(note2.text),
|
|
58971
59229
|
goal: note2.text,
|
|
58972
59230
|
context: [
|
|
58973
59231
|
`Found during lap ${note2.lap} test drive of ${feature.slug}.`,
|
|
@@ -59030,9 +59288,56 @@ function noteLine(ctx, note2) {
|
|
|
59030
59288
|
const shot = screenshotSuffix(note2);
|
|
59031
59289
|
if (note2.status === "open")
|
|
59032
59290
|
return `- [ ] ${note2.text}${shot}`;
|
|
59291
|
+
if (note2.status === "carried")
|
|
59292
|
+
return `- [\u2192] ${note2.text} (carried into lap ${note2.carriedLap})${shot}`;
|
|
59033
59293
|
const ticket = note2.ticketId ? ` (\u2192 ticket ${getTicket(ctx, note2.ticketId).seq})` : "";
|
|
59034
59294
|
return `- [x] ${note2.text}${ticket}${shot}`;
|
|
59035
59295
|
}
|
|
59296
|
+
function carryNotes(ctx, featureId, noteIds, lap) {
|
|
59297
|
+
const selected = noteIds.map((id3) => getNote(ctx, id3));
|
|
59298
|
+
for (const note2 of selected) {
|
|
59299
|
+
if (note2.featureId !== featureId)
|
|
59300
|
+
throw new InvalidInputError("every carried note must belong to the feature");
|
|
59301
|
+
assertOpen(note2, "carry");
|
|
59302
|
+
ctx.db.update(testNotes).set({ status: "carried", carriedLap: lap, updatedAt: Date.now() }).where(eq12(testNotes.id, note2.id)).run();
|
|
59303
|
+
emit(ctx, featureId, { type: "note.carried", message: `note carried into lap ${lap}`, data: { noteId: note2.id, lap } });
|
|
59304
|
+
}
|
|
59305
|
+
renderTestNotes(ctx, getFeatureRow(ctx, featureId));
|
|
59306
|
+
return selected.map((note2) => getNote(ctx, note2.id));
|
|
59307
|
+
}
|
|
59308
|
+
function reopenNote(ctx, noteId) {
|
|
59309
|
+
const note2 = getNote(ctx, noteId);
|
|
59310
|
+
if (note2.status !== "carried")
|
|
59311
|
+
throw new InvalidInputError("only a carried note can be reopened");
|
|
59312
|
+
ctx.db.update(testNotes).set({ status: "open", carriedLap: null, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
|
|
59313
|
+
emit(ctx, note2.featureId, { type: "note.reopened", message: "note reopened", data: { noteId } });
|
|
59314
|
+
renderTestNotes(ctx, getFeatureRow(ctx, note2.featureId));
|
|
59315
|
+
return getNote(ctx, noteId);
|
|
59316
|
+
}
|
|
59317
|
+
function triageNotes(ctx, featureId, input) {
|
|
59318
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
59319
|
+
assertIterable(ctx, feature);
|
|
59320
|
+
for (const id3 of input.dismissIds)
|
|
59321
|
+
deleteNote(ctx, id3);
|
|
59322
|
+
const noteTickets = input.quickFixIds.length ? freezeAsTickets(ctx, input.quickFixIds).tickets.length : 0;
|
|
59323
|
+
const findingTickets = input.quickFixFindingIds.length ? promoteOpenDefects(ctx, featureId, input.quickFixFindingIds).tickets.length : 0;
|
|
59324
|
+
const remaining = input.carry ? listByFeature4(ctx, featureId).filter((note2) => note2.status === "open") : [];
|
|
59325
|
+
if (remaining.length)
|
|
59326
|
+
carryNotes(ctx, featureId, remaining.map((note2) => note2.id), feature.lap + 1);
|
|
59327
|
+
return { minted: noteTickets + findingTickets, carried: remaining.length, dismissed: input.dismissIds.length };
|
|
59328
|
+
}
|
|
59329
|
+
function triagePreview(ctx, featureId) {
|
|
59330
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
59331
|
+
const standing = ctx.db.select().from(tickets).where(and9(eq12(tickets.featureId, featureId), eq12(tickets.kind, "implementation"), eq12(tickets.status, "pending"), lt(tickets.lap, feature.lap))).all();
|
|
59332
|
+
const grouped = new Map;
|
|
59333
|
+
for (const ticket of standing)
|
|
59334
|
+
grouped.set(ticket.lap, (grouped.get(ticket.lap) ?? 0) + 1);
|
|
59335
|
+
return {
|
|
59336
|
+
openNotes: listByFeature4(ctx, featureId).filter((note2) => note2.status === "open").length,
|
|
59337
|
+
openDefects: viewByFeature(ctx, featureId).openDefects.length,
|
|
59338
|
+
standingFixTickets: [...grouped].sort(([a], [b]) => a - b).map(([lap, count3]) => ({ count: count3, lap }))
|
|
59339
|
+
};
|
|
59340
|
+
}
|
|
59036
59341
|
function renderTestNotes(ctx, feature) {
|
|
59037
59342
|
const notes = listByFeature4(ctx, feature.id);
|
|
59038
59343
|
const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
|
|
@@ -59048,7 +59353,6 @@ function renderTestNotes(ctx, feature) {
|
|
|
59048
59353
|
`)}
|
|
59049
59354
|
`, "utf8");
|
|
59050
59355
|
}
|
|
59051
|
-
var TITLE_MAX = 60;
|
|
59052
59356
|
var init_test_notes = __esm(() => {
|
|
59053
59357
|
init_src();
|
|
59054
59358
|
init_paths();
|
|
@@ -59058,6 +59362,8 @@ var init_test_notes = __esm(() => {
|
|
|
59058
59362
|
init_feature_docs();
|
|
59059
59363
|
init_repo();
|
|
59060
59364
|
init_tickets();
|
|
59365
|
+
init_features();
|
|
59366
|
+
init_review_findings();
|
|
59061
59367
|
});
|
|
59062
59368
|
|
|
59063
59369
|
// src/mcp/server.ts
|
|
@@ -59302,7 +59608,7 @@ function requireProject(ctx, session) {
|
|
|
59302
59608
|
throw new GateError(`project ${session.projectId} not found`);
|
|
59303
59609
|
return project3;
|
|
59304
59610
|
}
|
|
59305
|
-
function toolRecordFinding(ctx, session, input) {
|
|
59611
|
+
async function toolRecordFinding(ctx, session, input) {
|
|
59306
59612
|
const project3 = requireProject(ctx, session);
|
|
59307
59613
|
const source = input.userSupplied ? "human" : "session";
|
|
59308
59614
|
if (!isOverwritable(ctx, project3.id, input.key) && !input.userSupplied) {
|
|
@@ -59313,11 +59619,17 @@ function toolRecordFinding(ctx, session, input) {
|
|
|
59313
59619
|
skipped: `${input.key} was set by a human and is not auto-overwritable \u2014 ask them to clear it first if it should change`
|
|
59314
59620
|
};
|
|
59315
59621
|
}
|
|
59622
|
+
let establishedSha;
|
|
59623
|
+
try {
|
|
59624
|
+
const mainLine = await detectMainBranch(project3.repoPath);
|
|
59625
|
+
establishedSha = await headSha(project3.repoPath, mainLine);
|
|
59626
|
+
} catch {}
|
|
59316
59627
|
recordFinding(ctx, project3.id, {
|
|
59317
59628
|
key: input.key,
|
|
59318
59629
|
value: input.value,
|
|
59319
59630
|
source,
|
|
59320
|
-
...input.evidence ? { evidence: input.evidence } : {}
|
|
59631
|
+
...input.evidence ? { evidence: input.evidence } : {},
|
|
59632
|
+
...establishedSha ? { establishedSha } : {}
|
|
59321
59633
|
});
|
|
59322
59634
|
emitProject(ctx, project3.id, {
|
|
59323
59635
|
type: "prep.finding_recorded",
|
|
@@ -59594,7 +59906,7 @@ function buildMcpServer(audience) {
|
|
|
59594
59906
|
const rs = await resolveCtxSession(extra);
|
|
59595
59907
|
if (!rs)
|
|
59596
59908
|
return noSession();
|
|
59597
|
-
return ok(toolRecordFinding(rs.ctx, rs.session, args2));
|
|
59909
|
+
return ok(await toolRecordFinding(rs.ctx, rs.session, args2));
|
|
59598
59910
|
});
|
|
59599
59911
|
}
|
|
59600
59912
|
if (wants("dry_run_drive")) {
|
|
@@ -60071,7 +60383,7 @@ var init_ws = __esm(() => {
|
|
|
60071
60383
|
});
|
|
60072
60384
|
|
|
60073
60385
|
// src/services/prep.ts
|
|
60074
|
-
import { and as
|
|
60386
|
+
import { and as and10, desc as desc6, eq as eq13, inArray as inArray5 } from "drizzle-orm";
|
|
60075
60387
|
function keysToPrepare(ctx, project3) {
|
|
60076
60388
|
return unsetPreparedKeys(project3).filter((key) => isOverwritable(ctx, project3.id, key));
|
|
60077
60389
|
}
|
|
@@ -60081,10 +60393,10 @@ function isPrepared(ctx, project3) {
|
|
|
60081
60393
|
return hasCompletedProjectSession(ctx, project3.id, "prepare");
|
|
60082
60394
|
}
|
|
60083
60395
|
function preparedAt(ctx, projectId) {
|
|
60084
|
-
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(
|
|
60396
|
+
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(and10(eq13(sessions.projectId, projectId), eq13(sessions.kind, "prepare"))).all().map((r) => r.id));
|
|
60085
60397
|
if (prepareSessions.size === 0)
|
|
60086
60398
|
return null;
|
|
60087
|
-
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(
|
|
60399
|
+
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(and10(eq13(events.projectId, projectId), inArray5(events.type, ["session.ended", "session.reconciled"]))).orderBy(desc6(events.ts)).all();
|
|
60088
60400
|
for (const end5 of ends) {
|
|
60089
60401
|
const sessionId = end5.data?.sessionId;
|
|
60090
60402
|
if (sessionId && prepareSessions.has(sessionId))
|
|
@@ -60339,15 +60651,12 @@ var init_hooks = __esm(() => {
|
|
|
60339
60651
|
});
|
|
60340
60652
|
|
|
60341
60653
|
// src/routes/reviews.ts
|
|
60342
|
-
import { createReadStream, statSync as
|
|
60654
|
+
import { createReadStream, statSync as statSync6 } from "fs";
|
|
60343
60655
|
import { Readable } from "stream";
|
|
60344
60656
|
import { Hono as Hono3 } from "hono";
|
|
60345
|
-
function walkthroughUrl(ticketId) {
|
|
60346
|
-
return `/api/reviews/ticket/${ticketId}/walkthrough.webm`;
|
|
60347
|
-
}
|
|
60348
60657
|
function fileSize(path2) {
|
|
60349
60658
|
try {
|
|
60350
|
-
const stat3 =
|
|
60659
|
+
const stat3 = statSync6(path2);
|
|
60351
60660
|
return stat3.isFile() ? stat3.size : undefined;
|
|
60352
60661
|
} catch {
|
|
60353
60662
|
return;
|
|
@@ -60390,9 +60699,6 @@ function parseRange(header, size6) {
|
|
|
60390
60699
|
function fileStream(path2, start3, end5) {
|
|
60391
60700
|
return Readable.toWeb(createReadStream(path2, { start: start3, end: end5 }));
|
|
60392
60701
|
}
|
|
60393
|
-
function findNote(ctx, noteId) {
|
|
60394
|
-
return lookupOrUndefined(() => getNote(ctx, noteId));
|
|
60395
|
-
}
|
|
60396
60702
|
function isPng(bytes) {
|
|
60397
60703
|
return bytes.length > PNG_MAGIC.length && PNG_MAGIC.every((byte, i) => bytes[i] === byte);
|
|
60398
60704
|
}
|
|
@@ -60404,20 +60710,32 @@ var init_reviews = __esm(() => {
|
|
|
60404
60710
|
init_test_notes();
|
|
60405
60711
|
init_tickets();
|
|
60406
60712
|
reviews = new Hono3;
|
|
60407
|
-
reviews.get(
|
|
60713
|
+
reviews.get(REVIEW_ARTIFACTS_ROUTE, async (c) => {
|
|
60408
60714
|
const ctx = await getRuntimeCtx();
|
|
60409
|
-
const
|
|
60715
|
+
const allTickets = listByFeature2(ctx, c.req.param("featureId"));
|
|
60716
|
+
const artifacts = allTickets.filter((t) => t.kind === "review").map((t) => {
|
|
60410
60717
|
const hasVideo = fileSize(reviewWalkthroughPath(t.id)) !== undefined;
|
|
60411
60718
|
return {
|
|
60412
60719
|
ticketId: t.id,
|
|
60413
60720
|
seq: t.seq,
|
|
60721
|
+
lap: t.lap,
|
|
60722
|
+
passKind: t.passKind,
|
|
60723
|
+
reviewedCommit: t.reviewedCommit,
|
|
60724
|
+
completedAt: t.completedAt,
|
|
60725
|
+
landedSince: t.completedAt === null ? 0 : allTickets.filter((candidate) => candidate.kind === "implementation" && candidate.status === "done" && candidate.completedAt !== null && candidate.completedAt > t.completedAt).length,
|
|
60414
60726
|
hasVideo,
|
|
60415
|
-
videoUrl: hasVideo ?
|
|
60416
|
-
};
|
|
60727
|
+
videoUrl: hasVideo ? reviewWalkthroughUrl(t.id) : null
|
|
60728
|
+
};
|
|
60729
|
+
}).sort((a, b) => {
|
|
60730
|
+
if (a.completedAt === null)
|
|
60731
|
+
return b.completedAt === null ? a.seq - b.seq : 1;
|
|
60732
|
+
if (b.completedAt === null)
|
|
60733
|
+
return -1;
|
|
60734
|
+
return a.completedAt - b.completedAt || a.seq - b.seq;
|
|
60417
60735
|
});
|
|
60418
60736
|
return c.json(artifacts);
|
|
60419
60737
|
});
|
|
60420
|
-
reviews.get(
|
|
60738
|
+
reviews.get(REVIEW_WALKTHROUGH_ROUTE, async (c) => {
|
|
60421
60739
|
const ctx = await getRuntimeCtx();
|
|
60422
60740
|
const ticket = findReviewTicket(ctx, c.req.param("ticketId"));
|
|
60423
60741
|
if (!ticket)
|
|
@@ -60448,7 +60766,7 @@ var init_reviews = __esm(() => {
|
|
|
60448
60766
|
PNG_MAGIC = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
60449
60767
|
reviews.post(NOTE_SCREENSHOT_UPLOAD_ROUTE, async (c) => {
|
|
60450
60768
|
const ctx = await getRuntimeCtx();
|
|
60451
|
-
const note2 =
|
|
60769
|
+
const note2 = lookupOrUndefined(() => getNote(ctx, c.req.param("noteId")));
|
|
60452
60770
|
if (!note2)
|
|
60453
60771
|
return c.notFound();
|
|
60454
60772
|
const png = new Uint8Array(await c.req.arrayBuffer());
|
|
@@ -60458,7 +60776,7 @@ var init_reviews = __esm(() => {
|
|
|
60458
60776
|
});
|
|
60459
60777
|
reviews.get(NOTE_SCREENSHOT_ROUTE, async (c) => {
|
|
60460
60778
|
const ctx = await getRuntimeCtx();
|
|
60461
|
-
const note2 =
|
|
60779
|
+
const note2 = lookupOrUndefined(() => getNote(ctx, c.req.param("noteId")));
|
|
60462
60780
|
if (!note2)
|
|
60463
60781
|
return c.notFound();
|
|
60464
60782
|
const path2 = annotationPath(note2.id);
|
|
@@ -60525,7 +60843,7 @@ var init_stream = __esm(() => {
|
|
|
60525
60843
|
});
|
|
60526
60844
|
|
|
60527
60845
|
// src/routes/web.ts
|
|
60528
|
-
import { existsSync as existsSync21, statSync as
|
|
60846
|
+
import { existsSync as existsSync21, statSync as statSync7 } from "fs";
|
|
60529
60847
|
import { readFile as readFile4 } from "fs/promises";
|
|
60530
60848
|
import { extname, join as join20, normalize as normalize2, resolve as resolve11, sep as sep3 } from "path";
|
|
60531
60849
|
function contentType(path2) {
|
|
@@ -60533,7 +60851,7 @@ function contentType(path2) {
|
|
|
60533
60851
|
}
|
|
60534
60852
|
function isFile2(path2) {
|
|
60535
60853
|
try {
|
|
60536
|
-
return
|
|
60854
|
+
return statSync7(path2).isFile();
|
|
60537
60855
|
} catch {
|
|
60538
60856
|
return false;
|
|
60539
60857
|
}
|
|
@@ -60836,7 +61154,7 @@ var init_events2 = __esm(() => {
|
|
|
60836
61154
|
// src/launcher/launcher.ts
|
|
60837
61155
|
import { existsSync as existsSync23 } from "fs";
|
|
60838
61156
|
import { join as join23 } from "path";
|
|
60839
|
-
import { and as
|
|
61157
|
+
import { and as and11, eq as eq14 } from "drizzle-orm";
|
|
60840
61158
|
function ptyExitMessage(exitCode) {
|
|
60841
61159
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
60842
61160
|
return `terminal exited (code ${label})`;
|
|
@@ -60856,7 +61174,7 @@ function renderCommand(runtime5, spec) {
|
|
|
60856
61174
|
return [...env, runtime5.binary, ...spec.argv].join(" ");
|
|
60857
61175
|
}
|
|
60858
61176
|
function activeRunFor(ctx, featureId) {
|
|
60859
|
-
const row = ctx.db.select().from(runs).where(
|
|
61177
|
+
const row = ctx.db.select().from(runs).where(and11(eq14(runs.featureId, featureId), eq14(runs.status, "running"))).limit(1).get();
|
|
60860
61178
|
return row ? rowToRun(row) : null;
|
|
60861
61179
|
}
|
|
60862
61180
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -61500,69 +61818,126 @@ var init_launcher = __esm(() => {
|
|
|
61500
61818
|
// src/services/outcome.ts
|
|
61501
61819
|
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
61502
61820
|
import { join as join25 } from "path";
|
|
61503
|
-
function
|
|
61821
|
+
function digestHasSubstance(text3) {
|
|
61822
|
+
return text3.split(`
|
|
61823
|
+
`).some((line) => {
|
|
61824
|
+
const trimmed = line.trim();
|
|
61825
|
+
return /^[-*]\s+/.test(trimmed) && !EMPTY_VALUE.test(trimmed);
|
|
61826
|
+
});
|
|
61827
|
+
}
|
|
61828
|
+
function findingResolution(finding) {
|
|
61829
|
+
return finding.status === "fixed" ? "fixed" : finding.status === "dismissed" ? "dismissed" : "open";
|
|
61830
|
+
}
|
|
61831
|
+
function noteResolution(note2) {
|
|
61832
|
+
if (note2.status === "promoted")
|
|
61833
|
+
return `quick-fixed \u2192 ticket ${note2.ticketId ?? "unknown"}`;
|
|
61834
|
+
if (note2.status === "carried")
|
|
61835
|
+
return `carried \u2192 lap ${note2.carriedLap ?? "?"}`;
|
|
61836
|
+
if (note2.status === "done")
|
|
61837
|
+
return "dismissed/done";
|
|
61838
|
+
return "open";
|
|
61839
|
+
}
|
|
61840
|
+
function composeOutcomeDoc(input) {
|
|
61841
|
+
const { feature, tickets: tickets2, findings, notes, artifacts, shippedAt, delta } = input;
|
|
61842
|
+
const laps = Math.max(feature.lap, ...tickets2.map((ticket) => ticket.lap), 1);
|
|
61504
61843
|
const blocks = [
|
|
61505
61844
|
`# Outcome \u2014 ${feature.title}`,
|
|
61506
61845
|
feature.oneLiner,
|
|
61507
|
-
|
|
61508
|
-
|
|
61846
|
+
`- Shipped: ${new Date(shippedAt).toISOString().slice(0, 10)}
|
|
61847
|
+
- Laps run: ${laps}`,
|
|
61848
|
+
`## What shipped
|
|
61849
|
+
|
|
61850
|
+
${delta.commits} commits \xB7 ${delta.files} files`
|
|
61509
61851
|
];
|
|
61510
|
-
let
|
|
61511
|
-
|
|
61512
|
-
|
|
61513
|
-
|
|
61514
|
-
`))
|
|
61515
|
-
|
|
61516
|
-
|
|
61517
|
-
|
|
61518
|
-
|
|
61519
|
-
|
|
61520
|
-
|
|
61521
|
-
|
|
61522
|
-
|
|
61852
|
+
for (let lap = 1;lap <= laps; lap += 1) {
|
|
61853
|
+
const implementation = tickets2.filter((ticket) => ticket.kind === "implementation" && ticket.lap === lap);
|
|
61854
|
+
const line = (status3, label) => {
|
|
61855
|
+
const matches = implementation.filter((ticket) => ticket.status === status3);
|
|
61856
|
+
return `- ${matches.length} ${label}${matches.length ? `: ${matches.map((t2) => `#${t2.seq} ${t2.title}`).join("; ")}` : ""}`;
|
|
61857
|
+
};
|
|
61858
|
+
blocks.push(`### Lap ${lap}
|
|
61859
|
+
${line("done", "tickets landed")}
|
|
61860
|
+
${line("cancelled", "waived")}
|
|
61861
|
+
${line("failed", "failed")}`);
|
|
61862
|
+
}
|
|
61863
|
+
blocks.push("## Review record");
|
|
61864
|
+
const completed = artifacts.filter((artifact) => artifact.completedAt !== null);
|
|
61865
|
+
const latest = [...completed].sort((a, b) => a.completedAt - b.completedAt).at(-1);
|
|
61866
|
+
for (const artifact of completed) {
|
|
61867
|
+
const ticket = tickets2.find((candidate) => candidate.id === artifact.ticketId);
|
|
61868
|
+
blocks.push(`### Lap ${artifact.lap} \xB7 ${artifact.passKind}
|
|
61869
|
+
|
|
61870
|
+
- Reviewed commit: ${artifact.reviewedCommit ?? "unknown"}
|
|
61871
|
+
- Landed since: ${artifact.landedSince}
|
|
61872
|
+
- Outcome: ${ticket?.status ?? "unknown"}`);
|
|
61873
|
+
if (artifact.ticketId === latest?.ticketId) {
|
|
61874
|
+
const passFindings = findings.filter((finding) => finding.reviewTicketId === artifact.ticketId);
|
|
61875
|
+
blocks.push(passFindings.length ? passFindings.map((finding) => `- **${finding.title}** \u2014 ${findingResolution(finding)}`).join(`
|
|
61876
|
+
`) : "- No findings");
|
|
61877
|
+
}
|
|
61878
|
+
}
|
|
61879
|
+
if (!completed.length)
|
|
61880
|
+
blocks.push("- No completed review pass");
|
|
61881
|
+
blocks.push("## Notes record", notes.length ? notes.map((note2) => `- ${note2.text} \u2014 ${noteResolution(note2)}`).join(`
|
|
61882
|
+
`) : "- No human notes");
|
|
61883
|
+
const substantive = tickets2.filter((ticket) => ticket.digest && digestHasSubstance(ticket.digest));
|
|
61884
|
+
if (substantive.length) {
|
|
61885
|
+
blocks.push("## Per-ticket digests");
|
|
61886
|
+
for (let lap = 1;lap <= laps; lap += 1) {
|
|
61887
|
+
const inLap = substantive.filter((ticket) => ticket.lap === lap);
|
|
61888
|
+
if (inLap.length)
|
|
61889
|
+
blocks.push(`### Lap ${lap}`, ...inLap.map((ticket) => `#### ${ticket.seq}. ${ticket.title}
|
|
61890
|
+
|
|
61891
|
+
${ticket.digest.trim()}`));
|
|
61523
61892
|
}
|
|
61524
61893
|
}
|
|
61525
|
-
flush();
|
|
61526
61894
|
return `${blocks.join(`
|
|
61527
61895
|
|
|
61528
61896
|
`)}
|
|
61529
61897
|
`;
|
|
61530
61898
|
}
|
|
61531
|
-
function
|
|
61532
|
-
return new Date(shippedAt).toISOString().slice(0, 10);
|
|
61533
|
-
}
|
|
61534
|
-
function oneLineEntry(ticket) {
|
|
61535
|
-
const headline = errorHeadline3(ticket.error ?? "");
|
|
61536
|
-
return `- **${ticket.seq}. ${ticket.title}** \u2014 ${ticket.status}${headline ? `: ${headline}` : ""}`;
|
|
61537
|
-
}
|
|
61538
|
-
function errorHeadline3(error) {
|
|
61539
|
-
const lines = error.split(`
|
|
61540
|
-
`).map((l) => l.trim()).filter(Boolean);
|
|
61541
|
-
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
61542
|
-
const headline = causes.at(-1) ?? lines[0] ?? "";
|
|
61543
|
-
return headline.length > HEADLINE_MAX ? `${headline.slice(0, HEADLINE_MAX)}\u2026` : headline;
|
|
61544
|
-
}
|
|
61545
|
-
async function promoteOutcomeDoc(ctx, project3, feature) {
|
|
61899
|
+
async function promoteOutcomeDoc(ctx, project3, feature, target, delta) {
|
|
61546
61900
|
try {
|
|
61547
|
-
const
|
|
61548
|
-
const
|
|
61549
|
-
|
|
61550
|
-
|
|
61551
|
-
|
|
61552
|
-
|
|
61553
|
-
|
|
61901
|
+
const tickets2 = listByFeature2(ctx, feature.id);
|
|
61902
|
+
const artifacts = tickets2.filter((ticket) => ticket.kind === "review").map((ticket) => ({
|
|
61903
|
+
ticketId: ticket.id,
|
|
61904
|
+
lap: ticket.lap,
|
|
61905
|
+
passKind: ticket.passKind,
|
|
61906
|
+
reviewedCommit: ticket.reviewedCommit,
|
|
61907
|
+
completedAt: ticket.completedAt,
|
|
61908
|
+
landedSince: ticket.completedAt === null ? 0 : tickets2.filter((candidate) => candidate.kind === "implementation" && candidate.status === "done" && candidate.completedAt !== null && candidate.completedAt > ticket.completedAt).length
|
|
61909
|
+
}));
|
|
61910
|
+
const doc = composeOutcomeDoc({
|
|
61911
|
+
feature,
|
|
61912
|
+
tickets: tickets2,
|
|
61913
|
+
findings: listByFeature3(ctx, feature.id),
|
|
61914
|
+
notes: listByFeature4(ctx, feature.id),
|
|
61915
|
+
artifacts,
|
|
61916
|
+
shippedAt: Date.now(),
|
|
61917
|
+
delta: { commits: delta.commits ?? 0, files: delta.files ?? 0 }
|
|
61918
|
+
});
|
|
61919
|
+
await onBranch(project3.repoPath, target, async () => {
|
|
61920
|
+
const docsDir = join25(project3.repoPath, ...featureDocsRel(feature.slug).split("/"));
|
|
61921
|
+
mkdirSync14(docsDir, { recursive: true });
|
|
61922
|
+
writeFileSync10(join25(docsDir, "outcome.md"), doc, "utf8");
|
|
61923
|
+
await commitDocs(project3.repoPath, `runcastle: outcome for ${feature.slug}`);
|
|
61924
|
+
});
|
|
61925
|
+
} catch (error) {
|
|
61554
61926
|
emit(ctx, feature.id, {
|
|
61555
61927
|
type: "docs.outcome_failed",
|
|
61556
|
-
message: `outcome.md not promoted: ${
|
|
61928
|
+
message: `outcome.md not promoted on ${target}: ${error instanceof Error ? error.message : String(error)}`
|
|
61557
61929
|
});
|
|
61558
61930
|
}
|
|
61559
61931
|
}
|
|
61560
|
-
var
|
|
61932
|
+
var EMPTY_VALUE;
|
|
61561
61933
|
var init_outcome = __esm(() => {
|
|
61562
61934
|
init_paths();
|
|
61563
61935
|
init_events();
|
|
61564
61936
|
init_git();
|
|
61937
|
+
init_review_findings();
|
|
61938
|
+
init_test_notes();
|
|
61565
61939
|
init_tickets();
|
|
61940
|
+
EMPTY_VALUE = /^(?:[-*]\s*)?(?:none|nothing|n\/?a)[.!]?$/i;
|
|
61566
61941
|
});
|
|
61567
61942
|
|
|
61568
61943
|
// src/trpc/routers/feature.ts
|
|
@@ -61585,6 +61960,7 @@ var init_feature = __esm(() => {
|
|
|
61585
61960
|
projectId: z7.string(),
|
|
61586
61961
|
title: z7.string().min(1),
|
|
61587
61962
|
oneLiner: z7.string(),
|
|
61963
|
+
brief: z7.string().optional(),
|
|
61588
61964
|
baseBranch: z7.string().optional(),
|
|
61589
61965
|
draft: z7.boolean().optional()
|
|
61590
61966
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
@@ -61632,6 +62008,10 @@ var init_feature = __esm(() => {
|
|
|
61632
62008
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
61633
62009
|
return reviewCommitCount(projectForFeature(ctx, feature), feature);
|
|
61634
62010
|
}),
|
|
62011
|
+
mergeDelta: publicProcedure.input(z7.object({ featureId: z7.string() })).query(({ ctx, input }) => {
|
|
62012
|
+
const feature = getFeatureRow(ctx, input.featureId);
|
|
62013
|
+
return mergeDelta(projectForFeature(ctx, feature), feature);
|
|
62014
|
+
}),
|
|
61635
62015
|
merge: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(async ({ ctx, input }) => {
|
|
61636
62016
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
61637
62017
|
requireNotDraft(feature);
|
|
@@ -61639,9 +62019,17 @@ var init_feature = __esm(() => {
|
|
|
61639
62019
|
if (activeTestDriveFeatureId() === feature.id) {
|
|
61640
62020
|
await testDrive(ctx, project3, feature, "stop");
|
|
61641
62021
|
}
|
|
61642
|
-
await
|
|
62022
|
+
const delta = await mergeDelta(project3, feature);
|
|
62023
|
+
const standingConflict = unresolvedMergeConflict(listAfter(ctx, feature.id, 0));
|
|
61643
62024
|
const res = await mergeFeature(project3, feature);
|
|
61644
62025
|
if (res.ok) {
|
|
62026
|
+
if (standingConflict) {
|
|
62027
|
+
emit(ctx, input.featureId, {
|
|
62028
|
+
type: "merge.resolved",
|
|
62029
|
+
message: "conflict retired by the merge"
|
|
62030
|
+
});
|
|
62031
|
+
}
|
|
62032
|
+
await promoteOutcomeDoc(ctx, project3, feature, res.target, delta);
|
|
61645
62033
|
setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${res.target}`);
|
|
61646
62034
|
setFeatureStatus(ctx, input.featureId, "shipped");
|
|
61647
62035
|
} else {
|
|
@@ -61657,9 +62045,9 @@ var init_feature = __esm(() => {
|
|
|
61657
62045
|
});
|
|
61658
62046
|
|
|
61659
62047
|
// src/services/fsbrowse.ts
|
|
61660
|
-
import { existsSync as existsSync24, readdirSync as readdirSync6, statSync as
|
|
62048
|
+
import { existsSync as existsSync24, readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
61661
62049
|
import { homedir as homedir4 } from "os";
|
|
61662
|
-
import { basename as
|
|
62050
|
+
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute3, join as join26, resolve as resolve12, sep as sep4 } from "path";
|
|
61663
62051
|
function expandPath(input, home = homedir4()) {
|
|
61664
62052
|
const trimmed = input.trim();
|
|
61665
62053
|
if (trimmed === "")
|
|
@@ -61711,7 +62099,7 @@ function crumbsFor(dir) {
|
|
|
61711
62099
|
const out = [];
|
|
61712
62100
|
let cur = dir;
|
|
61713
62101
|
for (;; ) {
|
|
61714
|
-
out.unshift({ name:
|
|
62102
|
+
out.unshift({ name: basename3(cur) || cur, path: cur });
|
|
61715
62103
|
const parent = dirname14(cur);
|
|
61716
62104
|
if (parent === cur)
|
|
61717
62105
|
break;
|
|
@@ -61733,7 +62121,7 @@ function browseDir(input, showHidden = false, home = homedir4()) {
|
|
|
61733
62121
|
}
|
|
61734
62122
|
let dir = target;
|
|
61735
62123
|
try {
|
|
61736
|
-
if (!
|
|
62124
|
+
if (!statSync8(target).isDirectory())
|
|
61737
62125
|
dir = dirname14(target);
|
|
61738
62126
|
} catch (e) {
|
|
61739
62127
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
|
|
@@ -61763,7 +62151,7 @@ function browseDir(input, showHidden = false, home = homedir4()) {
|
|
|
61763
62151
|
let isDirectory = dirent.isDirectory();
|
|
61764
62152
|
if (isSymlink) {
|
|
61765
62153
|
try {
|
|
61766
|
-
isDirectory =
|
|
62154
|
+
isDirectory = statSync8(path2).isDirectory();
|
|
61767
62155
|
} catch {
|
|
61768
62156
|
continue;
|
|
61769
62157
|
}
|
|
@@ -61901,38 +62289,72 @@ function withoutKickoff(turns, kind, runtime5) {
|
|
|
61901
62289
|
const kickoff = kickoffLineFor(kind, undefined, runtime5);
|
|
61902
62290
|
return turns.filter((turn) => !(turn.role === "user" && promptMatchesKickoff(kickoff, turn.text)));
|
|
61903
62291
|
}
|
|
62292
|
+
function saidText(text3) {
|
|
62293
|
+
const trimmed = text3.trim();
|
|
62294
|
+
if (trimmed.startsWith("<command-name>"))
|
|
62295
|
+
return null;
|
|
62296
|
+
if (trimmed === INTERRUPTED)
|
|
62297
|
+
return null;
|
|
62298
|
+
return trimmed.replace(/\[Image #\d+\]/g, " ").trim() || null;
|
|
62299
|
+
}
|
|
61904
62300
|
function deriveTitle(turns, runtime5) {
|
|
61905
|
-
const
|
|
61906
|
-
|
|
62301
|
+
for (const turn of withoutKickoff(turns, "project", runtime5)) {
|
|
62302
|
+
if (turn.role !== "user")
|
|
62303
|
+
continue;
|
|
62304
|
+
const said = saidText(turn.text);
|
|
62305
|
+
if (said)
|
|
62306
|
+
return elide(said);
|
|
62307
|
+
}
|
|
62308
|
+
return null;
|
|
61907
62309
|
}
|
|
61908
62310
|
function elide(text3) {
|
|
61909
62311
|
const line = text3.replace(/\s+/g, " ").trim();
|
|
61910
|
-
if (line.length <=
|
|
62312
|
+
if (line.length <= TITLE_MAX)
|
|
61911
62313
|
return line;
|
|
61912
|
-
return `${line.slice(0,
|
|
62314
|
+
return `${line.slice(0, TITLE_MAX).trimEnd()}\u2026`;
|
|
61913
62315
|
}
|
|
61914
|
-
function
|
|
61915
|
-
return
|
|
62316
|
+
function junkTitle(title) {
|
|
62317
|
+
return title.startsWith("<command-name>") || title.startsWith("[");
|
|
61916
62318
|
}
|
|
61917
62319
|
function listProjectConversations(ctx, projectId) {
|
|
61918
|
-
return projectSessions(ctx, projectId, "project").map((
|
|
61919
|
-
const
|
|
61920
|
-
const
|
|
61921
|
-
|
|
61922
|
-
if (!title) {
|
|
61923
|
-
title = deriveTitle(readTranscript2(session.transcriptPath, runtime5).turns, runtime5);
|
|
61924
|
-
if (title)
|
|
61925
|
-
setSessionTitle(ctx, session.id, title);
|
|
61926
|
-
}
|
|
62320
|
+
return groupByConversation(projectSessions(ctx, projectId, "project")).map((group) => {
|
|
62321
|
+
const [latest] = group;
|
|
62322
|
+
const earliest = group[group.length - 1];
|
|
62323
|
+
const stamps = group.map((s) => s.createdAt).filter((at) => at !== undefined);
|
|
61927
62324
|
return {
|
|
61928
|
-
id:
|
|
61929
|
-
title:
|
|
61930
|
-
createdAt,
|
|
61931
|
-
status:
|
|
61932
|
-
resumable:
|
|
62325
|
+
id: latest.id,
|
|
62326
|
+
title: conversationTitle(ctx, earliest),
|
|
62327
|
+
createdAt: stamps.length ? Math.min(...stamps) : null,
|
|
62328
|
+
status: latest.status,
|
|
62329
|
+
resumable: true
|
|
61933
62330
|
};
|
|
61934
62331
|
});
|
|
61935
62332
|
}
|
|
62333
|
+
function groupByConversation(rows) {
|
|
62334
|
+
const groups = new Map;
|
|
62335
|
+
for (const row of rows) {
|
|
62336
|
+
if (!row.ccSessionId)
|
|
62337
|
+
continue;
|
|
62338
|
+
const group = groups.get(row.ccSessionId);
|
|
62339
|
+
if (group)
|
|
62340
|
+
group.push(row);
|
|
62341
|
+
else
|
|
62342
|
+
groups.set(row.ccSessionId, [row]);
|
|
62343
|
+
}
|
|
62344
|
+
return [...groups.values()];
|
|
62345
|
+
}
|
|
62346
|
+
function conversationTitle(ctx, earliest) {
|
|
62347
|
+
const cached5 = earliest.title;
|
|
62348
|
+
if (cached5 && !junkTitle(cached5))
|
|
62349
|
+
return cached5;
|
|
62350
|
+
const runtime5 = earliest.runtime ?? DEFAULT_RUNTIME;
|
|
62351
|
+
const derived = deriveTitle(readTranscript2(earliest.transcriptPath, runtime5).turns, runtime5);
|
|
62352
|
+
if (derived)
|
|
62353
|
+
setSessionTitle(ctx, earliest.id, derived);
|
|
62354
|
+
else if (cached5)
|
|
62355
|
+
clearSessionTitle(ctx, earliest.id);
|
|
62356
|
+
return derived ?? UNTITLED;
|
|
62357
|
+
}
|
|
61936
62358
|
function conversationTranscript(ctx, sessionId) {
|
|
61937
62359
|
const session = getSessionRow(ctx, sessionId);
|
|
61938
62360
|
if (!session)
|
|
@@ -61945,7 +62367,7 @@ function conversationTranscript(ctx, sessionId) {
|
|
|
61945
62367
|
runtime: runtime5
|
|
61946
62368
|
};
|
|
61947
62369
|
}
|
|
61948
|
-
var
|
|
62370
|
+
var TITLE_MAX = 60, INTERRUPTED = "[Request interrupted by user]", UNTITLED = "Untitled";
|
|
61949
62371
|
var init_conversations = __esm(() => {
|
|
61950
62372
|
init_src();
|
|
61951
62373
|
init_sessions();
|
|
@@ -61954,7 +62376,7 @@ var init_conversations = __esm(() => {
|
|
|
61954
62376
|
|
|
61955
62377
|
// src/services/projects.ts
|
|
61956
62378
|
import { existsSync as existsSync25 } from "fs";
|
|
61957
|
-
import { basename as
|
|
62379
|
+
import { basename as basename4, join as join27 } from "path";
|
|
61958
62380
|
import { eq as eq15, isNull } from "drizzle-orm";
|
|
61959
62381
|
function listProjects(ctx) {
|
|
61960
62382
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
@@ -61973,7 +62395,7 @@ function warnIfTranslatedMount(ctx, projectId, repoPath) {
|
|
|
61973
62395
|
async function openProject(ctx, rawPath) {
|
|
61974
62396
|
const repoPath = expandPath(rawPath);
|
|
61975
62397
|
await assertRepoTolerant(ctx, repoPath);
|
|
61976
|
-
const name =
|
|
62398
|
+
const name = basename4(repoPath) || repoPath;
|
|
61977
62399
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
61978
62400
|
if (existing) {
|
|
61979
62401
|
ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq15(projects.id, existing.id)).run();
|
|
@@ -62066,7 +62488,9 @@ var init_project = __esm(() => {
|
|
|
62066
62488
|
list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
|
|
62067
62489
|
roots: publicProcedure.query(() => listRoots()),
|
|
62068
62490
|
browse: publicProcedure.input(z8.object({ dir: z8.string().optional(), showHidden: z8.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
|
|
62069
|
-
branches: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId)
|
|
62491
|
+
branches: publicProcedure.input(z8.object({ projectId: z8.string(), includeFeatureBranches: z8.boolean().optional() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId), {
|
|
62492
|
+
includeFeatureBranches: input.includeFeatureBranches
|
|
62493
|
+
})),
|
|
62070
62494
|
sessionBranch: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => sessionBranchView(requireProjectById(ctx, input.projectId))),
|
|
62071
62495
|
open: publicProcedure.input(z8.object({ repoPath: z8.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
|
|
62072
62496
|
close: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
|
|
@@ -62104,16 +62528,52 @@ var init_review_findings2 = __esm(() => {
|
|
|
62104
62528
|
});
|
|
62105
62529
|
});
|
|
62106
62530
|
|
|
62531
|
+
// src/services/runs.ts
|
|
62532
|
+
import { and as and12, asc as asc6, eq as eq16, isNotNull as isNotNull3 } from "drizzle-orm";
|
|
62533
|
+
function runTicketIds(ctx, runId) {
|
|
62534
|
+
const rows = ctx.db.select({ ticketId: events.ticketId }).from(events).where(and12(eq16(events.runId, runId), isNotNull3(events.ticketId))).orderBy(asc6(events.id)).all();
|
|
62535
|
+
const seen = new Set;
|
|
62536
|
+
for (const row of rows)
|
|
62537
|
+
if (row.ticketId)
|
|
62538
|
+
seen.add(row.ticketId);
|
|
62539
|
+
return [...seen];
|
|
62540
|
+
}
|
|
62541
|
+
function runLap(ctx, runId) {
|
|
62542
|
+
const row = ctx.db.select({ lap: events.lap }).from(events).where(eq16(events.runId, runId)).orderBy(asc6(events.id)).limit(1).get();
|
|
62543
|
+
return row?.lap ?? 1;
|
|
62544
|
+
}
|
|
62545
|
+
function listRunSummaries(ctx, featureId) {
|
|
62546
|
+
return listRunsByFeature(ctx, featureId).map((run7) => ({
|
|
62547
|
+
id: run7.id,
|
|
62548
|
+
status: run7.status,
|
|
62549
|
+
startedAt: run7.startedAt,
|
|
62550
|
+
...run7.endedAt === undefined ? {} : { endedAt: run7.endedAt },
|
|
62551
|
+
lap: runLap(ctx, run7.id),
|
|
62552
|
+
ticketIds: runTicketIds(ctx, run7.id)
|
|
62553
|
+
}));
|
|
62554
|
+
}
|
|
62555
|
+
function getRunWithTickets(ctx, runId) {
|
|
62556
|
+
const run7 = getRunRow(ctx, runId);
|
|
62557
|
+
return { ...run7, tickets: listByIds(ctx, runTicketIds(ctx, runId)) };
|
|
62558
|
+
}
|
|
62559
|
+
var init_runs = __esm(() => {
|
|
62560
|
+
init_schema();
|
|
62561
|
+
init_repo();
|
|
62562
|
+
init_tickets();
|
|
62563
|
+
});
|
|
62564
|
+
|
|
62107
62565
|
// src/trpc/routers/run.ts
|
|
62108
62566
|
import * as z10 from "zod";
|
|
62109
62567
|
var runRouter;
|
|
62110
62568
|
var init_run = __esm(() => {
|
|
62111
62569
|
init_agent_stream();
|
|
62112
62570
|
init_repo();
|
|
62571
|
+
init_runs();
|
|
62113
62572
|
init_runner();
|
|
62114
62573
|
init_context();
|
|
62115
62574
|
runRouter = router({
|
|
62116
|
-
get: publicProcedure.input(z10.object({ runId: z10.string() })).query(({ ctx, input }) =>
|
|
62575
|
+
get: publicProcedure.input(z10.object({ runId: z10.string() })).query(({ ctx, input }) => getRunWithTickets(ctx, input.runId)),
|
|
62576
|
+
listByFeature: publicProcedure.input(z10.object({ featureId: z10.string() })).query(({ ctx, input }) => listRunSummaries(ctx, input.featureId)),
|
|
62117
62577
|
agentTranscript: publicProcedure.input(z10.object({ ticketId: z10.string(), after: z10.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
62118
62578
|
cancel: publicProcedure.input(z10.object({ runId: z10.string() })).mutation(({ ctx, input }) => {
|
|
62119
62579
|
getRunRow(ctx, input.runId);
|
|
@@ -62137,7 +62597,7 @@ var init_settings2 = __esm(() => {
|
|
|
62137
62597
|
});
|
|
62138
62598
|
|
|
62139
62599
|
// src/trpc/routers/setup.ts
|
|
62140
|
-
import { isNotNull as
|
|
62600
|
+
import { isNotNull as isNotNull4 } from "drizzle-orm";
|
|
62141
62601
|
import * as z12 from "zod";
|
|
62142
62602
|
var setupRouter;
|
|
62143
62603
|
var init_setup2 = __esm(() => {
|
|
@@ -62151,7 +62611,7 @@ var init_setup2 = __esm(() => {
|
|
|
62151
62611
|
init_context();
|
|
62152
62612
|
setupRouter = router({
|
|
62153
62613
|
doctor: publicProcedure.query(({ ctx }) => {
|
|
62154
|
-
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(
|
|
62614
|
+
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(isNotNull4(projects.model)).all().map((p) => p.model);
|
|
62155
62615
|
return runDoctor({
|
|
62156
62616
|
exec: createSystemExec(),
|
|
62157
62617
|
runtimes: configuredRuntimes(ctx.config, projectModels),
|
|
@@ -62241,18 +62701,27 @@ var testNotesRouter;
|
|
|
62241
62701
|
var init_test_notes2 = __esm(() => {
|
|
62242
62702
|
init_test_notes();
|
|
62243
62703
|
init_context();
|
|
62704
|
+
init_errors();
|
|
62244
62705
|
testNotesRouter = router({
|
|
62245
62706
|
list: publicProcedure.input(z14.object({ featureId: z14.string() })).query(({ ctx, input }) => listByFeature4(ctx, input.featureId)),
|
|
62246
62707
|
add: publicProcedure.input(z14.object({
|
|
62247
62708
|
featureId: z14.string(),
|
|
62248
62709
|
text: z14.string().min(1),
|
|
62249
|
-
videoTimestamp: z14.number().nonnegative().optional()
|
|
62250
|
-
|
|
62710
|
+
videoTimestamp: z14.number().nonnegative().optional(),
|
|
62711
|
+
reviewTicketId: z14.string().optional()
|
|
62712
|
+
})).mutation(({ ctx, input }) => {
|
|
62713
|
+
if (input.videoTimestamp !== undefined && !input.reviewTicketId)
|
|
62714
|
+
throw new InvalidInputError("a video timestamp must identify its review ticket");
|
|
62715
|
+
return addNote(ctx, input.featureId, input.text, "human", input.videoTimestamp, input.reviewTicketId);
|
|
62716
|
+
}),
|
|
62251
62717
|
edit: publicProcedure.input(z14.object({ noteId: z14.string(), text: z14.string().min(1) })).mutation(({ ctx, input }) => editNote(ctx, input.noteId, input.text)),
|
|
62252
62718
|
remove: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => deleteNote(ctx, input.noteId)),
|
|
62253
62719
|
toggle: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => toggleNote(ctx, input.noteId)),
|
|
62254
62720
|
promote: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => promoteNote(ctx, input.noteId)),
|
|
62255
|
-
promoteMany: publicProcedure.input(z14.object({ noteIds: z14.array(z14.string()).min(1) })).mutation(({ ctx, input }) => promoteMany(ctx, input.noteIds))
|
|
62721
|
+
promoteMany: publicProcedure.input(z14.object({ noteIds: z14.array(z14.string()).min(1) })).mutation(({ ctx, input }) => promoteMany(ctx, input.noteIds)),
|
|
62722
|
+
reopen: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => reopenNote(ctx, input.noteId)),
|
|
62723
|
+
triage: publicProcedure.input(z14.object({ featureId: z14.string(), quickFixIds: z14.array(z14.string()), quickFixFindingIds: z14.array(z14.string()), dismissIds: z14.array(z14.string()), carry: z14.boolean() })).mutation(({ ctx, input }) => triageNotes(ctx, input.featureId, input)),
|
|
62724
|
+
triagePreview: publicProcedure.input(z14.object({ featureId: z14.string() })).query(({ ctx, input }) => triagePreview(ctx, input.featureId))
|
|
62256
62725
|
});
|
|
62257
62726
|
});
|
|
62258
62727
|
|
|
@@ -62278,6 +62747,7 @@ var init_ticket = __esm(() => {
|
|
|
62278
62747
|
return { stopped: false, swept: false };
|
|
62279
62748
|
}),
|
|
62280
62749
|
cancel: publicProcedure.input(z15.object({ ticketId: z15.string(), reason: z15.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
|
|
62750
|
+
durationStats: publicProcedure.input(z15.object({ projectId: z15.string() })).query(({ ctx, input }) => ticketDurationStats(ctx, input.projectId)),
|
|
62281
62751
|
edit: publicProcedure.input(z15.object({
|
|
62282
62752
|
ticketId: z15.string(),
|
|
62283
62753
|
title: z15.string().min(1).optional(),
|
|
@@ -62323,14 +62793,14 @@ var init_router = __esm(() => {
|
|
|
62323
62793
|
});
|
|
62324
62794
|
|
|
62325
62795
|
// src/workflows/reconcile-runs.ts
|
|
62326
|
-
import { eq as
|
|
62796
|
+
import { eq as eq17 } from "drizzle-orm";
|
|
62327
62797
|
async function reconcileStaleRuns(ctx) {
|
|
62328
|
-
const stale = ctx.db.select().from(runs).where(
|
|
62798
|
+
const stale = ctx.db.select().from(runs).where(eq17(runs.status, "running")).all().map(rowToRun);
|
|
62329
62799
|
const reconciled = [];
|
|
62330
62800
|
for (const run7 of stale) {
|
|
62331
62801
|
if (isRunActive(run7.id))
|
|
62332
62802
|
continue;
|
|
62333
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(
|
|
62803
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq17(runs.id, run7.id)).run();
|
|
62334
62804
|
const released = releaseForSession(ctx, run7.id);
|
|
62335
62805
|
const swept = workflowClaimsFeatureBranch(run7.workflow) ? sweepOrphanedBurning(ctx, run7.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
62336
62806
|
if (workflowClaimsFeatureBranch(run7.workflow)) {
|