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